Kilitlenmeler

Bir Android uygulaması, işlenmemiş bir istisna veya sinyal nedeniyle beklenmedik bir çıkış olduğunda kilitlenir. Java veya Kotlin kullanılarak yazılan bir uygulama, Throwable sınıfıyla temsil edilen işlenmemiş bir istisna oluşturursa çöker. Makine dili veya C++ kullanılarak yazılmış bir uygulama, yürütülmesi sırasında SIGSEGV gibi işlenmemiş bir sinyal varsa kilitlenir.

Bir uygulama çöktüğünde Android, uygulamanın sürecini sonlandırır ve kullanıcıya uygulamanın durduğunu bildirmek için Şekil 1'de gösterildiği gibi bir iletişim kutusu gösterir.

Android destekli bir cihazda uygulamanın kilitlenmesi
Şekil 1. Android destekli bir cihazda uygulamanın kilitlenmesi.

Bir uygulamanın kilitlenmesi için ön planda çalışması gerekmez. Arka planda çalışan yayın alıcılar veya içerik sağlayıcılar gibi bileşenler de dahil olmak üzere tüm uygulama bileşenleri, uygulamanın kilitlenmesine neden olabilir. Bu kilitlenmeler, kullanıcılar uygulamanızla aktif olarak etkileşimde bulunmadığı için genellikle kafa karıştırıcıdır.

Uygulamanız kilitleniyorsa sorunu teşhis etmek ve düzeltmek için bu sayfadaki yönergelerden yararlanabilirsiniz.

Sorunu tespit etme

Kullanıcılarınızın uygulamanızı kullanırken kilitlenme sorunu yaşadığını her zaman bilemeyebilirsiniz. Uygulamanızı zaten yayınladıysanız uygulamanızın kilitlenme oranlarını görmek için Android vitals'ı kullanabilirsiniz.

Android vitals

Android vitals, uygulamanızın kilitlenme oranını izlemenize ve iyileştirmenize yardımcı olabilir. Android vitals, çeşitli kilitlenme oranlarını ölçer:

  • Kilitlenme oranı: Günlük etkin kullanıcı sayınız arasında herhangi bir türde kilitlenme yaşayanların yüzdesi.
  • Kullanıcı tarafından algılanan kilitlenme oranı: Uygulamanızı etkin olarak kullanırken en az bir kilitlenme (kullanıcı tarafından algılanan kilitlenme) yaşayan günlük etkin kullanıcı sayınızın yüzdesi. Herhangi bir etkinlik görüntüleyen veya ön plan hizmeti yürüten uygulamalar etkin olarak kullanılıyor kabul edilir.

  • Çoklu kilitlenme oranı: Günlük etkin kullanıcı sayınız arasında en az iki kez kilitlenme yaşayanların yüzdesi.

Günlük etkin kullanıcı, uygulamanızı tek bir günde tek bir cihazda (birden fazla oturumda) kullanan benzersiz bir kullanıcıdır. Tek bir günde uygulamanızı birden çok cihazda kullanan bir kullanıcı, o günün etkin kullanıcı sayısına her cihaz için bir kullanıcı olarak eklenir. Birden fazla kullanıcı tek bir gün içinde aynı cihazı kullanırsa bunlar tek bir etkin kullanıcı olarak sayılır.

Kullanıcı tarafından algılanan kilitlenme oranı önemli bir metriktir. Yani Google Play'de uygulamanızın keşfedilebilirliğini etkiler. Bu metriğin saydığı kilitlenmelerin her zaman kullanıcılar uygulamayla etkileşim halindeyken gerçekleşerek çok fazla aksamaya yol açması, bu metriği önemli kılar.

Play, bu metrik için iki kötü davranış eşiği belirlemiştir:

  • Genel olarak kötü davranış eşiği: Tüm cihaz modellerinde, günlük etkin kullanıcı sayısı en az% 1,09'u, kullanıcı tarafından algılanan kilitlenme yaşamıştır.
  • Cihaz bazında kötü davranış eşiği: Günlük etkin kullanıcı sayısının en az% 8'i tek bir cihaz modeliyle ilişkili, kullanıcı tarafından algılanan kilitlenme yaşamıştır.

Uygulamanız genel kötü davranış eşiğini aşarsa tüm cihazlarda bulunabilirliği azalabilir. Uygulamanız bazı cihazlarda cihaz başına kötü davranış eşiğini aşarsa bu cihazlardaki bulunabilirliği azalabilir ve mağaza girişinizde bir uyarı gösterilebilir.

Android vitals, uygulamanız aşırı kilitlenme gösterdiğinde Play Console'da sizi uyarabilir.

