با Firebase Crashlytics، ردپای حافظه برنامهتان را کارآمد و بدون خطای segfault نگه دارید. این جلسه به بررسی مجموعه جامع ابزارهای عیبیابی حافظه میپردازد که در کنسول Firebase و Android Studio دردسترس است. ابتدا نحوه استفاده از اطلاعات آماری ANR و گزارشهای GWP-ASan را برای تشخیص اشکالات حافظه گریزان پوشش میدهیم و سپس به جدیدترین ابزارهایمان برای رویدادهای «کمبود حافظه» (OOM) و «کشتن محدودکننده حافظه» (MLK) میپردازیم که بهطور خاص برای کمک به شما در پیمایش و سازگاری با محدودیتهای جدید حافظه معرفیشده در Android 17 طراحی شدهاند.
ApplicationExitInfo را پُرسمان میکند تا ردگیریهای چندرشتهای را نمایش دهد و رشتههای مسدودکننده ریشه را شناسایی کند.ProfilingManager Android 17 ادغام میشود تا تصویرهای کلی پشته ابهامزداییشده را جمعآوری کند که میتوان آنها را در Perfetto یا ازطریق CLI Android بررسی کرد.آوانویسی
Polina Govorkova
سلام به همه.
به عیبیابی حافظه با Firebase Crashlytics خوش آمدید.
صبر کن، چه عنوان شرکتیای.
بیایید کمی آن را تغییر دهیم.
Keeping apps lean and mean with Firebase Crashlytics.
اوه، این بهتر بهنظر میرسد.
نام من پولینا است و من توسعهدهنده نرمافزار ارشد در تیم Firebase Crashlytics و ارائهدهنده شما در امروز هستم.
در نقطهای از چرخه توسعه، احساس خواهید کرد که همه اشکالها را حل کردهاید.
آن غلطهای املایی تجزیه JSON، بررسیهای فراموششده اشارهگر تهی، همه ازبین رفتهاند!
و شاید از خودتان بپرسید «کارم تمام شده است؟»
خب، من خبر بدی برای شما دارم، چون شما اینطور نیستید.
چون چیزی که برای شما باقی میماند، درواقع، مشکلات سخت است.
و من امروز ادعا میکنم که دو دسته از سختترین مشکلاتی که با آنها مواجه خواهید شد، پاسخگویی و حافظه خواهند بود.
خبر خوب اینکه ما هوایتان را داریم.
امروز، ابزارهای موجود و کاملاً جدید در Android و Crashlytics را بررسی خواهیم کرد تا به شما کمک کنیم علت ریشهای مشکلات پاسخدهی را که ازطریق خطاهای «برنامه پاسخ نمیدهد»، استثناهای کمبود حافظه، نشت حافظه، و خرابی حافظه بومی ظاهر میشوند پیدا کنید.
بیایید شروع کنیم.
چرا مشکلات پاسخدهی و حافظه اینقدر بدنام هستند؟
یک اشکال کلاسیک را درنظر بگیرید: شما یک رشته را تعریف میکنید، آن را روی null تنظیم میکنید، و در نقطهای، سعی میکنید طول آن را بخوانید.
چه چیزی دریافت میکنید؟
یک NullPointerException قدیمی خوب.
اما درواقع کار در اینجا آسان است، زیرا ردیابی پشته استثنا دقیقاً به خطی از کد اشاره میکند که باعث خرابی برنامهتان شده است.
به آن ردیابی پشته در سمت چپ نگاه کنید.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainمیتوانیم فوراً بگوییم که مشکل دقیقاً در خط ۴۲ در MainActivity.kt است.
و این رویکرد برای اکثر اشکالهای جزئی صادق است.
اکنون، وقتی مشکلی در پاسخگویی یا حافظه رخ میدهد، مانند این «برنامه پاسخ نمیدهد» در سمت راست، ردیابی پشته تقریباً هرگز به مقصر واقعی اشاره نمیکند.
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainدر بهترین حالت، فقط اثر را به شما نشان میدهد که یک رشته یا تخصیص بیخطر است که بهطور اتفاقی در زمان توقف برنامه یا اتمام حافظه درحال اجرا بوده است.
درحالیکه علت ریشهای واقعی خیلی زودتر یا شاید در جایی کاملاً متفاوت اتفاق افتاده است.
خوشبختانه وقتی نوبت به این مشکلات میرسد، لازم نیست در تاریکی حدس بزنید، زیرا Firebase Crashlytics پاسخ را برای ما دارد.
اما قبلاز اینکه آستینهایمان را برای عیبیابی بالا بزنیم، اگر با Crashlytics آشنا هستید دستتان را بالا ببرید.
اوه، عالی است.
دیدن همه آن دستها را دوست دارم.
بنابراین، برای کسانی که تازه به اینجا آمدهاند، Firebase Crashlytics یک راهکار گزارش خطا در زمان واقعی است که دادههای میدانی را همراه با بینشهای زمینهای و مبتنی بر هوش مصنوعی، نکات عیبیابی، و سیگنالهای واضح درباره نسخههای جدید شما ارائه میدهد، همه اینها برای کمک به شما در رفع سریعتر مشکلات است.
و Crashlytics مورد علاقه توسعهدهندگان ما است و بهعنوان پرکاربردترین راهکار گزارش خرابی در تلفن همراه شناخته میشود که بیشاز ۳۷۰٬۰۰۰ برنامه بومی، ازجمله ۵۷٪ از برنامههای برتر Android را پشتیبانی میکند.
برای اینکه ببینید Crashlytics چگونه به ما کمک میکند تا اشکالات را در عمل ردیابی کنیم، بیایید به یک مثال واقعی نگاه کنیم.
امروز میخواهم داستان ChromaClip را برایتان بگویم.
این یک استخراجکننده پالت رنگ ویدیو است که من درحال ساخت آن هستم و میخواهم به شما بگویم که چگونه در مسیر توسعه آن با مشکلات پاسخگویی و حافظه مقابله کردم.
بنابراین، ایده پشت ChromaClip ساده است: کاربران ویدیویی را انتخاب میکنند، شاید کلیپی از یک غروب زیبا یا صحنهای با درجهبندی رنگ عالی، و برنامه فریمها را تجزیهوتحلیل میکند تا یک پالت رنگ سفارشی ایجاد کند، که سپس کاربران میتوانند از آن برای طراحیها، هنر دیجیتال، تابلوهای خلقوخو، یا هر چیز دیگری استفاده کنند.
بنابراین، داستان ما درست قبلاز انتشار نسخه ۱.۰ من شروع میشود.
میخواستم مطمئن شوم که قبلاز راهاندازی کاملاً بینقص است، بنابراین آزمایشهای نهایی را بهصورت محلی در چند دستگاه اجرا میکردم، ویدیو را انتخاب میکردم، روی دکمه ضربه میزدم تا پالت رنگ را استخراج کنم، و، از شانس من، دیالوگ موردعلاقه هر توسعهدهنده Android، اینطور نیست؟
ChromaClip پاسخ نمیدهد.
بنابراین، این خطای «برنامه پاسخ نمیدهد» است که به آن ANR هم میگویند.
Polina Govorkova
بنابراین، در این شرایط، بهجز وحشتزده شدن، چه کاری انجام میدهیم؟
طبق اولین غریزهمان، چون تلفن آزمایشی مستقیماً به رایانه کیفی وصل است، «استودیو Android» را باز میکنیم و Logcat را بررسی میکنیم.
خب، ردیابی پشته را برای رشته اصلی میبینیم.
14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)درواقع، این به ما چه میگوید؟
ابتدا، درست در بالا، تأییدیه دریافت میکنیم که این یک ANR است که در MainActivity اجرا میشود.
دیگر چه؟
دلیل آن را میبینیم که مهلت ارسال ورودی بهپایان رسیده است.
این یعنی کاربر روی صفحه تکضرب زده است.
بله، همینطور است.
اما رشته اصلی آنقدر مشغول بود که نتوانست آن رویداد را در مهلت پنج ثانیهای ناظر Android پردازش کند.
سپس، میبینیم که سیستم فایل را در traces.txt تخلیه کرده است و درست در زیر آن، logcat رد پشته را برای رشته اصلی ما چاپ میکند.
توجه کنید که در آنجا، حالت برابر با S است.
رشته اصلی ما حتی بهطور فعال از CPU استفاده نمیکند.
S بهمعنی خوابیدن است.
چرا رشته اصلی ما درحالیکه منتظریم در حالت خواب است؟
بیایید نگاهی دقیقتر به چارچوبهای پشتهای بیندازیم.
درست در اینجا در وسط ردیابی، میبینیم که رشته اصلی ما در داخل Unsafe.park پارک شده است و منتظر یک شیء ناشناخته است.
چرا نمیدانیم منتظر چه چیزی است؟
خب، درواقع درست است که برای برخیاز خطاهای ANR، مثلاً وقتی رشتهای توسط قفل همگامسازیشده جاوا کلاسیک مسدود میشود، ردگیری پشته رشته اصلی آنقدر خوب است که به ما بگوید درواقع منتظر چه چیزی است و چه چیزی قفل را نگه داشته است.
اما وقتی از یک سطح بالاتر همزمانی اولیه در پشت صحنه استفاده میکنید، تمام چیزی که logcat به ما میگوید این است که ما در انتظار یک شیء ناشناخته هستیم.
بنابراین، ما نمیدانیم که منتظر کدام شیء هستیم، بهطور خاص منتظر کدام رشته عامل پسزمینه هستیم، یا چرا بیشاز ۵ ثانیه طول میکشد.
و چون بهصورت محلی آزمایش میکنیم، میتوانیم آن traces.txt را از دستگاه بیرون بکشیم و بهصورت دستی دهها رشته غیرفعال را پیمایش کنیم و احتمالاً مقصر را پیدا کنیم یا شاید با پرسیدن از هوش مصنوعی، توکنها را بسوزانیم.
در برخی موارد، واقعاً کار میکند و درحقیقت، برای این مورد کار میکند.
اما نه تنها واقعاً نمیخواهم تخلیه رشته خام را بهصورت دستی بخوانم - صادقانه بگویم، این کار خیلی قدیمی است - بلکه میدانم که این رویکرد اصلاً زمانی که در مرحله تولید هستیم، قابل اجرا نیست.
ما دسترسی مستقیم به فایل traces.txt در تلفنهای کاربر نداریم، درست است؟
پس خوب است که با Firebase Crashlytics ادغام شدهایم.
زیرا نیاز به جستجو را برطرف میکند و حتی در تولید به ریشه ANR میرسد.
چگونه؟
اینجاست که میانای برنامهسازی کاربردی ApplicationExitInfo ویژه Android وارد عمل میشود.
از Android 11، هرگاه سیستم یک ANR را پرچمگذاری کند یا فرایند برنامه شما را خاتمه دهد، سیستمعامل فرادادههای خروج غنی را ثبت میکند، ازجمله دلیل دقیق ازکارافتادن، وضعیت حافظه، و ردیابی کامل سیستم برای هر رشتهای که در آن لحظه در فرایند شما درحال اجرا است.
و بهترین بخش این است که نیازی به نصب افزونههای اضافی یا نوشتن حتی یک خط کد ندارید.
تا زمانی که از کیت توسعه نرمافزار Crashlytics نسخه ۱۸.۳.۵ یا بالاتر استفاده میکنید، این ویژگی بهطور پیشفرض کار میکند.
بنابراین، بهمحض بازراهاندازی برنامه، کیت توسعه نرمافزار Crashlytics بهطور خودکار ApplicationExitInfo را جستجو میکند، نسخه کامل چندرشتهای را همراه با دورسنجی دستگاه ضبط میکند، و آن را به پسبخش Crashlytics ارسال میکند.
بیایید کنسول Crashlytics را باز کنیم و ببینیم چه چیزی به ما میگوید.
این نمای جزئیات مشکل کنسول Firebase Crashlytics برای ANR است و ما بلافاصله در زیر برگه ردیابی پشته، DefaultDispatcher worker one را میبینیم.
DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)این درواقع همان رشتهای است که Crashlytics در بالاترین قسمت نشان داده است و گلوگاه رشته اصلی ما است، و Crashlytics آن را با نشان «مسدود کردن ریشه» علامتگذاری میکند.
همانطور که نکته ابزار روی نشان توضیح میدهد، این رشته عامل درواقع همان رشتهای است که رشته راهانداز ANR را مسدود میکند.
ضمناً Crashlytics نشانهای هوشمندی برای سایر مقصران نیز دارد.
بنابراین، اگر رشته درحال خواندن یک فایل بزرگ یا اجرای یک پُرسمان پایگاه داده بسیار کند گیر کرده باشد، درصورت ایجاد خطای ANR، رشته بهعنوان مسدودکننده ریشه ورودی/خروجی برچسبگذاری میشود.
و اگر دو رشته درانتظار یکدیگر گیر کرده باشند، Crashlytics آنها را بهعنوان بنبست برچسبگذاری میکند.
اکنون، بیایید به خود چارچوبهای پشتهای در رشته مسدودکننده ریشه نگاه کنیم.
بنابراین، PaletteExtractor processVideo ما FrameDecoder getUncompressedFrame را فراخوانی میکند که با رنگ آبی در آنجا برجسته شده است، و کاملاً در داخل createBitmap گیر کرده است و فقط سعی میکند حافظه را برای یک فریم اختصاص دهد.
ازآنجاییکه میدانیم منتظر قفل دیگری نیست و میدانیم که در تماس شبکهای نیست، مظنون اصلی ما باید حافظه باشد و درواقع میتوانیم فوراً با بررسی حافظه آزاد دستگاه درست در همینجا در کنسول Crashlytics آن را تأیید کنیم.
به دو جایی که Crashlytics این را نشان میدهد نگاه کنید.
در پایین برگه داده برای رویداد خاص درست در کنار برگه ردیابی پشتهای که همین الان درحال بررسی آن بودیم، و همچنین در بالا سمت چپ که میانگین را در همه رویدادها نشان میدهد.
و در هر دو مورد، اوه، بهنظر میرسد که تقریباً هیچ چیزی وجود ندارد، حتی ۱۰۰ کیلوبایت.
بنابراین، بیایید فکر کنیم.
دلیل این اتفاق این است که چون فرایند با کمبود حافظه مواجه شده بود، زمان اجرای Android تمام وقت خود را صرف اجرای دیوانهوار جمعآوری زباله کرد تا فضای کافی برای نقشه بیتی بعدی پیدا کند، اما هرگز آن را بهدست نیاورد.
متوقف کردن رشته عامل ما و محروم کردن واسط کاربر برای بیشاز پنج ثانیه.
بنابراین، در اینجا یک نکته سریع برای شما آورده شده است.
قبلاز اینکه، هروقت ANR همراه با این نوع، RAM آزاد کم دریافت کنید، این مدرک قطعی شما است که ANR درواقع یا نشت حافظه است یا مشکل تورم.
اکنون سؤال این است که آیا برنامه ما قربانی است یا اینکه خودش هم حافظه را احتکار میکند؟
بنابراین، بیایید به فهرست مشکلات پروژهمان برگردیم و ببینیم آیا چیز دیگری وجود دارد که مرتبط بهنظر برسد.
Polina Govorkova
و درست آنجا را نگاه کنید.
در پایین، فهرست مشکلات را میبینیم و درست بالای ANR ما با هشت رویداد، استثنای OutOfMemory را داریم.
بنابراین، ممکن است درنهایت ما مقصر باشیم.
و این درواقع سرنخ بزرگی است.
چون در Android، OutOfMemoryError Java، یا بهاختصار OOM، فقط بهمعنای کمبود حافظه دسترسی تصادفی در تلفن نیست.
این یعنی فرایند برنامه خودمان از حد مجاز پشته Java زمان اجرای Android فراتر رفته است.
بنابراین، در این مرحله، نظریه کاری من این است که ANR و OOM ما درواقع دو روی یک سکه هستند.
بسته به زمانبندی درطول اجرای آزمایشی، ChromaClip یا برای مدت طولانی در داخل جمعآوری زباله گیر میکرد و باعث ایجاد ANR میشد، یا توده را کاملاً تمام میکرد و منجر به کمبود حافظه میشد.
بیایید استثنای OutOfMemory را بررسی کنیم تا مطمئن شویم.
شروع با ردیابی پشتهای.
java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)یک لحظه صبر کنید.
Bitmap.createBitmap، این آشنا است.
قبلاً آن را ندیده بودیم؟
البته، بهجز اینکه ببینید این بار از کجا تماس گرفته شده است.
هنگام باز کردن فعالیت تنظیمات، ۲۵۶ بایت اختصاص داده نشد.
دوباره داریم در همان تله میافتیم.
اگر برنامهتان بهدلیل اینکه نمیتواند نماد تنظیمات کوچکی را بزرگ کند ازکار بیفتد، فعالیت تنظیمات مقصر نخواهد بود.
این فقط تماسگیرنده بدشانسی است که وقتی حافظهای وجود نداشت، درخواست یک قطره حافظه کرد.
بنابراین، برای پیدا کردن مقصر واقعی، باید نمای کلی را ببینیم و همه چیزهایی را که هنگام ازکارافتادن در حافظه قرار داشتند بررسی کنیم.
بهعبارت دیگر، به رونوشت پشته جاوا نیاز داریم.
اگر میدانید چگونه کمبود حافظه را راهاندازی کنید، میتوانید بهصورت دستی یک رونوشت پشته Java را با تحلیلگر حافظه Android Studio دریافت کنید.
اما وقتی کمبود حافظه بهطور غیرقابلپیشبینی درطول اجرای آزمایش یا در محیط واقعی اتفاق میافتد، بازهم این روش کارساز نخواهد بود.
Polina Govorkova
بنابراین، این یکی دیگر از لحظات درخشان Firebase Crashlytics است.
علاوهبر اطلاعات خروج از برنامه، Crashlytics اکنون با ProfilingManager API در Android ادغام میشود.
ProfilingManager API در Android 17 با نمایهسازی حافظه راهاندازیشده توسط سیستم تقویت شده است و این دقیقاً همان چیزی است که در اینجا نیاز داریم.
این یعنی درست زمانی که برنامه شما با خطای OutOfMemory مواجه میشود یا از محدودیتهای حافظه سیستم فراتر میرود، سیستمعامل یک تصویر کلی پشته در پسزمینه ثبت میکند و سپس در Crashlytics، مجموعه فعالشده بهطور رایگان و آماده استفاده ارائه میشود، بهشرطی که از Crashlytics SDK نسخه ۲۰.۱.۰ یا بالاتر استفاده کنید.
سپس Crashlytics بهطور خودکار موارد توقف بهدلیل کمبود حافظه و «محدودکننده حافظه» را گروهبندی میکند، بعداً در داشبورد بیشتر درباره آنها صحبت خواهیم کرد.
و وقتی ازطریق کنسول Firebase یا CLI با جمعآوری رونوشت پشته موافقت میکنید، Crashlytics بهطور خودکار شروع به جمعآوری آن تصویرهای کلی پشته ضبطشده میکند، آنها را برای شما ابهامزدایی میکند، و اساساً، شما یک تصویر کامل اشعه ایکس از هر چیزی که در پشته Java برنامهتان در لحظه خرابی وجود دارد دریافت خواهید کرد.
بنابراین، در اینجا نحوه نمایش ChromaClip OOM در کنسول Crashlytics پساز ضبط توده حافظه آورده شده است.
در بالا، توضیحی درباره اینکه چرا Java حافظه پشته را تمام کرده است و چرا این اتفاق افتاده است میبینید.
سپس، نکات عیبیابی مفید و اشارهگرهایی به منابع خارجی نیز خواهید دید.
در برگه ردیابی پشته، درست بالای آن فعالیت تنظیمات گمراهکننده، Crashlytics درواقع پرچمگذاری میکند که ردیابیهای پشته OOM قطعی نیستند، بنابراین افراد مجبور نیستند وقت خود را برای نگاه کردن به ردیابی پشته تلف کنند، همانطور که ما انجام دادیم.
سپس Crashlytics فایل Perfetto ابهامزداییشده، یعنی رونوشت پشته، را برای جلسه پیوست میکند.
و از اینجا میتوانیم فایل را بارگیری کنیم.
میتوانیم همه دادههای تخلیهشده را در Google Cloud Storage مرور کنیم یا آن را در واسط کاربر Perfetto باز کنیم.
بنابراین، کلیک کردن روی «تجزیهوتحلیل» در واسط کاربر Perfetto، کاوشگر رونوشت پشته Perfetto را باز میکند.
حالا، اگر قبلاً این را دیدهاید، دستتان را بالا ببرید.
بسیارخوب، چند دست بالا رفته است.
به نظر من، این دیدگاه بسیار ترسناک است، اما در این مورد، مسئله درواقع بسیار ساده است.
به مجموعه نوارهای بزرگ که تقریباً در کل توده Java گسترده شدهاند نگاه کنید.
بنابراین، در بیشتر موارد، آرایهای از اشیاء جفتی است که هرکدام یک رنگ و یک نمونه رشته را دربرمیگیرد.
اوه!
فکر کنم واقعاً میدانم چه اتفاقی دارد میافتد.
کد ما برای هر پیکسل در هر فریم یک رنگ بهعلاوه برخی دادههای پشتیبانی اختصاص میدهد و میلیونها مورد از آنها را در یک فهرست نگه میدارد.
So, no wonder Settings activity couldn't allocate those 256 bytes.
خب، حداقل یک راز حل شد.
بیایید آن را اصلاح کنیم.
و در این مرحله، چند گزینه داریم.
بنابراین، ازآنجاییکه Perfetto قبلاً ما را به PaletteExtractor ارجاع داده است، میتوانیم IDE خود را باز کنیم و حلقه را بهصورت دستی بازنویسی کنیم، یا از عامل کدنویسی هوش مصنوعی استفاده کنیم.
یا میتوانیم بازرسی دستی پشته را بهطور کامل دور بزنیم و از Android CLI استفاده کنیم.
# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id>بنابراین، با نصب Android CLI و Perfetto skills میتوانید رونوشتهای پشته Crashlytics ابهامزداییشده را فهرست و بارگیری کنید، اشیای حفظشده برتر را از این رونوشتهای پشته ازطریق trace_processor پُرسمان کنید، و دادههای مشکل Crashlytics منطبق را مانند کلیدهای سفارشی، گزارشها، ردپاها، عرضه مرتبط، و غیره، همه را از پایانه خود واکشی کنید.
بهتر است به پایین اسلایدها نگاه کنید.
این فرمانها را خودتان اجرا نکنید.
دوباره، از عامل کدنویسی هوش مصنوعی موردعلاقهتان به زبان طبیعی بخواهید که جدیدترین رونوشت پشته کمبود حافظه را بااستفاده از فرمان Android بررسی کند و اصلاحیه را ارسال کند.
# Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)و بافت کافی برای انجام کل جریان برای شما خواهد داشت.
بنابراین، فهرست کردن کمبودهای حافظه، بارگیری ردگیری، اجرای تجزیهوتحلیل، و تولید وصله کد مستقیماً در پروژه شما انجام میشود.
و به این وصله در فایل PaletteExtractor نگاه کنید.
// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}بهجای میلیونها تخصیص شیء، اکنون فقط فریم را فرونهنمونه میکنیم و اعداد صحیح خام را مستقیماً میشماریم.
بنابراین، بیایید اصلاح را اعمال کنیم، مشکلات را ببندیم، نسخه خود را ارتقا دهیم و آزمایش را ادامه دهیم.
امیدواریم که نهتنها خطای کمبود حافظه ما برطرف شود، بلکه چون دیگر در جمعآوری زباله دچار مشکل نخواهیم شد، امیدواریم که آن خطای ANR ناخوشایند نیز برطرف شود.
بنابراین، ساخت وصله را در دستگاههای آزمایشیمان مستقر کردیم، کلیپ جدیدی انتخاب کردیم، و شروع به استخراج پالتها کردیم.
و واسط کاربر دوباره از کار میافتد.
یک ANR دیگر.
جالب اینکه با آزمایش بیشتر متوجه شدیم که در برخی دستگاهها هنوز آن خطای ANR را میبینیم، اما در دستگاههای دیگر، برنامه اکنون بهطور تصادفی ازکار میافتد و این اتفاق بسیار مکرر است، حتی مکررتر از آن خطاهای کمبود حافظه.
ولی ما مشکل کمبود حافظه را برطرف کردیم، پس مشکل چیست؟
Polina Govorkova
خب، به Crashlytics برمیگردیم.
اول خبر خوب.
فهرست مشکلات ما را ببینید.
استثنای کمبود حافظه ما کاملاً ازبین رفته است، همانطور که امیدوار بودیم.
خبر بد؟
اکنون که ChromaClip کمی بیشتر دوام میآورد، وقت دارد تا چیزهای دیگری را که در زیر پنهان شدهاند کشف کند.
بنابراین، مشکل ANR ما برطرف نشد.
و حالا دو خرابی بومی در پایین داریم که خوشبختانه فقط چند رویداد دارند.
و درست در بالاترین قسمت، با ۱۱ رویداد، Memory Limiter kill را داریم که خوشبختانه قبلاً نشان رونوشت پشته را دریافت کرده است.
صبر کنید.
«بستن ناشی از محدودکننده حافظه» چیست؟
میتوانم از شما بخواهم دستتان را بالا ببرید، کدامیک از شما درباره آن شنیدهاید؟
بسیار خوب.
چند دست نادر.
بیایید با هم یاد بگیریم.
اگر قبلاً با «کشتن» Memory Limiter مواجه نشدهاید، به این دلیل است که این ویژگی در Android 17 کاملاً جدید است.
و Java OOM فقط زمانی فعال میشود که از حد مجاز پشته Java مدیریتشده فراتر بروید، اما وقتی ردپای حافظه کل برنامه شما شامل RAM بومی نیز باشد، مانند میانگیرهای گرافیکی و تخصیصهای C++، قبلاً هیچ سقفی برای هر برنامه وجود نداشت.
بنابراین، ما «بستن ناشی از کمبود حافظه» را داشتیم که سراسری بود.
این ویژگی زمانی وارد عمل میشد که برنامهای تمام حافظه دسترسی تصادفی (RAM) سیستم را میبلعید و کل تلفن را از کار میانداخت.
اکنون، از Android 17، «محدودکننده حافظه» ردپای حافظه هر برنامه را محدود میکند.
بنابراین، وقتی فرایند مقدار نامتناسبی از RAM را مصرف میکند، سیستمعامل آن برنامه خاص را متوقف میکند.
و Crashlytics همه آن جزئیات را ضبط میکند و توقفها را در داشبورد گروهبندی میکند.
بنابراین، با کلیک کردن روی توقف «محدودکننده حافظه» در Crashlytics، درست مثل OOM، توضیحی درباره دلیل مداخله سیستمعامل دریافت میکنید، بنابراین نیازی نیست آنچه را که گفتم حفظ کنید.
همچنین نکاتی درباره نحوه مقابله با آن در آینده و پیوندهایی مستقیم به اسناد اجرای حافظه Android دریافت میکنید.
و درست مانند OOM، ردیابی پشتهای یک قاب بیگناه را مقصر میداند که سیستمعامل آن را قطع کرده است و داشبورد Crashlytics آن را برجسته میکند.
اکنون، توقفهای «محدودکننده حافظه» عمدتاً در کد بومی اتفاق میافتد زیرا محیطهای بومی فاقد آن لایهای هستند که ایمنی و تخصیص حافظه را مدیریت میکند.
درغیراینصورت، با کمبود حافظه مواجه میشد.
So, our first question is, are our Kotlin objects holding onto huge native-backed allocations?
یا کد خام C++ است که خودش نشت میکند؟
خوب، بیایید روی «تجزیهوتحلیل» در میانای کاربری Perfetto کلیک کنیم تا متوجه شویم.
به برگه «نمای کلی» در کاوشگر رونوشت پشته Perfetto نگاه کنید.
میبینیم که در بخش اطلاعات عمومی، RSS ناشناس بهعلاوه فضای مبادله نشان میدهد که کل حافظه فرایند به ۴ گیگابایت رسیده است.
بسیارخوب.
بااینحال، در زیر آن، میبینیم که در بخش «بایتهای حفظشده توسط توده»، کل توده مدیریتشده ما، ازجمله حافظه بومی مرتبط با اشیاء Kotlin مانند بیتمپها، تنها ۴۲ مگابایت است.
بسیارخوب.
بنابراین، در ۱۰ ثانیه، همه کدهای Kotlin و Java را رد کردیم.
بنابراین، عامل اصلی باید نشت از لایه C++ بومی باشد.
و ازآنجاییکه ChromaClip یک لایه نازک ++C برای پردازش فریم دارد، لازم نیست خیلی دور برویم.
با اشاره کردن عامل کدنویسی ما یا نمایهساز بومی Android Studio به NativePaletteEngine.cpp processFrame، اشکال دقیقاً همانجا است.
// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}بنابراین، در هر فریم ویدیو، کد C++ ما یک میانگیر موقت اختصاص میدهد، که اگر آن را آزاد نکنید، چندان موقت نیست، چه اشتباه ناشیانهای.
پس وقت آن است که نقطهها را به هم وصل کنیم.
چه اتفاقی افتاده است؟
وقتی نشت Java را برطرف کردیم، نشت C++ زمان خواهد داشت تا انباشته شود و از کنترل خارج شود.
در Android 17، «محدودکننده حافظه» آن را شناسایی کرد و برنامه را متوقف کرد.
اما در دستگاههای قدیمیتر که «محدودکننده حافظه» هنوز وجود نداشت، این مشکل بهصورت خطای ANR ظاهر میشد.
پس بالاخره همه چیز منطقی بهنظر میرسد.
بیایید delete[] scratch را اینجا اضافه کنیم، نسخه را ارتقا دهیم، و بهروزرسانی را منتشر کنیم.
With Memory Limiter kills and ANRs finally gone, we should be good to ship to 1.0.
هیجانانگیز.
و بهلطف سرعت و پایداری ChromaClip، برنامه ما بهسرعت رشد میکند.
هر روز هزاران نفر ویدیوها را بارگذاری و ریمیکس میکنند.
اما با این حجم از ترافیک، آن خرابیهای بومی مرموز را که قبلاً در پایین داشبورد دیدیم بهخاطر دارید؟
خب، آنها هم آزادانه میدوند.
ناگهان تعداد آنها خیلی بیشتر میشود.
و کار ما هنوز تمام نشده است.
Polina Govorkova
بنابراین، با باز کردن آن خرابی بومی در Crashlytics، به ردیابی پشته نگاه کنید.
حتی یک خط کد از برنامه خودمان وجود ندارد.
RenderThread هنگام آزاد کردن حافظه در عمق libc.so ازکار میافتد.
و نکته دیگری که در اینجا برای شما داریم این است که وقتی رشته سیستم در داخل تخصیصدهنده حافظه ازکار میافتد و هیچکدام از کدهای برنامه شما در آن وجود ندارد، این نشانه کلاسیک خرابی حافظه بومی است.
بنابراین، کد C++ ما بافر را زودتر خراب یا آزاد کرده است و رشته پردازش بعداً با آن برخورد کرده است.
پس چطور میتوانیم مقصر واقعی را دقیقاً در لحظه وقوع خرابی پیدا کنیم؟
Android دو ابزار قدرتمند به ما میدهد و Crashlytics از هر دو آنها بهطور پیشفرض پشتیبانی میکند.
اول، GWP-ASan که یک شبکه ایمنی سبک است که بهطور خاص برای تولید ساخته شده است.
این ابزار بخش کوچکی از تخصیصهای پشته بومی شما را نمونهبرداری میکند و آنها را با صفحات محافظ احاطه میکند تا سربار CPU و حافظه تقریباً صفر باشد.
در هزاران جلسه کاربر در محیط واقعی، وقتی اشکالی به حافظه آزادشده یا خارج از محدوده در تخصیص نمونهبرداریشده برخورد میکند، GWP-ASan بلافاصله آن را شناسایی میکند.
بااینحال، چون GWP-ASan ذاتاً احتمالی است، وقتی چیزها را بهصورت محلی آزمایش میکنید، نمیخواهید تلفن آزمایشیتان را ۱۶ ساعت یا بیشتر اجرا کنید تا به نمونه تصادفی برسید.
پس چه چیزی دریافت میکنید؟
برای اشکالزدایی محلی قطعی، ARM MTE یا Memory Tagging Extension را دارید.
MTE از برچسبهای حافظه و اشارهگر سختافزاری برای بررسی هر تخصیص استفاده میکند و در دستگاههای ARM v9 مانند Pixel 8 و جدیدتر دردسترس است.
بنابراین، آن را با GWP-ASan در مرحله تولید جفت کنید تا بهترین نتیجه را از هر دو بگیرید.
و همانطور که انتظار میرود، وقتی هرکدام از این ابزارها اشکال را پیدا میکند، Crashlytics بهطور خودکار به مشکل نشان میدهد و به شما امکان میدهد براساس GWP-ASan یا MTE فیلتر کنید و کارت اطلاعات را با علت دقیق خرابی در داشبورد سنجاق میکند.
بنابراین، ببینید وقتی GWP-ASan خرابی را شناسایی میکند، چه اتفاقی در کنسول Crashlytics میافتد.
در کنار برگه ردیابی پشته معمولی، ردیابی پشته حافظه را داریم.
و در بالا، علت دقیق خرابی را میبینیم: «استفاده پساز آزاد شدن».
و بهجای نشان دادن ازکارافتادن رشته پرداز در libc.so، اکنون GWP-ASan شرح کامل آن میانگیر را در effect_pipeline.cc خودمان به ما میدهد.
بنابراین، در بخش «اختصاصیافته توسط»، میبینیم که میانگیر کجا ایجاد شده است.
و درست در زیر Deallocated By، مقصر اصلی را میبینیم که آن را از زیر رشته پردازشی آزاد کرده است.
و این در خط ۲۰۸ در خط لوله جلوه ما پنهان شده است.
بنابراین، کد بومیمان را در EffectPipeline باز میکنیم و اشکال به ما پوزخند میزند.
// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}هرگاه کاربر فیلتر رنگ را در رشته پسزمینه تغییر میدهد، میانگیر قدیمی را درحالیکه RenderThread هنوز درحال خواندن آن است آزاد میکنیم.
بسیار خوب، پس قفل mutex را به اطراف تعویض میانگیر اضافه میکنیم، بهروزرسانی را ارسال میکنیم، و درنهایت میبینیم که هم گزارشهای GWP-ASan و هم همه آن خرابیهای مرموز تقریباً یکشبه ناپدید میشوند.
و سنجه کاربران بدون تجربه خرابی ما به ۱۰۰٪ رسید.
یووو هووو!
یادتان هست، در ابتدای صحبت از خودمان پرسیدیم: «آیا کارم تمام شده است؟»
درست قبلاز اینکه با همه مشکلات مربوط به پاسخگویی و حافظه مواجه شویم.
خب، خطاهای ANR، خطاهای «کمبود حافظه»، توقفهای ناشی از محدودکننده حافظه، و خرابیهای ناشی از خرابی حافظه بومی اکنون همگی حل شدهاند.
و حتی اگر ردیابی پشته دروغ بگوید و مقصر جایی در میان رشتهها در C++ پنهان شده باشد، Android و Firebase Crashlytics ابزاری را برای ردیابی و ارسال با اطمینان به شما ارائه میدهند.
Polina Govorkova
بنابراین، بیایید آنچه را که میتوانید بلافاصله پساز بازگشت به رایانه کیفی خود استفاده کنید مرور کنیم: اطلاعات آماری رشته ANR و سایر فرادادههای مرتبط ازطریق ApplicationExitInfo؛ جمعآوری خودکار راهاندازهای توقف محدودکننده حافظه و OOM ازطریق ProfilingManager؛ و پشتیبانی کامل GWP-ASan و Arm MTE برای خرابی حافظه بومی.
و رونوشتهای پشته فیلد در Android 17 یا بالاتر در پیشنمایش خصوصی راهاندازی میشود.
اگر میخواهید جزو اولین کسانی باشید که جمعآوری خودکار رونوشت پشته را در Firebase Crashlytics امتحان میکنند و آنها را با Perfetto و Android CLI تجزیهوتحلیل میکنند، لطفاً عکس سریعی از اسلاید بگیرید و در پیوند ثبتنام کنید.
خب، حالا که بیشتر صحبتها را انجام دادهایم، میخواهم از شما بابت همراهی امروزتان و وقتی که گذاشتید تشکر کنم.
امیدواریم رشته اصلیتان مسدود نشود، تودهتان تمیز بماند، و نسخه بعدیتان پایدارترین نسخه باشد.
پس، اشکالزدایی خوبی داشته باشید.
باز هم بسیار سپاسگزاریم.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id># Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}