İçeriğe geç
Tüm yazılar

Kotlin, Room ve Compose için R8 ve ProGuard: yalnızca release'te olan çökme

Bir Room + Compose Android uygulaması debug'da neden kusursuz çalışıp üretimde neden çöküyor ve bunu bir kullanıcı bulmadan önce yakalayan spesifik R8/ProGuard keep kuralları.

MFKAPPS 3 dk okuma

Cihazınızda her testi geçen bir derlemenin, tam olarak aynı sürümde bir kullanıcı için çökmesi, Android’in sunduğu en rahatsız edici hatalardan biridir ve genellikle tek bir sebebi vardır: minifyEnabled true. Debug derlemeleri küçültme ve gizlemeyi tamamen atlar, bu yüzden R8’in dokunacağı hiçbir şey, zaten Play Store’da olan release APK’sına — yani hiç çalıştırmadığınız koda — kadar test edilmez.

Bu, küçültmeye karşı bir argüman değil. Bir Kotlin + Compose uygulamasının metot sayısını küçültmek ve kullanılmayan kodu ayıklamak, kurulum boyutu ve soğuk başlatma için gerçek anlamda yardımcı oluyor ve Google giderek bunu bekliyor. Çözüm daha küçük: kod tabanınızın hangi kısımlarını R8’in statik olarak çözümleyemediğini bilin ve ona onlara dokunmamasını söyleyin.

R8, “açıkça” çalışan şeyleri neden bozar

R8, uygulamanızın giriş noktalarından — activity’ler, manifest, doğrudan çağrılan her şey — erişilebilirliği izleyerek neyi tutacağına karar verir. Yalnızca yansıma (reflection) yoluyla, bir sınıf adının string olarak saklanmasıyla veya bir kütüphanenin çalışma zamanında bir türü yeniden oluşturmasıyla erişilen kod, bu izleme için görünmezdir. R8 onu yeniden adlandırır veya kaldırır, uygulama sorunsuz derlenir ve hata yalnızca o yansıma yolu gerçekten çalıştığı anda ortaya çıkar.

Bu durumun tipik bir yerel-öncelikli Android uygulamasını pratikte ısırdığı iki yer:

Ad ile örneklenen WorkManager worker’ları

Arka plan işi zamanlıyorsanız — Hydrame’deki hatırlatıcı zamanlamasının arkasındaki türden — ListenableWorker alt sınıflarınız doğrudan çağrılmaz. WorkManager, worker’ın tam nitelikli sınıf adını kendi veritabanında saklar ve iş gerçekten çalıştığında — bazen onu zamanlayan uygulama süreci çoktan yok olmuşken, saatler sonra — Class.forName ile örneği yeniden oluşturur. O sınıf adını gizleyin ve çökme zamanlama anında olmaz — daha sonra, sessizce, WorkManager’ın dispatcher’ının derinliklerinde kodunuza işaret eden hiçbir yığın izi olmayan bir ClassNotFoundException olarak olur:

# proguard-rules.pro
-keep public class * extends androidx.work.ListenableWorker {
    public <init>(android.content.Context, androidx.work.WorkerParameters);
}

Son work-runtime sürümleri buna yakın bir consumer kuralını zaten içeriyor, ama “muhtemelen kütüphane tarafından zaten karşılanıyor” bir release derlemesinde inanarak kabul edilecek bir şey değil — varsaymak yerine doğrulayın.

Yedekleme ve dışa aktarma için yansıma tabanlı JSON

Gerçek bir dışa/içe aktarma akışına sahip herhangi bir yerel-öncelikli uygulama — Stocky’nin kiler verilerini yedeklemek için kullandığı türden — entity data class alanlarını JSON’a ve geri serileştiriyordur. Bu serileştirme yansıma tabanlı bir kütüphaneden (Gson, ya da codegen olmadan Moshi) geçiyorsa, JSON anahtarları varsayılan olarak Kotlin özellik adlarınızdır. R8, güvenli olduğunu düşündüğü private alanları yeniden adlandırır, ürettiği dışa aktarma dosyası hâlâ normal görünür ve hata yalnızca içe aktarmada ortaya çıkar; yansıma tabanlı okuyucu artık küçültülmüş sınıfta var olmayan bir alan adı arar. Çökme yok, hata yok — sadece geri gelmeyen veri, sessizce.

-keepclassmembers class com.example.app.data.** {
    <fields>;
}
-keepattributes Signature

Bunu tüm kod tabanınıza değil, gerçek data paketinize kapsayın — genel bir -keep class **, küçültmenin amacını tamamen ortadan kaldırır.

Gerçekten gönderilen derlemeyi test etmek

Bunları yakalamanın tek güvenilir yolu, debug değil, release derlemesini çalıştırmaktır:

buildTypes {
    release {
        isMinifyEnabled = true
        isShrinkResources = true
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}

./gradlew assembleRelease, APK’yı gerçek bir cihaza kurun ve her gönderimden önce tüm temel akışları elle adım adım gözden geçirin — bir hatırlatıcı zamanlamak, veri dışa aktarmak, sonra uygulamayı zorla kapatıp yeniden açarak içe aktarmanın onu hâlâ okuyabildiğini doğrulamak. Bu, aksi takdirde Play Console’un çökme raporları dolmaya başlayana kadar görünmez olan bir hata sınıfına karşı beş dakikalık bir kontrol; üçüncü taraf bir çökme raporlayıcısı olmayan bir uygulama için, sonradan elde edeceğiniz tek görünürlük bu.

Saklamaya değer bir alışkanlık daha: R8’in her release için ürettiği mapping.txt dosyasını saklayın (Play Console bunu isteyecek, App Bundle üzerinden yüklerseniz otomatik olarak da yapar). Bu olmadan, üretimde gerçekten size ulaşan bir çökme, tek harfli sınıf ve metot adlarıyla dolu, R8 için okunabilir ama sizin için değil bir yığın izidir.

Çıkarım

Küçültme yüksek sesle başarısız olmaz. Elle yeniden test etmediğiniz tek kod yolunda, kontrol ettiğiniz her şeyi geçen derleme gönderildikten haftalar sonra başarısız olur. Ada göre örneklenen her sınıfı — worker’lar, yansıma tabanlı serileştiriciler, bir kütüphanenin bir string’den yeniden oluşturduğu her şey — bir kütüphanenin varsayılanlarının sizin için karşılayacağını umduğunuz değil, kasıtlı olarak yazdığınız bir keep kuralı olarak ele alın.