Google Play'in Android vitals verilerini nasıl topladığı hakkında bilgi edinmek için Play Console belgelerine bakın.

Kilitlenmeleri teşhis etme

Uygulamanızın kilitlenmeler bildirdiğini belirledikten sonraki adım, bu kilitlenmeleri teşhis etmektir. Kilitlenme sorunlarını çözmek zor olabilir. Ancak kilitlenmenin temel nedenini belirleyebilirseniz büyük olasılıkla soruna çözüm bulabilirsiniz.

Uygulamanızın kilitlenmesine neden olabilecek birçok durum vardır. Bazı nedenler (ör. boş değer veya boş dize kontrolü) açıktır ancak diğerleri (ör. bir API'ye geçersiz bağımsız değişkenler iletme veya karmaşık çok iş parçacıklı etkileşimler) daha az belirgindir.

Android'deki kilitlenmeler, programınızdaki iç içe yerleştirilmiş işlev çağrılarının kilitlenme anına kadarki adım sırasının anlık görüntüsü olan bir yığın izleme (stack trace) oluşturur. Kilitlenme yığın izlemelerini Android vitals'ta görüntüleyebilirsiniz.

Yığın izlemeyi okuma

Kilitlenmeyi düzeltmenin ilk adımı, kilitlenmenin gerçekleştiği yeri belirlemektir. Play Console'u kullanıyorsanız rapor ayrıntılarında bulunan yığın izleme (stack trace) veya Logcat aracının çıkışını kullanabilirsiniz. Yığın iziniz yoksa uygulamayı manuel olarak test ederek veya etkilenen kullanıcılara ulaşarak kilitlenmeyi yerel olarak yeniden oluşturmanız ve logcat'i kullanırken yeniden oluşturmanız gerekir.

Aşağıdaki izleme, Jetpack Compose kullanılarak yazılmış bir uygulamadaki kilitlenme örneğini gösterir:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException
    at com.android.developer.crashsample.ComposableSingletons$MainActivityKt.lambda$0(MainActivity.kt:27)
    at androidx.compose.foundation.ClickableNode.handleUpEvent(Clickable.kt:958)
    at androidx.compose.foundation.ClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:895)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:446)
    at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:181)
    at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:118)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:2650)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2969)
    at android.app.Activity.dispatchTouchEvent(Activity.java:4683)
    at android.os.Looper.loop(Looper.java:398)
    at android.app.ActivityThread.main(ActivityThread.java:9569)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:918)

Yığın izleme, kilitlenmeyle ilgili hataları ayıklamak için kritik öneme sahip iki bilgi gösterir:

  • İstisna türü.
  • İstisnanın oluşturulduğu kod bölümü.

Oluşan istisnanın türü, genellikle neyin yanlış gittiği konusunda çok güçlü bir ipucu verir. IOException, OutOfMemoryError veya başka bir şey olup olmadığına bakın ve istisna sınıfıyla ilgili dokümanları bulun.

İstisnanın oluşturulduğu kaynak dosyanın sınıfı, yöntemi, dosyası ve satır numarası yığın izinin ikinci satırında gösterilir. Çağrılan her işlev için başka bir satırda önceki çağrı sitesi (yığın çerçevesi olarak adlandırılır) gösterilir.

Yığını inceleyip kodu inceleyerek yanlış değer ileten bir yer bulabilirsiniz. Kodunuz yığın izinde görünmüyorsa muhtemelen bir yerde eşzamansız bir işleme geçersiz bir parametre iletmişsinizdir. Genellikle yığın izinin her satırını inceleyerek, kullandığınız API sınıflarını bularak ve ilettiğiniz parametrelerin doğru olduğunu ve izin verilen bir yerden çağırdığınızı onaylayarak ne olduğunu anlayabilirsiniz.

C ve C++ kodu içeren uygulamalarda yığın izleri de benzer şekilde çalışır.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
Timestamp: 2020-02-16 11:16:31+0100
pid: 8288, tid: 8288, name: com.example.testapp  >>> com.example.testapp <<<
uid: 1010332
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
    x0  0000007da81396c0  x1  0000007fc91522d4  x2  0000000000000001  x3  000000000000206e
    x4  0000007da8087000  x5  0000007fc9152310  x6  0000007d209c6c68  x7  0000007da8087000
    x8  0000000000000000  x9  0000007cba01b660  x10 0000000000430000  x11 0000007d80000000
    x12 0000000000000060  x13 0000000023fafc10  x14 0000000000000006  x15 ffffffffffffffff
    x16 0000007cba01b618  x17 0000007da44c88c0  x18 0000007da943c000  x19 0000007da8087000
    x20 0000000000000000  x21 0000007da8087000  x22 0000007fc9152540  x23 0000007d17982d6b
    x24 0000000000000004  x25 0000007da823c020  x26 0000007da80870b0  x27 0000000000000001
    x28 0000007fc91522d0  x29 0000007fc91522a0
    sp  0000007fc9152290  lr  0000007d22d4e354  pc  0000007cba01b640

