Android Performans Mühendisi
Android uygulamaları, genellikle üçüncü taraf kitaplıklarından kaynaklanan aşırı katı saklama kuralları nedeniyle optimize edilmeden yayınlanır. Bu da APK'ların şişmesine, soğuk başlatmaların yavaşlamasına ve çalışma zamanı performansının düşmesine neden olur.
Google Play'in DEX optimizasyonu için yeni performans eşikleriyle birlikte, uygulama görünürlüğü artık doğrudan optimizasyon eğrisinin önünde kalmaya bağlıdır.
Bu konuşmada, kural şişkinliğini gidermek için modern bir R8 iş akışı tanıtılmaktadır. Öğrenecekleriniz:
Transkript
Herkese merhaba.
Ben Breana, Android DevRel ekibindeyim.
Ben de Chris Craik, Android Performans ekibindeyim.
Tip güvenli saklama kuralları ve DEX optimizasyonu eğitimine hoş geldiniz.
Bugün, DEX optimizasyonunun ne olduğundan, neden önemli olduğundan ve heyecan verici son güncellemelerden bahsedeceğiz.
Uzun bir süre boyunca DEX optimizasyonu, yalnızca "olsa iyi olur" denebilecek bir şey olarak görüldü.
Bu nedenle, belki bir kez yapılandırıp olduğu gibi bırakırsınız veya belki de özel bir zaman ayırıp bu işi halledersiniz.
Ancak mobil sektör, cihaz belleği kullanılabilirliğini değiştiren önemli donanım tedarik kısıtlamalarıyla karşı karşıya olduğundan, uygulamanızın daha az bellek kullanması, daha hızlı başlatılması, ANR'leri azaltması ve genel olarak oluşturma ve çalışma zamanı performansını iyileştirmesi için iyi optimize edilmiş bir DEX gereklidir.
Google Play, Android ekosisteminin sağlıklı kalmasını ve uygulamalarınızın üst düzey deneyimler sunmaya devam etmesini sağlamak için Şubat ayında yürürlüğe girecek performans eşiklerini içeren yeni uygulama kalitesi şartını duyurdu.
DEX kodu optimizasyonuyla ilgili eşikler de buna dahildir.
Uygulamaların küçültme, optimizasyon ve kod karartma için %25'in üzerinde bir oran elde etmesi gerekir.
Peki bu durum, Android geliştiricisi olarak sizin için ne anlama geliyor?
Optimize edilmemiş DEX kodu, kullanıcı deneyimini olumsuz etkilemenin yanı sıra uygulamanızın Google Play Store'daki görünürlüğünü ve yayınlama özelliklerini de etkileyebilir.
Ancak doğru yerdesiniz ve bu konuşmanın sonunda bu gereksinimleri karşılamakla kalmayıp aşmanızı sağlayacak araçlara sahip olacaksınız.
DEX kodu optimizasyonu, Android'in uygulama optimizasyon aracı R8 ile yapılabilir.
Başka bir uygulama optimizasyon aracı uygulamanızı bu eşiklerin üzerine çıkarabiliyorsa R8'i kullanmanız zorunlu değildir. Ancak R8, doğrudan Android'in araç zincirine yerleşik olarak bulunan ve önerilen yöntemdir.
İyi optimize edilmiş bir DEX, uygulama paketi daha az bellek kullansa da buradaki temel avantajlar performansla ilgilidir: daha az ANR, daha hızlı başlatma ve daha iyi çalışma zamanı performansı.
Uygulamanızı optimize etmenin, uygulamanız ve kullanıcılarınız için gerçek, kanıtlanmış ve olumlu sonuçları vardır.
Örneğin, Grab, R8 tam modunu benimseyerek ve saklama kurallarını hassaslaştırarak uygulamasında %78 optimizasyon, %79 küçültme ve %82 kod karartma elde etti.
Bu slayta bakıp "Tamam, bu sayılar oldukça yüksek ama gerçekte ne anlama geliyor?" diye düşünüyor olabilirsiniz.
Bu iyileştirmeler ve Temel Profiller sayesinde ANR'leri %32 oranında azalttılar ve başlatma süresini %35 oranında iyileştirdiler.
En iyi uygulamalar R8'i ilk kez kullandıklarında DEX boyutunda ortalama %38, kullanıcı tarafından algılanan ANR'lerde ise %16 azalma görülür.
R8'i zaten kullanıyorsanız size güzel bir haberimiz var.
R8'i etkinleştirdikten sonra optimizasyon, küçültme ve kod karartma seviyelerini %50'nin üzerine çıkarmak için ek adımlar atan uygulamalarda DEX boyutunda ortalama %25 daha fazla azalma, başlatma süresinde %4 azalma ve kullanıcı tarafından algılanan ANR'lerde %17 daha fazla azalma görülür. Bu oran, önceki slaytta gösterdiğimiz %16'lık azalmaya ek olarak gerçekleşir.
Bugün, bu tür rakamlara nasıl ulaşabileceğinizi anlatmak istiyoruz.
DEX kodunuzu optimize etmenize ve aşırı geniş ve yanlış yapılandırılmış saklama kurallarından belirli ve dar kurallara geçmenize yardımcı olabilecek araçları ele alacağız.
Öncelikle R8'in uzun yıllardır kullanıldığını belirtmek isteriz.
Bu özelliğin yüksek etkili optimizasyon sağladığını kabul etmekle birlikte, yapılandırmanın hâlâ çok dikkatli bir şekilde yapılması gerektiğini biliyoruz.
Yansıtıcı çağrılar, girişlerin tahmin edilemez olduğu anlamına gelir ve çalışma zamanı kilitlenmelerine yol açabilir.
Bu durum, geçmişte hem uygulama hem de kitaplık ekiplerinin, işlerini garantiye almak için aşırı geniş kapsamlı saklama kuralları eklemesine neden oluyordu.
En iyi uygulamaların %41'inin Google Play tarafından tanımlanan %25 optimizasyon seviyelerinin altında olduğunu biliyoruz. Ancak en iyi uygulamaların %13'ünün bu eşiğin üç katına ulaştığını da biliyoruz.
Bu sayıya ulaşabilen uygulamalar, çok çeşitli sektörlerden ve kullanıcı yolculuklarından geliyor.
Hem kendi saklama kurallarınızı hem de kitaplıklardan devralınan kuralları yönetmek için doğru araçlara ve doğru görünürlüğe sahip olduğunuz sürece uygulamanız için yüksek optimizasyon puanları elde edebilirsiniz.
Bu konuda yardımcı olacak yeni araçlara ve iyileştirmelere geçmeden önce, geçtiğimiz yıl yapılan ve bu optimizasyonu biraz daha kolaylaştıran diğer birkaç R8 iyileştirmesini de vurgulamak istiyoruz.
İlk olarak, yeni uygulama şablonları bir paket kapsamı içerir. Bu, en başından itibaren minimum düzeyde optimizasyon sağlamak için kullanışlıdır.
Birazdan bahsedeceğimiz R8 yapılandırma analiz aracı artık AGP'nin eski sürümlerinde de destekleniyor.
Ayrıca Bazel için resmi olmayan bir şekilde desteklenir.
AGP 9.2'de ise eş yordam temel öğeleri iki kat daha hızlı hale geldi.
AGP 9.2'de yapılan R8 iyileştirmeleri sayesinde Tinder, 9.0.1 sürümünden yükseltme yaptıktan sonra ANR'lerde %13'lük bir düşüş gördü.
AGP 9.3 ayrıca daha net adlandırma, varsayılan kurallar ve kaynak daraltma için daha basit varsayılanlar ve saklama kuralları için AGP kaynak dizini desteği sunan yeniden tasarlanmış bir R8 API'si içerir.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}Bugün ele alacağımız güncellemeler, geliştirici deneyimini de iyileştiriyor.
Öncelikle, eski kural ek yükünü belirlemek ve iyileştirme yolunu saptamak için R8 Yapılandırma Analiz Aracı'ndan ve R8 becerisinden nasıl yararlanacağınızı ele alacağız.
Ardından Chris, geniş kapsamlı ve joker karakterli saklama kurallarından yeni, kesin, tip güvenli ve otomatik olarak üretilmiş ek açıklamalara nasıl geçiş yapılacağını anlatacak.
Son olarak, kilitlenmelere neden olmadan agresif optimizasyon sağlamak için yeni CLI doğrulama araçlarının nasıl kullanılacağını inceleyeceğiz.
R8 Yapılandırma Analiz Aracı, DEX optimizasyonu incelemenize başlamak için ideal bir araçtır.
Aracı daha önce kullanmadıysanız görünümü bu şekildedir.
R8 yapılandırmanızla ilgili bilgileri görselleştiren bir HTML raporudur.
Bu rapor, Analytics Bloat adlı bir kitaplık içeren uygulamam için oluşturuldu.
Tahmin edebileceğiniz gibi bu gerçek bir uygulama veya kütüphane değil ancak üstte mavi renkle vurgulanmış.
Yapılandırma Analiz Aracı, küçültme oranını ölçer. Bu oran, R8'in saklama kuralları tarafından küçültülmesinin engellenmediği sınıfların, yöntemlerin ve alanların yüzdesidir.
Bu değer, R8 ilk küçültme işlemini gerçekleştirdikten sonra hesaplanır.
Ayrıca, yöntem satır içi ekleme ve sınıf birleştirme gibi tüm program optimizasyonlarına açık olan kod yüzdesi olan optimizasyonu da ölçer.
Gizleme ise daha kısa tanımlayıcılarla gizlenebilecek veya yeniden adlandırılabilecek üyelerin yüzdesidir.
R8 Config Analyzer bu puanları son optimizasyonlardan ve DEX derlemesinden önce hesapladığı için bu raporda optimizasyon, küçültme ve kod karartma için gördüğünüz değerlerin Google Play Console'da gördüğünüz değerlerle tam olarak eşleşmeyeceğini de belirtmek isteriz.
Bunun nedeni, Google Play'in nihai DEX çıkışını ölçmesidir.
Bu tutarsızlığın nedeni, geliştirme yaparken hızlı ve yerel bir geri bildirim döngüsüne erişebilmenizi sağlamaktır.
Ancak R8 Yapılandırma Analiz Aracı'nda gördüğünüz puanlar, Google Play Store'a yüklemeden önce DEX optimizasyon potansiyelini doğrulamak için yine de çok faydalıdır.
Raporda, rapora girdiğinizde gördüğünüz ana görünüm olan Saklama Kuralı Analizi tablosu da yer alır.
Buradan herhangi bir kuralı tıklayarak belirli bir kuralın tuttuğu sınıfların, yöntemlerin ve alanların tam sayısını görebilirsiniz. Böylece hangi kuralların gereğinden fazla veri tuttuğunu anlayabilirsiniz.
Ayrıca, burada yakınlaştırılan kapsanan kuralları da vardır.
Bunlar, daha geniş kapsamlı bir kuralla çakışan kurallardır.
Bu anahtar kelimeler vurgulanır. Böylece geniş olanı kaldırıp yalnızca daha dar olanı tutabilirsiniz.
Ayrıca, uygulamanızın kararlılığını riske atmadan silinebilecek kurallar olan kullanılmayan kuralları da gösterir.
Config Analyzer, eski kural birikimiyle başlayan birkaç sorunu çözmek için tasarlanmıştır.
Daha önce, kural bazında görünürlük olmadığından eski kuralları silmek riskliydi.
Bu nedenle ekipler bunları olduğu gibi bırakır veya daha geniş joker karakterler ekler.
İkinci olarak, kitaplık kuralı kör noktalarını keşfetmenize yardımcı olur.
Bu nedenle, geçmişte bir SDK eklediğinizde, bu kitaplıkla birlikte gelen bir kuralın çok geniş olup olmadığını veya örneğin 10 sürüm sonra bile gerekli olup olmadığını anlamak zordu.
Kütüphaneler konusuna da birazdan daha ayrıntılı olarak değineceğiz.
Üçüncü olarak, optimizasyonun etkisini değerlendirmenize yardımcı olur.
Önceden, bir kuralın iki sınıfı mı yoksa 2.000 sınıfı mı sakladığını anlamanın kolay bir yolu yoktu.
Şimdi R8'in uygulamam ve daha önce incelediğimiz kitaplık bağlamında nasıl çalıştığına bakalım.
Burada bir uygulama modülüm ve üç kitaplığım var.
Sağdaki üçüncü kitaplığa dikkat edin.
Bu, paket genelinde bir saklama kuralı içeren Analytics Bloat kitaplığıdır.
İlk olarak R8, tüm birleştirilmiş saklama kurallarından kök giriş noktasını tanımlar. Bu kurallar burada sarı renkle vurgulanmıştır.
Genellikle bunlar, manifestteki sınıflar gibi uygulamanın ana giriş noktaları olmalıdır.
Analytics Bloat kitaplığının joker karakterli saklama kuralı nedeniyle, kitaplıktaki her sınıf bir giriş noktası olarak değerlendirilir.
Ardından R8, bu köklerden çağrı grafiği boyunca erişilebilirliği izler.
Kökten erişilebilen tüm sınıflar yeşil renkle işaretlenerek saklanır. Referans verilmeyen sınıflar ise ölü kod olarak sınıflandırılır ve kırmızı renkle işaretlenir.
R8 daha sonra ağaç sallama işlemi gerçekleştirir ve referans verilmeyen ölü kodu kaldırır.
Bu ağaç sallama veya ilk küçültme işleminden sonra geriye kalanlar şunlardır.
Uygulamamızın küçülme puanı %42,5.Bu oran, Google Play'in %25'lik eşiğinin üzerinde olsa da istediğimiz seviyede değil.
Bunun nedeni büyük ölçüde, Analytics kitaplığındaki paket genelinde geçerli olan saklama kuralıdır.
Bu nedenle, kalan 40 sınıftan yalnızca 17'si daha da küçültülebilir.
Burada vurgulamak istediğim nokta, tek bir hatalı saklama kuralının uygulamanızın optimizasyonunda büyük sorunlara yol açabileceğidir.
Bunu göz önünde bulundurarak kitaplıklara kısaca odaklanalım.
Ekibimiz geçtiğimiz yıl, kitaplık tüketici saklama kurallarının ekosistem için optimizasyonu nasıl etkilediğini anlamak amacıyla yoğun bir çalışma yürüttü.
Birçok kitaplık, kararlılık adına aşırı geniş kurallarla birlikte sunulur.
Ayrıca, uygulama geliştiricilerin kendi uygulamalarına eklemeleri için belgelerinde genel kurallar da belirtirler.
Ancak geniş kurallarla gönderim yapmak kararlılık için gerekli değildir ve performansı olumsuz etkiler. Bu durumda maliyetleri öncelikle uygulama geliştiriciler olarak sizin karşılamanız gerekir.
Analytics bloat kitaplığı gibi paket genelinde bir saklama kuralı tarafından tüm kitaplığın korunduğu durumlar gördük. Bu kitaplık söz konusu olduğunda, bu durum uygulamamın optimizasyon potansiyeli üzerinde geniş kapsamlı etkilere sahipti.
Google'ın kendi SDK'larının bile bu durumdan etkilendiğini ve geçtiğimiz yıl içinde bir düzineden fazla Google kitaplığının aşırı geniş kapsamlı saklama kurallarını düzelttiğini de belirtmek isterim.
Yapılandırma analiz aracını kullanmak istiyorsanız bu araç doğrudan AGP 9.3'e entegre edilmiştir ancak R8 sürümünüzü yükseltirseniz eski sürümlerde de kullanılabilir.
Bu komutu çalıştırdığınızda, saklama kurallarınızı, küçültme, optimizasyon ve kod karartma işlemlerinin tek tek kurallar ve bir bütün olarak uygulamanız üzerindeki etkilerini gösteren HTML raporu oluşturulur.
Tam bir R8 yayınlanan derlemesi çalıştırıyorsanız rapor otomatik olarak oluşturulur ve bu dosya yolunda bulunabilir.
Bu bağımsız görevin avantajı, oluşturulmasının tam derlemeye göre daha kısa sürmesidir.
WHOOP ekibi (ki aramızda olabilirler) yapılandırma analiz aracını çalıştırdı ve küçültme oranını %74, kod karartma oranını %74 ve optimizasyon oranını 0 olarak gördü.
Tahmin edebileceğiniz gibi ekip, bu optimizasyon değerini görünce çok şaşırdı.
Yapılandırma analiz aracının sağladığı görünürlük sayesinde, derlemelerinin optimizasyonu devre dışı bırakan eski bir ProGuard dosyası kullandığını fark ettiler. Güncel sürüme geçerek uygulamaları için optimizasyonu etkinleştirebildiler.
Bu sayede %55 optimizasyon ve APK boyutunda %4 küçülme sağlandı.
Temel Profiller ile birlikte kullanıldığında, baştan başlatma sürelerini %38 oranında iyileştirdiler ve aşırı donmuş kareleri %28 oranında azalttılar.
Soldaki ekranda Temel Profiller kullanılmıyor, sağdaki ekranda ise kullanılıyor. Gördüğünüz gibi, Temel Profiller'in eklenmesiyle uygulama daha hızlı başlatılıyor.
Bir kez daha dinleyelim.
Mükemmel.
Bu bağlantı sizi yapılandırma analizörü kılavuzuna yönlendirir. Raporu manuel olarak incelemek iyi bir yöntemdir ancak uygulamanızın R8 yapılandırmasının durumuna bağlı olarak çok zaman alabilir.
Süreci hızlandırmak için R8 Analyzer becerisini kullanabilirsiniz.
$ android skills list
$ android skills add r8-analyzerTemsilciden R8 yapılandırmanızı analiz etmesini isteyebilirsiniz. Bu beceri, R8 yapılandırma analiz aracı raporunu oluşturmakla kalmaz, aynı zamanda sizi en etkili kurallara yönlendirir.
Örnek bir çıkış görüyorsunuz. Birkaç ayrıntıya değinmek gerekirse %42 optimizasyon puanı, kod tabanının %52'sinin (daha doğrusu %58'inin) R8 tarafından optimize edilemediği anlamına gelir.
Özellikle saklama kuralı bölümüne bakarsak analytics bloat kitaplığı için paket genelindeki saklama kuralı, kod tabanının %22'sinin optimizasyonunu engelliyor. Beceri, bu saklama kuralını daraltmamızı da öneriyor. Çünkü bildiğimiz gibi bu kural çok geniş.
Artık R8 yapılandırma analiz aracını ve beceriyi kullanarak uygulamanın şişmesine ve DEX optimizasyonunun engellenmesine neden olan sorunu teşhis edebilirsiniz.
Ancak aşırı geniş bir saklama kuralı bulduğunuzda, yanlışlıkla beklenmedik bir kilitlenmeye neden olmadan bu kuralı nasıl güvenli bir şekilde değiştirebilirsiniz?
Sözü Chris'e bırakıyorum. Chris, eski saklama kurallarından yeni tip güvenli otomatik olarak üretilmiş saklama ek açıklamalarına nasıl geçiş yapacağınızı gösterecek.
Teşekkürler Breana.
Peki, buraya nasıl geldik?
Bu şişirilmiş analiz kitaplığı ve her şeyi saklayan saklama kuralı nedeniyle karşılaştığımız temel sorun, yansımadır.
Şimdi, hatırlatmak amacıyla yansıtıcı koda biraz göz atalım.
fun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}Bu, belirli bir nesneyi yansıtmalı olarak genişleten bir nesne örneklendirme işlevidir.
DEX optimizasyonu bağlamında bunun dezavantajı, tüm bu statik çağrıları çalışma zamanına kadar geciktirmesidir.
R8 optimizasyon aracımız, yalnızca koda bakarak bu sınıf adının nereden geldiğine bağlı olarak burada ne olduğunu göremez.
Bunlar, Dex optimizasyonunu varsayılan olarak bozan durumlardır. Bu nedenle, genellikle bir Keep kuralı eklemeniz gerekir.
İdeal olan, bu durumdan kaçınmaktır.
İdeal olan, özellikle de serileştirme ve bağımlılık ekleme için bu tür sorunları tamamen ortadan kaldırabilen muhteşem kod oluşturma araçlarına sahip olduğumuz 2026 yılında, yansımadan tamamen kaçınmaktır.
Ancak hepsi bu kadar değil, değil mi?
Bazı durumlarda yansıma hâlâ gerekli.
Şimdi bu şişkin kitaplıkta bunun tam olarak nasıl gerçekleştiğini inceleyelim.
Örneğin, yaygın bir bağımlılık türü olan isteğe bağlı bağımlılık.
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}Burada, kitaplık sınıf yolunda erişilemeyen belirli bir türü şişiriyoruz. Bu nedenle, bunu bir dizeyle yapmamız gerekiyor.
Bu, kitaplığın oluşturulduğu sırada mevcut olabilecek veya olmayabilecek belirli bir sınıfa atıfta bulunur. Bu nedenle, bu kitaplığın ilgili bir kitaplıktaki üyeyi bulmak için yansımayı kullanması son derece mantıklıdır.
Burada ele alacağımız bir diğer durum da alt sınıfları dinamik olarak örneklendirmektir.
Bu nedenle, özellikle bir manifesto dosyasından veya sunucu tarafından gönderilen bir şey varsa yerel uygulamayı bilgilendiririz: "Lütfen bu şeyi şişirin, bu şeyi geç oluşturun."
Burada gördüğümüz üçüncü kategori ise utanç verici bir şekilde serileştirme.
Tüm üyeler arasında iterasyon yaparak gerçekleşen bir JSON serileştirme işlemim var.
Bu gerçekten iğrenç bir durum ama bazen bu tür şeyler olabiliyor.
Her zaman gerektiği anda düzeltilmiyor.
Ancak bunun sonucunun ne olduğuna bakalım.
Bu düşünme çağrıları biriktikçe, kolay olanı yapma ve kullanıcı deneyiminiz yerine geliştirici deneyimi için optimizasyon yapma konusunda daha fazla motivasyon elde edersiniz.
Bu durum can sıkıcı olabilir. Bu yüzden bir yapılacak iş ekleyeceğim ve her şey yoluna girecek.
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };Bir gün gelip düzelteceğim, değil mi?
Şimdi, ne yapmamız gerektiğinden, en azından bugünden önce ne yapmamız gerektiğinden ve burada ideal bir saklama kuralını nasıl yazacağınızdan bahsedelim.
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}Hazır olun.
İlk kısımda, bu yansıtıcı işlevden döndürdüğümüz sınıfı korumak istiyoruz.
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}Aslında hayır, bekleyin.
Biliyor musunuz?
Bu aslında doğru değil.
Bu bir Kotlin sınıfı. String türünde olduğundan emin olmam gerekiyor. KT'yi ekleyeyim.
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}Aslında burada önemli olan, EventFilter döndürmüyor olmam. EventFilter alt sınıfı döndürüyorum. Bu nedenle, bunu doğru şekilde genişletebilmek için EventFilter sınıfını genişleten sınıfları tutmam gerekiyor.
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}Bu işlemi daha verimli bir şekilde yapmak istiyorsam, bu yöntem işe yarayacaktır ancak daha verimli hale getirmek için iyi bir kitaplık geliştiricisi olarak daha da ileri gidip ne yapmalıyım?
Yani -keepclasseswithmembers demeliyim ve yalnızca nesneyi şişirmek için kullandığım bu özel <init> oluşturucuya sahip sınıfları tutmalıyım.
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}Ayrıca, gerçekten iyi bir şey yapmak istersem bu saklama kuralının hedeflediği tüm API'lerde daha fazla optimizasyon yapılmasına izin vermek için allowaccessmodification eklerim. Çünkü R8'in daha fazla optimizasyon yapmasını engellemek istemiyorum, iyi bir vatandaş olmaya çalışıyorum.
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}Son adım ise neredeyse hiç kimsenin yapmadığı bir adım. Çünkü ekstra bir iş gerektiriyor ancak bir kütüphane için harika bir şey: Koşullu hale getirmek.
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}Kavramsal olarak, bu işlevi gerçekten çağırmadığınız sürece tüm bu alt sınıfları tutmanız gerekmez. Keep kuralının en üstündeki bu if ifadesi de bunu yapar.
Bunu yapanların el kaldırmasını isteyebilirim ama neredeyse hiç kimsenin yapmadığını biliyorum.
Yani, gerçekten yazmak, koşullu hale getirmek.
Peki, bunu biraz daha iyi hale getirebilir miyiz?
Öncelikle, "see", "like" ve "okay" kelimelerine bakalım.
Belki de bunu yapmanın farklı bir yolu vardır.
Manuel olarak dar kapsamlı bir saklama kuralı yazarsam ne olur?
Teknik olarak mümkün olsa da yazması, okuması ve sürdürmesi zordur. Çünkü içinde, altta bir şey olduğunda kolayca bozulabilen çok sayıda küçük dize türünde bit vardır.
Altında bir şey varsa o değişir.
Alternatiflerimiz de var.
@Keep gibi ek açıklamalarımız var.
@Keep, uzun süredir AndroidX ek açıklamalar paketinde yer alıyor ancak günümüzde pek tercih edilmiyor.
@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }Kitaplık geliştiricileri için iyi bir çözüm değildir ve aslında şirket içinde bir nevi yasaklanmıştır. Çünkü bakımı daha kolay olsa da çok esnek değildir ve daha önce bahsettiğim optimizasyonlardan bazılarını içermez.
Aslında yapamaz.
Bu analiz, çağrı merkezi analizine izin vermez.
Şimdi, daha önce bahsettiğimiz şu duruma bakalım: "EventFilter'ime @Keep ekleyebilirim, değil mi?"
@Keep
class EventFilter { … }Hayır.
@Keep aslında alt sınıfları hedeflemez, yalnızca ilgili sınıfı hedefler.
@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }Bu nedenle, aslında tüm alt sınıfları hedeflemem gerekiyor ve artık yazması zor olan tek bir Keep kuralıyla yazacağımdan daha fazla @Keep ek açıklaması yazıyorum.
Yani her ikisinde de sorunlar var, değil mi?
Bu tür aramalar kolayca unutulur, geç fark edilir ve varsayılan olarak arayan kişiye bağlı değildir.
Özellikle kitaplıklar için daha iyi bir yöntem istiyoruz.
Kullanıcıların, ürünleri kutudan çıkardıkları anda kolayca kullanabilmelerini istiyoruz.
Burada gördüğümüz en büyük sorunlardan biri, bu nesnelerin aslında korumaya çalıştığımız yansımadan çok uzakta olması.
Sonuç olarak, istediğimiz çözüm, hedeflediğimiz şeyleri şişiren yansıma kodunun hemen yanında yer alan bir çözümdür.
Şimdi, tam olarak bu sorunu çözmek için tasarlanmış yeni AndroidX kitaplığı ve eklentisi olan Jetpack Keep'ten bahsedelim.
Jetpack Keep'in asıl amacı, ek açıklamalardan saklama kuralları oluşturarak başlamak. Ancak bunun da ötesine geçerek doğrudan kod tabanınızda bulunan yansımadan ek açıklamalar oluşturmak.
Kurulumu çok kolaydır.
plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}Bir Gradle eklentisi ve ek açıklama kitaplığıdır.
Ancak bunun derleme zamanında nasıl çalıştığına bakalım.
Bilinen bir türün alt sınıfını şişirdiğim önceki örneği ele alalım. Bu durumda ne yapmam gerekir?
lintFix koşuyorum.
./gradlew analyticsbloat:lintFixLint, bu yöntemin içindeki tüm yansımaları analiz eder ve "Bu ek açıklamaya ihtiyacınız var." der.
@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}Bu ek açıklama, bu çağrıdaki yansımanın ne yaptığını tam olarak ifade eder.
"Bu türden bir sınıfı şişiriyorum" diyor.
Bu örnekte EventFilter, bilinen bir sınıftır.
Referans vermek için dize kullanmam gerekmiyor.
Bu ek bir güvenlik önlemi ve hatta tam olarak hangi kurucunun tutulduğunu belirtmek için parameterTypes'yi de kodluyorum.
Peki bu ne işe yarar?
Örneğin, bu durum nihai R8 derlemesini nasıl etkiler?
Saklama kuralını oluşturur. Daha önce gördüyseniz bu, daha önce sahip olduğumuz ideal saklama kuralının aynısıdır.
# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}Tam olarak aynı sınıfları ve alt sınıfları hedefliyor ve bu yansıma noktasına bağlı.
Ayrıca tüm alt sınıflara ek açıklama ekleme konusunda endişelenmem gerekmiyor.
Başka bir yerde ayrı bir saklama kuralı dosyası tutmam gerekmiyor.
Bu, saklama kurallarınızı yönetmenin çok daha kolay bir yoludur.
İsteğe bağlı bağımlılık örneğine tekrar bakalım.
@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}Sınıf, sınıf yolunda olmasa da sınıfa referans verebiliyorum.
Bunu bir dize kullanarak yapıyorum ancak diğer her şey, parametre türleri, sınıf referanslarıyla referans verilebiliyor, bu yüzden ben de öyle yapıyorum.
Bu durumda da ideal saklama kuralını oluşturuyoruz.
# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}Koşullu, spesifik ve hedeflidir. Hedeflenen sınıflarda mümkün olan her yerde optimizasyon yapılmasını sağlar.
Burada alanlar üzerinde yansıma var. Bu örnekte, bir nesnenin içindeki tüm alanları yineliyordum. Ancak burada da bu yıldız operatörüyle tüm alanları hedefleyerek bunu sorunsuz bir şekilde hallediyoruz.
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}Bu sayede tüm alanları hedefleyebiliyorum. Bakın, tam da beklediğiniz saklama kuralını oluşturuyor. Uygulamamda olmaması gereken bu inanılmaz talihsiz yansıma için bunu yapıyor. Ama geri dönüp bunu temizleyeceğim, söz veriyorum.
# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}Peki, Jetpack Keep ne işe yarar?
Saklama kuralı bakımını önemli ölçüde azaltır.
Otomatik olarak oluşturabileceğiniz her şey, saklama kurallarını sürdürmek için endişe alanınızdan ve risk alanınızdan kaldırılır.
Ayrıca lint'in determinizmini de sağlar.
Sadece siz geçiş yapmaya çalışırken sorunları düzeltmekle kalmıyor, aynı zamanda kod her değiştiğinde yeni yansımaları ve yansıtıcı değişiklikleri yakalıyor. Bu güvenlik kesinlikle çok önemli.
Bunu kullanabilir miyim?
Peki buradaki bağımlılıklar nelerdir?
Eklenti ve lint için AGP 8.7 gerekir.
Neredeyse herkesin bu grupta olması gerekir.
Ancak bu özelliği nerede kullanabileceğiniz konusunda da çok esnektir.
Bu özelliği uygulamalarda kullanabilirsiniz.
Bunu Android kitaplıklarında, AAR'lerde, KMP kitaplıklarında ve JAR'larda kullanabilirsiniz. Bu durumda biraz manuel bağlantı yapmanız gerekir.
Ancak buradaki temel özellik, kitaplık geliştiricisi olarak kullanımının güvenli olmasıdır.
Birisi R8'in eski bir sürümünü veya kitaplığınızı kullanan bir üçüncü taraf aracını kullanıyor olsa bile gerçek standart tüketici kurallarını dışa aktarırız. Böylece herkes sizi kullanmaya devam edebilir ve yeni bir uyumsuz biçim kullanma konusunda endişelenmesine gerek kalmaz.
Jetpack Keep'in optimizasyon yapmanıza ve yansıtıcı kod çağrılarınızı koruma konusunda daha az endişelenmenize nasıl yardımcı olabileceğinden bahsettim. Şimdi de son sürüm aşamasını bir kez daha kontrol etmek için Android CLI'da kısa süre önce kullanıma sunduğumuz yeni özelliklerden bahsedelim.
Breana daha önce Play Bulguları'nda boyutlandırma bilgileriniz, DEX boyutunuz ve optimizasyon yüzdeleriniz hakkında bilgi veren araçlar olduğundan bahsetmişti.
Artık bu özelliği Android KSA üzerinden de kullanıma sunmaya başladık.
Google Play Store ile tamamen aynı mantığı kullandığından, paketi Google Play Store'a yüklemeniz gerekmeden CLI'da analiz ettirebilirsiniz.
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>Ayrıca, yerel analizle aynı bilgileri doğrudan ve anında alırsınız.
Bu örnekte, uygulamanın optimizasyon oranının çok düşük olduğuna dair bir uyarı aldığını görebilirsiniz. Ayrıca, Play Store'un size söyleyeceği gibi "DEX boyutunuz bu, optimizasyon yüzdeleriniz de bunlar" şeklinde bir uyarı da gösteriliyor.
Ayrıca burada bir JSON çıkışı da var. Bu sayede, bu değerlerden herhangi birinin önemli ölçüde düşmesine karşı regresyon testini otomatikleştirmek için bir komut dosyası yazmak isterseniz (çünkü bunlar Google Play'in zorunlu kıldığı sayılardır ve bunları takip etmek istersiniz) bu JSON değeriyle (bu JSON raporlarıyla) bunu kolayca yapabilirsiniz.
{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}Burada da aynı sayıların olduğunu görüyoruz.
android analyze'ın tek özelliği bu değil.
android analyze, kitaplıklarda da çalıştırılabilir.
$ android analyze <library.aar>
$ android analyze <library.jar>Breana'nın da belirttiği gibi, kütüphanelerin içinde talihsiz saklama kuralları olan kütüphanelerle ilgili birçok sorun var.
Şimdi bu sorunlardan bazılarını incelemek için bu yöntemi kullanalım.
Bu aracı elimdeki belirli bir AAR'de çalıştırdığımda, bu kitaplığın en üstte vurgulanan bir dizi sorunu olduğunu görebiliyorum.
Bu kitaplığın saklama oranı yüksek. Bu, sınırsız saklama kurallarıyla çok fazla öğe sakladığı anlamına gelir.
Bu kural, optimize edilmesini engelleyen geniş kapsamlı saklama kuralları içeriyor. Aşağıda, bunun ne kadar kötü olduğunu gösteren bazı rakamlar var. Ayrıca, belki de en korkutucu olanı, hiçbir şeyi hedeflemeyen saklama kuralları içeriyor. Bu da muhtemelen başka birini sınırsız bir şekilde hedefledikleri anlamına geliyor.
Çok tehlikeli kod kokuları.
Tüm bunları bir AAR'de kontrol edebilirsiniz. Özellikle kitaplık yayıncısıysanız bu, hatalı saklama kuralları göndermediğinizden ve saklama kuralları yazma konusunda uzman olmayan biri kitaplık üzerinde çalışırken bu kuralların sızmasına izin vermediğinizden emin olmanın harika bir yoludur.
Burada, bir tutma oranı da çıkardığımızı görebilirsiniz.
Bu, kitaplığınızın genel kalitesini değerlendirmek için iyi bir yöntemdir.
Tek bir kitaplığı tamamen bağımsız olarak, referans olmadan R8 ile optimize edersem bunun sıfır bayt olduğunu görmem gerekir, değil mi?
İçinde hiçbir şey tutulmadığı için temelde boş olması gerekir.
İdeal olan budur.
Bu durumda, bu geçerli değildir.
R8 ve D8 boyutunu doğrudan görebilirsiniz.
Hatta en kötü saklama kurallarından bazılarını da vurgulayarak bunları sizin için biraz daha görünür hale getiriyoruz.
Bu da JSON biçimindedir. Dolayısıyla, kitaplık güncellemelerini kısıtlamaya çalışan bir uygulama geliştirici veya kitaplık kalitesinde büyük gerilemeleri önlemeye çalışan bir kitaplık geliştiriciyseniz aynı analizi yapabilir ve Android KSA'yı sarmalayan basit bir komut dosyasıyla bu sorunların oluşmasını önleyebilirsiniz.
{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}Bu nedenle, bir kitaplıkta sorun bulursanız lütfen önce güncellemeyi deneyin.
Breana'nın daha önce de belirttiği gibi, bazı talihsiz saklama kurallarını düzeltmek için bir düzineden fazla Google kitaplığını düzelttik ve düzeltilen başka kitaplıklar da var, ancak bunları kullanan uygulamalar tarafından güncellenmiyorlar.
Bu, harika bir ilk adım.
İkinci olarak, bir sorun arayabilir ve kitaplık geliştiricisine ulaşabilirsiniz. Muhtemelen onlara "Hey, çok fazla şey tutuyorsun.
Lütfen uygulamamı optimize etmeme yardımcı olun."
Özetlemek gerekirse R8 derlemenizde gerçekleşen her şeyi, nelerin optimize edilip nelerin edilemeyeceğini bildiren her şeyi incelemenize yardımcı olan R8 Config Analyzer'dan bahsettik.
Jetpack Keep'ten bahsettik. Bu yeni kitaplık, keep kurallarını otomatik olarak yazmanıza yardımcı olur.
Android KSA ise kontrol etmenize, son adımı tekrar kontrol etmenize ve sizin, temsilcinizin veya ekibinizdeki genç mühendisin tüketicileriniz için hiçbir şeyi bozmadığından emin olmanıza yardımcı olan yeni bir araçtır.
Daha fazla bilgi edinmek isterseniz R8, yapılandırma analiz aracı ve Jetpack Keep ile ilgili belgelere göz atabilirsiniz.
Android KSA ile ilgili belgelerde, analiz komutu ve bu komutun nasıl kullanılacağı açıklanmaktadır.
Teşekkür ederiz.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}$ android skills list
$ android skills add r8-analyzerfun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# TODO: clean this up
-keep com.example.analytics.bloat.** { * };fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }@Keep
class EventFilter { … }@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}./gradlew analyticsbloat:lintFix@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}$ android analyze <library.aar>
$ android analyze <library.jar>{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}