Firebase Crashlytics ile uygulamanızın bellekte kapladığı yeri azaltın ve segmentasyon hatası içermemesini sağlayın. Bu oturumda, Firebase Konsolu ve Android Studio'da bulunan kapsamlı bellek sorun giderme araçlarımızı inceleyeceğiz. Öncelikle, tespit edilmesi zor bellek hatalarını teşhis etmek için ANR analizlerinden ve GWP-ASan raporlarından nasıl yararlanacağınızı ele alacağız. Ardından, Android 17'de kullanıma sunulan yeni bellek sınırlarına uyum sağlamanıza yardımcı olmak için özel olarak tasarlanmış, yetersiz bellek (OOM) ve bellek sınırlayıcı sonlandırma (MLK) etkinliklerine yönelik en yeni araçlarımızı inceleyeceğiz.
ApplicationExitInfo sorgular.ProfilingManager özelliğiyle entegre olarak, Perfetto'da veya Android KSA aracılığıyla incelenebilecek, kod karartması kaldırılmış yığın anlık görüntülerini toplar.Transkript
Polina Govorkova
Herkese merhaba.
Firebase Crashlytics ile bellek sorunlarını giderme eğitimine hoş geldiniz.
Bir dakika, ne kurumsal bir unvan ama.
Biraz değişiklik yapalım.
Firebase Crashlytics ile uygulamaları yalın ve etkili tutma
Bu daha iyi bir fikir.
Adım Polina. Firebase Crashlytics ekibinde kıdemli yazılım geliştiriciyim ve bugünkü sunucunuzum.
Geliştirme döngünüzün bir noktasında tüm hataları çözdüğünüzü düşünebilirsiniz.
JSON ayrıştırma hataları, unutulan boş işaretçi kontrolleri artık yok.
"İşim bitti mi?" diye sorabilirsiniz.
Size kötü bir haberim var: Değilsiniz.
Çünkü geriye kalanlar, aslında zor problemlerdir.
Karşılaşacağınız en zor sorunların iki kategorisinin yanıt verme hızı ve bellek olacağını iddia ediyorum.
İyi haber şu ki size destek olmaya hazırız.
Bugün, Uygulama Yanıt Vermiyor hataları, yetersiz bellek istisnaları, bellek sızıntıları ve yerel bellek bozulması gibi sorunlara yol açan yanıt verme sorunlarının temel nedenini bulmanıza yardımcı olmak için Android ve Crashlytics'teki mevcut ve yepyeni araçları inceleyeceğiz.
Haydi başlayalım.
Tepki verme ve bellek sorunları neden bu kadar kötü bir üne sahip?
Klasik bir hatayı ele alalım: Bir dize tanımlayıp null olarak ayarladıktan sonra uzunluğunu okumaya çalışıyorsunuz.
What do you get?
Klasik bir NullPointerException.
Ancak burada işiniz aslında kolaydır. Çünkü istisna yığın izleme (stack trace), uygulamanızı bozan kod satırını tam olarak gösterir.
Soldaki yığın izlemeye bakın.
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.mainSorunun MainActivity.kt dosyasının 42. satırında olduğunu hemen anlayabiliriz.
Bu yaklaşım, önemsiz hataların çoğu için geçerlidir.
Şimdi, sağdaki Uygulama Yanıt Vermiyor hatası gibi bir yanıt verme veya bellek sorunu yaşandığında yığın izleme (stack trace) neredeyse hiçbir zaman asıl suçluyu göstermez.
"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.mainEn iyi ihtimalle, yalnızca etkiyi gösterir. Bu etki, uygulama donduğunda veya yetersiz bellek olduğunda çalışan masum bir iş parçacığı ya da ayırmadır.
Oysa asıl kök neden çok daha önce veya tamamen başka bir yerde ortaya çıkmış olabilir.
Neyse ki bu sorunlar söz konusu olduğunda karanlıkta tahmin yürütmeniz gerekmez. Firebase Crashlytics'te bu sorunun cevabı var.
Ancak sorun giderme işlemine başlamadan önce Crashlytics'i bilenler elini kaldırabilir mi?
Harika.
Bu kadar çok el görmek çok güzel.
Firebase Crashlytics, yeni başlayanlar için saha verilerinin yanı sıra bağlamsal ve yapay zeka destekli analizler, sorun giderme ipuçları ve yeni sürümleriniz hakkında net sinyaller sunarak sorunları daha hızlı çözmenize yardımcı olan anlık hata raporlama çözümüdür.
Crashlytics, geliştiricilerimiz tarafından çok seviliyor ve mobil cihazlarda en çok kullanılan kilitlenme raporu çözümü olarak öne çıkıyor. En popüler Android uygulamalarının %57'si de dahil olmak üzere 370.000'den fazla yerel uygulamayı destekliyor.
Crashlytics'in hataları tespit etmemize nasıl yardımcı olduğunu görmek için somut bir örneğe bakalım.
Bugün size ChromaClip'in hikayesini anlatmak istiyorum.
Bu, geliştirmekte olduğum bir video renk paleti çıkarıcı. Geliştirme sürecinde karşılaştığım yanıt verme hızı ve bellek hatalarını nasıl çözdüğümü anlatmak istiyorum.
ChromaClip'in arkasındaki fikir basit: Kullanıcılar bir video seçiyor (ör. muhteşem bir gün batımı klibi veya harika renk tonlamasına sahip bir sahne). Uygulama, kareleri analiz ederek özel bir renk paleti oluşturuyor. Kullanıcılar bu paleti tasarımlarında, dijital sanatlarında, moodboard'larında veya diledikleri her yerde kullanabiliyor.
Hikayemiz, 1.0 sürümünü yayınlamadan hemen önce başlıyor.
Lansmandan önce uygulamanın sorunsuz çalıştığından emin olmak istedim. Bu nedenle, birkaç cihazda yerel olarak son testleri yapıyordum. Videoyu seçip renk paletini çıkarmak için düğmeye dokunduğumda, her Android geliştiricisinin en sevdiği diyalogla karşılaştım.
ChromaClip yanıt vermiyor.
Bu, Uygulama Yanıt Vermiyor hatasıdır (ANR olarak da bilinir).
Polina Govorkova
Peki, bu durumda panik yapmak dışında ne yapabiliriz?
İlk olarak, test telefonu doğrudan dizüstü bilgisayara bağlı olduğu için Android Studio'yu açıp Logcat'i kontrol ediyoruz.
Ana iş parçacığının yığın izlemesini görüyoruz.
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)Bu bize aslında ne anlatıyor?
İlk olarak, en üstte, MainActivity'de çalışan bir ANR olduğunu doğruluyoruz.
Başka bir konu var mı?
Bunun nedeni, giriş dağıtımının zaman aşımına uğramasıdır.
Bu, kullanıcının ekrana dokunduğu anlamına gelir.
Evet, yaptık.
Ancak ana iş parçacığı o kadar meşguldü ki bu etkinliği Android'in beş saniyelik gözlemci zaman aşımı süresi içinde işleyemedi.
Ardından, sistemin dosyayı traces.txt dizinine attığını ve hemen altında logcat'in ana iş parçacığımız için yığın izlemeyi yazdırdığını görürüz.
Burada durumun S'ye eşit olduğuna dikkat edin.
Ana iş parçacığımız CPU'yu aktif olarak kullanmıyor bile.
S, uyku anlamına gelir.
Biz beklerken ana iş parçacığımız neden uyuyor?
Yığın çerçevelerini daha yakından inceleyelim.
İzin tam ortasında, ana iş parçacığımızın Unsafe.park içinde park edildiğini ve bilinmeyen bir nesneyi beklediğini görüyoruz.
Neden neyi beklediğini bilmiyoruz?
Bir iş parçacığının klasik Java senkronize kilidi tarafından engellenmesi gibi bazı ANR'lerde, ana iş parçacığı yığın izleme (stack trace) bize aslında neyi beklediğini ve kilidi neyin tuttuğunu gösterir.
Ancak arka planda daha yüksek düzeyde eşzamanlılık temel öğeleri kullandığınızda logcat yalnızca bilinmeyen bir nesneyi beklediğimizi söyler.
Bu nedenle, hangi nesneyi beklediğimizi, özellikle hangi arka plan çalışan iş parçacığını beklediğimizi veya neden 5 saniyeden uzun sürdüğünü bilmiyoruz.
Yerel olarak test ettiğimiz için cihazın traces.txt dosyasını alıp düzinelerce uyku iş parçacığı arasında manuel olarak gezinebilir ve suçluyu bulabilir ya da yapay zekaya sorarak jetonları harcayabiliriz.
Bazı durumlarda bu yöntem işe yarayabilir. Bu örnekte de işe yarayacaktır.
Ancak ham iş parçacığı dökümlerini manuel olarak okumak istemiyorum. Bu yöntem artık çok eskide kaldı. Ayrıca, bu yaklaşımın üretim aşamasında kullanılamayacağını da biliyorum.
Kullanıcıların telefonlarındaki traces.txt dosyasına doğrudan erişimimiz yok, değil mi?
Bu nedenle, Firebase Crashlytics ile entegrasyonumuzun olması iyi bir şey.
Çünkü arama ihtiyacını ortadan kaldırır ve üretimde bile ANR'nin kaynağına iner.
Nasıl mı?
Android'in ApplicationExitInfo API'si bu noktada devreye girer.
Android 11'den itibaren, sistem bir ANR'yi işaretlediğinde veya uygulama işleminizi sonlandırdığında işletim sistemi; kilitlenmenin tam nedeni, bellek durumu ve o anda işleminizde çalışan her iş parçacığı için eksiksiz bir sistem izi de dahil olmak üzere zengin çıkış meta verilerini kaydeder.
En iyi tarafı ise ek bir eklenti yüklemeniz veya tek bir satır kod yazmanız gerekmez.
Crashlytics SDK 18.3.5 veya sonraki bir sürümü kullanıyorsanız bu özellik kullanıma hazırdır.
Bu nedenle, uygulamanız yeniden başlatılır başlatılmaz Crashlytics SDK'sı ApplicationExitInfo'yu otomatik olarak sorgular, cihaz telemetrisiyle birlikte tam çok iş parçacıklı dökümü yakalar ve Crashlytics arka ucuna gönderir.
Crashlytics konsolunu açıp bize ne söylediğine bakalım.
Bu, ANR için Firebase Crashlytics konsolu sorun ayrıntıları görünümüdür. Yığın izleme sekmesinin hemen altında DefaultDispatcher çalışanını görüyoruz.
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)Bu, aslında Crashlytics tarafından en üstte gösterilen ve ana iş parçacığımız için darboğaz oluşturan iş parçacığıdır. Crashlytics, bu iş parçacığını "Kök engelleme" adlı bir rozetle işaretler.
Rozetteki ipucunda açıklandığı gibi, bu çalışan iş parçacığı, ANR'yi tetikleyen iş parçacığını engelleyen iş parçacığıdır.
Bu arada Crashlytics'te diğer sorunlar için de akıllı rozetler bulunur.
Örneğin, iş parçacığı devasa bir dosyayı okurken veya çok yavaş bir veritabanı sorgusu gerçekleştirirken takılırsa ve ANR tetiklenirse iş parçacığı, G/Ç kök engelleme olarak etiketlenir.
İki iş parçacığı birbirini beklerken takılı kalırsa Crashlytics bunları kilitlenmiş olarak etiketler.
Şimdi, kök engelleme iş parçacığındaki yığın çerçevelerine bakalım.
Burada, PaletteExtractor processVideo, FrameDecoder'ı getUncompressedFrame çağırıyor. Bu, mavi renkle vurgulanmış. Bu işlem, tek bir kare için bellek ayırmaya çalışırken createBitmap içinde tamamen takılıp kalmış.
Başka bir kilit için beklemediğini ve ağ çağrısında olmadığını bildiğimiz için asıl şüpheli bellek olmalıdır. Bunu, Crashlytics konsolunda cihazın boş bellek miktarını kontrol ederek hemen doğrulayabiliriz.
Crashlytics'in bu bilgiyi gösterdiği iki yere bakın.
Bu bilgiyi, az önce incelediğimiz yığın izleme sekmesinin hemen yanındaki belirli bir etkinliğin veri sekmesinde ve tüm etkinliklerin ortalamasını gösteren sağ üst kısımda bulabilirsiniz.
Her iki durumda da neredeyse hiçbir şey yok, 100 KB bile değil.
Şimdi düşünelim.
Burada olan şey, işlem için yeterli bellek olmadığından Android çalışma zamanının tüm zamanını çılgınca çöp toplama işlemi yaparak, bir sonraki bitmap için yeterli alan bulmaya çalışarak ve asla bulamayarak geçirmesidir.
Çalışan iş parçacığımızı donduruyor ve kullanıcı arayüzünü beş saniyeden uzun süre boyunca aç bırakıyor.
Bu nedenle, size kısa bir ipucu vereceğiz.
Bu tür düşük boş RAM ile eşleşen bir ANR aldığınızda, ANR'nin aslında bir bellek sızıntısı veya şişme sorunu olduğunu anlarsınız.
Şimdi asıl soruya gelelim: Uygulamamız mağdur mu yoksa belleği gereksiz yere kullanan uygulama mı?
Şimdi projemizin sorun listesine geri dönelim ve alakalı başka bir şey var mı bakalım.
Polina Govorkova
Şuraya bakın.
Altta sorunların listesini görüyoruz. Sekiz etkinliğin yer aldığı ANR'mizin hemen üzerinde OutOfMemory istisnası var.
Yani, suçlu biz olabiliriz.
Bu aslında çok önemli bir ipucu.
Çünkü Android'de Java OutOfMemoryError (kısaca OOM) yalnızca telefonun RAM'inin az olduğu anlamına gelmez.
Bu, kendi uygulama sürecimizin Android çalışma zamanı Java yığın sınırını aştığı anlamına gelir.
Bu noktada, ANR ve OOM sorunlarımızın aslında aynı madalyonun iki yüzü olduğunu düşünüyorum.
Test çalıştırması sırasındaki zamanlamaya bağlı olarak ChromaClip, çöp toplama işlemi sırasında ANR'yi tetikleyecek kadar uzun süre takılı kalıyor veya yığın belleği tamamen tüketerek OOM'ye neden oluyordu.
Kesin olarak öğrenmek için OutOfMemory istisnasını inceleyelim.
Yığın izleme (stack trace) ile başlayalım.
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)Bir saniye bekleyin.
Bitmap.createBitmap, bu tanıdık bir ses.
Bunu daha önce görmemiş miydik?
Ancak bu kez nereden çağrıldığına bakın.
Ayarlar etkinliği açılırken 256 bayt tahsis edilemedi.
Yine aynı tuzağa düşüyoruz.
Uygulamanız küçük bir ayarlar simgesini şişiremediği için kilitlenirse ayarlar etkinliği suçlu olmayacaktır.
Bu, bellek kalmadığında bellek isteyen şanssız çağırıcıdır.
Bu nedenle, asıl suçluyu bulmak için uzaklaşıp kilitlenme sırasında bellekte bulunan her şeyi görmemiz gerekir.
Başka bir deyişle, Java yığın dökümüne ihtiyacımız var.
Çalışma masanızda, OOM'yi nasıl tetikleyeceğinizi biliyorsanız Android Studio Memory Profiler ile manuel olarak Java yığın dökümü alabilirsiniz.
Ancak test çalıştırmalarınızda veya gerçek kullanımda beklenmedik bir şekilde OOM hatası oluştuğunda bu yöntem işe yaramaz.
Polina Govorkova
Firebase Crashlytics'in bir diğer önemli özelliği de budur.
Crashlytics artık uygulama çıkış bilgilerinin yanı sıra Android'in ProfilingManager API'si ile de entegre oluyor.
Android 17'deki ProfilingManager API, sistem tarafından tetiklenen bellek profili oluşturma özelliğiyle güçlendirildi. Tam olarak ihtiyacımız olan şey de bu.
Yani uygulamanız OutOfMemory hatası verdiğinde veya sistem bellek sınırlarını ihlal ettiğinde OS arka planda bir yığın anlık görüntüsü yakalar. Crashlytics SDK 20.1.0 veya sonraki bir sürümü kullanıyorsanız tetiklenen koleksiyon, Crashlytics'te kullanıma hazır olarak sunulur.
Ardından Crashlytics, OOM ve Memory Limiter sonlandırmalarınızı otomatik olarak gruplandırır. Bunlardan daha sonra kontrol panelinde bahsedeceğiz.
Firebase konsolu veya CLI üzerinden yığın dökümü toplamayı etkinleştirdiğinizde Crashlytics, yakalanan yığın anlık görüntülerini otomatik olarak toplamaya başlar ve bunları sizin için anlaşılır hale getirir. Temel olarak, kilitlenme anında uygulamanızın Java yığınında bulunan her şeyin tam bir röntgenini elde edersiniz.
Yığın yakalandıktan sonra ChromaClip OOM'miz Crashlytics konsolunda bu şekilde görünür.
Üst kısımda, Java'nın yığın belleği neden tükettiğine ve bunun neden olduğuna dair bir açıklama görürsünüz.
Ardından, sorun gidermeyle ilgili faydalı ipuçları ve harici kaynaklara yönlendiren bağlantılar göreceksiniz.
Yığın izleme sekmesinin alt kısmında, yanıltıcı ayarlar etkinliğinin hemen üzerinde Crashlytics, OOM yığın izlemelerinin kesin olmadığını işaretler. Böylece kullanıcılar, az önce yaptığımız gibi yığın izlemeye bakarak zaman kaybetmek zorunda kalmaz.
Ardından Crashlytics, oturum için kodu çözülmüş Perfetto dosyasını (yığın dökümü) ekler.
Buradan dosyayı indirebiliriz.
Google Cloud Storage'daki tüm dökümlere göz atabilir veya dökümleri Perfetto kullanıcı arayüzünde açabiliriz.
Perfetto kullanıcı arayüzünde Analyze'ı (Analiz et) tıkladığınızda Perfetto yığın dökümü gezgini açılır.
Şimdi, bunu daha önce gördüyseniz elinizi kaldırın.
Tamam, birkaç kişi el kaldırdı.
Bu görünüm bana çok korkutucu geliyor ancak bu durumda sorun aslında oldukça basit.
Java yığınının neredeyse tamamını kaplayan devasa çubuklara bakın.
Bu nedenle, çoğunlukla bir renk ve dize örneğini sarmalayan çift nesnelerden oluşan bir dizidir.
Bilmiyordum.
Sanırım neler olduğunu biliyorum.
Kodumuz her karedeki her piksel için bir renk ve bazı destekleyici veriler tahsis ediyor ve bunları milyonlarca öğe içeren bir listede tutuyor.
Bu nedenle, Ayarlar etkinliğinin bu 256 baytı ayıramaması şaşırtıcı değildir.
En azından bir gizem çözüldü.
Şimdi birlikte bu sorunu çözelim.
Bu noktada birkaç seçeneğimiz var.
Perfetto bizi PaletteExtractor'a yönlendirdiği için IDE'mizi açıp döngüyü manuel olarak yeniden yazabilir veya yapay zeka kodlama temsilcisini kullanabiliriz.
Alternatif olarak, manuel yığın incelemesini tamamen atlayıp Android CLI'dan yararlanabilirsiniz.
# 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 KSA ve Perfetto becerilerini yüklediğinizde, kodları çözülmüş Crashlytics yığın dökümlerini listeleyip indirebilir, trace_processor aracılığıyla bu yığın dökümlerindeki en çok tutulan nesneleri sorgulayabilir ve özel anahtarlar, günlükler, içerik haritaları, ilişkili kullanıma sunma işlemleri gibi eşleşen Crashlytics sorun verilerini doğrudan terminalinizden alabilirsiniz.
Daha da iyisi, slaytların alt kısmına bakın.
Bu komutları kendiniz çalıştırmayın.
Yine, favori yapay zeka kodlama temsilcinizden doğal dilde, Android komutunu kullanarak en son OOM yığın dökümünü incelemesini ve düzeltmeyi göndermesini isteyin.
# 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)Ayrıca tüm akışı sizin için tamamlayacak kadar bağlam bilgisine sahip olur.
Yani, bellek yetersizliği hatalarını listeleme, izi indirme, analizi çalıştırma ve kod yamasını doğrudan projenizde oluşturma.
PaletteExtractor dosyasındaki bu yamaya bakın.
// 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
}Milyonlarca nesne tahsisi yerine artık kareyi alt örnekleyip ham tam sayıları doğrudan sayıyoruz.
Şimdi düzeltmeyi uygulayalım, sorunları kapatalım, sürümümüzü yükseltelim ve test etmeye devam edelim.
Umarız yetersiz bellek hatası ortadan kalkar ve artık çöp toplama işlemiyle uğraşmayacağımız için o can sıkıcı ANR de kaybolur.
Bu nedenle, yama derlemesini test cihazlarımıza dağıttık, yeni bir klip seçtik ve paletleri ayıklamaya başladık.
Kullanıcı arayüzü tekrar donuyor.
Başka bir ANR.
İlginç bir şekilde, daha fazla test yaptığımızda bazı cihazlarda ANR'nin devam ettiğini, diğer cihazlarda ise uygulamanın rastgele ve oldukça sık (bellek yetersizliği sorunlarından daha sık) kilitlendiğini fark ettik.
Ancak yetersiz bellek sorununu düzelttik. Peki sorun ne?
Polina Govorkova
Crashlytics'e geri dönelim.
Önce iyi haberden başlayalım.
Sorun listemize göz atın.
Yetersiz bellek istisnası, umduğumuz gibi tamamen ortadan kalktı.
Kötü haber ne mi?
ChromaClip artık biraz daha uzun süre hayatta kalabildiği için altında gizlenen diğer şeyleri keşfetmeye vakti var.
Bu nedenle ANR sorunumuz çözülmedi.
Şimdi de en altta iki yerel kilitlenme var. Neyse ki sadece birkaç etkinlikte gerçekleşmiş.
En üstte ise 11 etkinlikle Memory Limiter kill var. Neyse ki bu etkinlik, yığın dökümü içerdiğini belirten bir rozete sahip.
Ama...
Bellek Sınırlayıcı tarafından sonlandırma nedir?
Aranızda bunu duyan var mı?
İşte böyle.
Nadir görülen birkaç el.
Birlikte öğrenelim.
Bellek Sınırlayıcı'nın daha önce bir işlemi sonlandırdığını görmediyseniz bunun nedeni Android 17'de yeni kullanıma sunulmuş olmasıdır.
Java OOM ise yalnızca yönetilen Java yığın sınırını aştığınızda tetiklenir. Ancak uygulamanızın toplam bellekte kapladığı yere grafik arabellekleri ve C++ ayırmaları gibi yerel RAM de dahil olduğunda, daha önce uygulama başına bir üst sınır yoktu.
Bu nedenle, global olan düşük bellek sonlandırıcımız vardı.
Bir uygulama, sistem RAM'ini tüm telefonun kilitlenmesine neden olacak kadar tükettiğinde devreye girerdi.
Android 17'den itibaren Bellek Sınırlayıcı, her uygulamanın bellekte kapladığı yeri ayrı ayrı sınırlandırır.
Bu nedenle, işlem aşırı miktarda RAM tükettiğinde işletim sistemi ilgili uygulamayı sonlandırır.
Crashlytics tüm bu ayrıntıları yakalar ve sonlandırmaları kontrol panelinde gruplandırır.
Crashlytics'te Memory Limiter sonlandırmasını tıkladığınızda, tıpkı OOM'de olduğu gibi işletim sisteminin neden müdahale ettiğine dair bir açıklama görürsünüz. Bu nedenle, az önce söylediklerimi ezberlemeniz gerekmez.
Ayrıca, bu sorunu nasıl çözeceğinizle ilgili ipuçları ve doğrudan Android bellek yaptırımı belgelerine yönlendiren bağlantılar da sunulur.
Yetersiz bellek hatasında olduğu gibi, işletim sistemi işlemi sonlandırdığında yığın izleme, masum bir kareyi suçlar ve Crashlytics kontrol paneli bu kareyi vurgular.
Bellek Sınırlayıcı sonlandırmaları artık yerel ortamlarda bellek güvenliğini ve ayırmalarını yöneten katman bulunmadığından öncelikle yerel kodda gerçekleşir.
Aksi takdirde, OOM olurdu.
İlk sorumuz şu: Kotlin nesnelerimiz, yerel destekli büyük ayırmaları tutuyor mu?
Yoksa kendi kendine sızan ham bir C++ kodu mu?
Öğrenmek için Perfetto kullanıcı arayüzünde Analyze'ı (Analiz et) tıklayalım.
Perfetto yığın dökümü gezginindeki Genel Bakış sekmesine bakın.
Genel bilgiler bölümünde, anonim RSS ve takas alanının toplam işlem belleğinin 4 GB'a ulaştığını gösterdiğini görüyoruz.
Anladım.
Ancak bunun altında, Yığın Tarafından Tutulan Baytlar bölümünde, bitmap'ler gibi Kotlin nesnelerine bağlı yerel bellek de dahil olmak üzere yönetilen yığınımızın tamamının yalnızca 42 MB olduğunu görüyoruz.
Anladım.
Böylece 10 saniye içinde tüm Kotlin ve Java kodlarını elemiş olduk.
Bu nedenle, sızıntının nedeni yerel bir C++ katmanı olmalıdır.
ChromaClip'te kare işleme için ince bir C++ katmanı olduğundan çok uzağa bakmamıza gerek yok.
Kodlama temsilcimizi veya Android Studio'nun yerel profil oluşturucusunu NativePaletteEngine.cpp processFrame adresine yönlendirdiğinizde hatayı hemen görebilirsiniz.
// 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
}Bu nedenle, her video karesinde C++ kodumuz geçici bir çalışma arabelleği ayırır. Bu arabellek, serbest bırakmazsanız geçici olmaz. Bu, acemice bir hatadır.
Şimdi noktaları birleştirme zamanı.
Ne oldu?
Java sızıntısını düzelttiğimizde C++ sızıntısı birikip kontrolden çıkabilir.
Android 17'de Bellek Sınırlayıcı bu durumu tespit etti ve uygulamayı kapattı.
Ancak Bellek Sınırlayıcı'nın henüz bulunmadığı eski cihazlarda bu durum ANR olarak ortaya çıkıyordu.
Sonunda her şey mantıklı bir hal aldı.
Buraya delete[] scratch ekleyelim, sürümümüzü yükseltelim ve güncellemeyi yayınlayalım.
Bellek Sınırlayıcı sonlandırmaları ve ANR'ler nihayet ortadan kalktığına göre 1.0 sürümünü yayınlayabiliriz.
Heyecan verici.
ChromaClip artık çok hızlı ve kararlı olduğu için uygulamamız büyük bir başarı yakaladı.
Her gün binlerce kişi video yüklüyor ve remiks yapıyor.
Ancak tüm bu trafikle birlikte, daha önce kontrol panelinin alt kısmında gördüğümüz o gizemli yerel kilitlenmeleri hatırlıyor musunuz?
Onlar da kontrolden çıkmış durumda.
Birdenbire çok daha fazla sayıda reklam görmeye başlarsınız.
Ancak işimiz henüz bitmedi.
Polina Govorkova
Crashlytics'te bu yerel kilitlenmeyi açıp yığın izlemeye bakın.
Kendi uygulamamızdan tek bir kod satırı bile yok.
RenderThread, bellekte yer açarken libc.so'nun derinliklerinde kilitleniyor.
Bir ipucu daha verelim: Bir sistem iş parçacığı, bellek ayırıcıda uygulamanızın kodu olmadan kilitlenirse bu, yerel bellek bozulmasının tipik bir işaretidir.
Yani C++ kodumuz, arabelleği daha önce bozmuş veya serbest bırakmış ve oluşturma iş parçacığı daha sonra bu arabelleğe takılmış.
Peki, gerçek suçluyu tam olarak bozulmanın gerçekleştiği anda nasıl yakalayabiliriz?
Android bize iki güçlü araç sunuyor ve Crashlytics her ikisini de kullanıma hazır şekilde destekliyor.
İlk olarak, özellikle üretim için tasarlanmış hafif bir güvenlik ağı olan GWP-ASan'ı ele alalım.
Yerel yığın ayırmalarınızın küçük bir kısmını örnekler ve bunları koruma sayfalarıyla çevreler. Böylece CPU ve bellek ek yükü neredeyse sıfır olur.
GWP-ASan, binlerce kullanıcı oturumunda serbest bırakılan veya sınır dışı belleğe dokunan bir hata olduğunda bunu hemen yakalar.
Ancak GWP-ASan doğası gereği olasılıksal olduğundan, yerel olarak test yaparken rastgele örneğe ulaşmak için test telefonunuzu 16 saat veya daha uzun süre çalıştırmak istemezsiniz.
Peki, bu durumda ne elde edersiniz?
Deterministik yerel hata ayıklama için ARM MTE veya Bellek Etiketleme Uzantısı'nı kullanabilirsiniz.
MTE, her tahsisi kontrol etmek için donanım işaretçisi ve bellek etiketleri kullanır. Pixel 8 ve daha yeni modeller gibi ARM v9 cihazlarda kullanılabilir.
Bu nedenle, üretimde GWP-ASan ile birlikte kullanarak her iki aracın da avantajlarından yararlanabilirsiniz.
Beklediğiniz gibi, her iki araç da hatayı yakaladığında Crashlytics, sorunu otomatik olarak etiketler ve GWP-ASan veya MTE'ye göre filtrelemenize olanak tanır. Ayrıca, tam bozulma nedenini içeren bilgi kartını kontrol paneline sabitler.
GWP-ASan kilitlenmeyi yakaladığında Crashlytics konsolunda ne olduğuna bakalım.
Normal yığın izleme sekmesinin yanında bellek yığın izleme sekmesi bulunur.
En üstte ise bozulmanın tam nedenini, yani "Use After Free"yi görüyoruz.
GWP-ASan, libc.so'da çöken oluşturma iş parçacığını göstermek yerine artık kendi effect_pipeline.cc'mızda bu arabelleğin tam geçmişini veriyor.
"Allocated By" (Tahsis Eden) bölümünde, arabelleğin nerede oluşturulduğunu görebiliriz.
"Deallocated By" (Ayırmayı Kaldıran) bölümünün hemen altında, oluşturma iş parçacığından ayırmayı kaldıran asıl suçluyu görüyoruz.
Bu da efekt ardışık düzenimizin 208. satırında gizleniyor.
Yerel kodumuzu EffectPipeline içinde açtığımızda hatayı hemen fark ederiz.
// 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
}Kullanıcı arka plan iş parçacığında renk filtresini her değiştirdiğinde, RenderThread eski arabelleği okumaya devam ederken biz bu arabelleği serbest bırakıyoruz.
Tamam, arabellek takasının etrafına mutex kilidini ekliyoruz, güncellemeyi yayınlıyoruz ve son olarak hem GWP-ASan raporlarının hem de tüm bu gizemli kilitlenmelerin neredeyse bir gecede ortadan kaybolduğunu görüyoruz.
Kilitlenme sorunu yaşamayan kullanıcılar metriğimiz de %100'e fırladı.
Yaşasın!
Konuşmanın başında kendimize "İşim bitti mi?" diye sormuştuk.
Ancak hemen ardından, tepki süresi ve bellek sorunlarıyla ilgili her türlü zorlukla karşılaştık.
ANR'ler, yetersiz bellek hataları, bellek sınırlayıcı sonlandırmaları ve yerel bellek bozulması kilitlenmeleri artık çözüldü.
Yığın izleme (stack trace) yanlış bilgi verse ve asıl sorun C++'taki iş parçacıklarında gizlense bile Android ve Firebase Crashlytics, sorunu bulup güvenle yayınlamanızı sağlayacak araçları sunar.
Polina Govorkova
Dizüstü bilgisayarınıza döndüğünüzde hemen kullanabileceğiniz özellikleri özetleyelim: ApplicationExitInfo aracılığıyla ANR iş parçacığı analizleri ve diğer alakalı meta veriler; ProfilingManager aracılığıyla OOM ve bellek sınırlayıcı sonlandırma tetikleyicilerinin otomatik olarak toplanması; yerel bellek bozulması için tam GWP-ASan ve Arm MTE desteği.
Android 17 veya sonraki sürümlerde özel önizleme olarak kullanıma sunulan saha yığın dökümleri.
Firebase Crashlytics'te otomatik yığın dökümü toplama özelliğini deneyen ve bunları Perfetto ve Android KSA ile analiz eden ilk kişilerden biri olmak istiyorsanız lütfen slaytın fotoğrafını çekip bağlantıdan kaydolun.
Konuşmamızın büyük bir kısmını tamamladık. Bugün bana katıldığınız ve zaman ayırdığınız için teşekkür ederim.
Ana iş parçacığınız engellenmesin, yığın belleğiniz temiz kalsın ve bir sonraki sürümünüz en kararlı sürümünüz olsun.
İyi hata ayıklamalar.
Tekrar teşekkür ederiz.
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
}