backtrace:
  #00  pc 0000000000042f89  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::Crasher::crash() const)
  #01  pc 0000000000000640  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::runCrashThread())
  #02  pc 0000000000065a3b  /system/lib/libc.so (__pthread_start(void*))
  #03  pc 000000000001e4fd  /system/lib/libc.so (__start_thread)

Yerel yığın izlemelerinde sınıf ve işlev düzeyinde bilgiler görmüyorsanız yerel hata simgeselleştirme dosyası oluşturmanız ve bu dosyayı Google Play Console'a yüklemeniz gerekebilir. Daha fazla bilgi için Kilitlenme yığını izlerinin kodunu çözme başlıklı makaleyi inceleyin. Yerel kilitlenmeler hakkında genel bilgi için Yerel kilitlenmeleri teşhis etme başlıklı makaleyi inceleyin.

Kilitlenmeyi yeniden üretmeyle ilgili ipuçları

Yalnızca bir emülatörü başlatarak veya cihazınızı bilgisayarınıza bağlayarak sorunu tam olarak yeniden üretemeyebilirsiniz. Geliştirme ortamları genellikle bant genişliği, bellek ve depolama alanı gibi daha fazla kaynağa sahiptir. Hangi kaynağın kıt olabileceğini belirlemek için istisna türünü kullanın veya Android sürümü, cihaz türü ya da uygulamanızın sürümü arasında bir korelasyon bulun.

Bellek hataları

OutOfMemoryError cihazınız varsa test etmek için düşük bellek kapasitesine sahip bir emülatör oluşturabilirsiniz. Şekil 2'de, cihazdaki bellek miktarını kontrol edebileceğiniz AVD yöneticisi ayarları gösterilmektedir.

AVD yöneticisindeki bellek ayarı
Şekil 2. AVD yöneticisindeki bellek ayarı.

Ağ iletişimi istisnaları

Kullanıcılar mobil veya kablosuz ağ kapsamına sık sık girip çıktığından, uygulama ağındaki istisnalar genellikle hata olarak değil, beklenmedik şekilde gerçekleşen normal çalışma koşulları olarak değerlendirilmelidir.

UnknownHostException gibi bir ağ istisnasını yeniden oluşturmanız gerekiyorsa uygulamanız ağı kullanmaya çalışırken uçak modunu açmayı deneyin.

Diğer bir seçenek de ağ hızı emülasyonu, ağ gecikmesi veya her ikisini de seçerek emülatördeki ağ kalitesini düşürmektir. AVD yöneticisindeki Hız ve Gecikme ayarlarını kullanabilir veya aşağıdaki komut satırı örneğinde gösterildiği gibi, -netdelay ve -netspeed işaretleriyle emülatörü başlatabilirsiniz:

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

Bu örnek, tüm ağ isteklerinde 20 saniyelik bir gecikme ve 14,4 Kbps yükleme ve indirme hızı ayarlar. Emülatörün komut satırı seçenekleri hakkında daha fazla bilgi için Emülatörü komut satırından başlatma başlıklı makaleyi inceleyin.

logcat ile okuma

Kilitlenmeyi yeniden oluşturabildiğinizde daha fazla bilgi edinmek için logcat gibi bir araç kullanabilirsiniz.

Logcat çıktısı, sistemden gelen diğer mesajlarla birlikte yazdırdığınız diğer günlük mesajlarını gösterir. Uygulamanız çalışırken yazdırılmaları CPU ve pil gücünü boşa harcadığı için eklediğiniz tüm Log ifadelerini devre dışı bırakmayı unutmayın.

Null işaretçi istisnalarından kaynaklanan çökmeleri önleme

Boş işaretçi istisnaları (çalışma zamanı hata türü NullPointerException ile tanımlanır), genellikle yöntemlerini çağırarak veya üyelerine erişerek boş bir nesneye erişmeye çalıştığınızda oluşur. Google Play'de uygulama çökmelerinin en büyük nedeni boş işaretçi istisnalarıdır. Boş değerin amacı, nesnenin eksik olduğunu belirtmektir. Örneğin, nesne henüz oluşturulmamış veya atanmamıştır.

Boş işaretçi istisnalarını önlemek için, üzerinde çalıştığınız nesne referanslarının, üzerinde yöntem çağırmadan veya üyelerine erişmeye çalışmadan önce boş olmadığından emin olmanız gerekir. Nesne referansı boşsa (null), bu durumu iyi bir şekilde ele alın (örneğin, nesne referansında herhangi bir işlem yapmadan önce bir yöntemden çıkın ve hata ayıklama günlüğüne bilgi yazın).

Çağrılan her yöntemin her parametresi için null denetimleri yapmak istemediğinizden, null değer alabilirliği belirtmek için IDE'ye veya nesnenin türüne güvenebilirsiniz.

Kotlin

Kotlin'de nullability, tür sisteminin bir parçasıdır. Örneğin, bir değişkenin en başından itibaren null değer atanabilir veya null değer atanabilir olmayan olarak tanımlanması gerekir. Null değer atanabilir türler ? ile işaretlenir:

// non-null
var s: String = "Hello"

// null
var s: String? = "Hello"

Boş değer atanamayan değişkenlere boş değer atanamaz ve boş değer atanabilen değişkenlerin null değer alabilme durumu, boş değer atanmayan değişken olarak kullanılmadan önce kontrol edilmelidir.

Boş değerleri açıkça kontrol etmek istemiyorsanız ?. güvenli çağrı operatörünü kullanabilirsiniz:

val length: Int? = string?.length  // length is a nullable int
                                   // if string is null, then length is null

En iyi uygulama olarak, boş değer atanabilir bir nesne için boş durumu ele aldığınızdan emin olun. Aksi takdirde uygulamanız beklenmedik durumlara girebilir. Uygulamanız NullPointerException ile artık kilitlenmiyorsa bu hataların varlığını bilemezsiniz.

Boş değerleri kontrol etmenin bazı yolları şunlardır:

  • if kontrol

    val length = if(string != null) string.length else 0
    

    Akıllı yayın ve boşluk kontrolü nedeniyle Kotlin derleyicisi, dize değerinin boş olmadığını bilir. Bu nedenle, güvenli çağrı operatörüne gerek kalmadan referansı doğrudan kullanmanıza izin verir.

  • ?: Elvis operatörü

    Bu operatör, "nesne boş değilse nesneyi döndür, aksi takdirde başka bir şey döndür" ifadesini belirtmenize olanak tanır.

    val length = string?.length ?: 0
    

Kotlin'de NullPointerException almaya devam edebilirsiniz. En yaygın durumlar şunlardır:

  • Açıkça NullPointerException atıyorsanız.
  • Null assertion !! operatörünü kullanırken. Bu operatör, tüm değerleri null olmayan bir türe dönüştürür. Değer null ise NullPointerException hatası verilir.
  • Platform türünün boş referansına erişirken.

Platform türleri

Platform türleri, Java'dan gelen nesne bildirimleridir. Bu türler özel olarak işlenir. Boş değer kontrolleri Java'daki kadar zorunlu değildir. Bu nedenle, boş olmayan değer garantisi Java'dakiyle aynıdır. Bir platform türü referansına eriştiğinizde Kotlin derleme zamanı hataları oluşturmaz ancak bu referanslar çalışma zamanı hatalarına yol açabilir. Kotlin belgelerindeki aşağıdaki örneğe bakın:

val list = ArrayList<String>() // non-null (constructor result) list.add("Item")
val size = list.size // non-null (primitive int) val item = list[0] // platform
type inferred (ordinary Java object) item.substring(1) // allowed, may throw an
                                                       // exception if item == null

Kotlin, bir platform değeri Kotlin değişkenine atandığında tür çıkarımına dayanır veya beklenen türü tanımlayabilirsiniz. Java'dan gelen bir referansın doğru nullability durumunu sağlamanın en iyi yolu, Java kodunuzda nullability ek açıklamaları (ör. @Nullable) kullanmaktır. Kotlin derleyicisi, bu referansları platform türleri olarak değil, gerçek null değer atanabilir veya atanabilir olmayan türler olarak temsil eder.

Java Jetpack API'leri gerektiği gibi @Nullable veya @NonNull ile açıklama eklenmiş olup Android 11 SDK'sında da benzer bir yaklaşım benimsenmiştir. Bu SDK'dan gelen ve Kotlin'de kullanılan türler, doğru null değer alabilen veya alamayan türler olarak gösterilir.

Kotlin'in tür sistemi, NullPointerException kilitlenmelerini önemli ölçüde azaltır. Örneğin, Google Home uygulaması yeni özellik geliştirme sürecini Kotlin'e taşıdığı yıl içinde boş işaretçi istisnalarından kaynaklanan kilitlenmelerde% 30 azalma gördü.