Ví dụ keep rule và Jetpack Keep
Các ví dụ dưới đây dựa trên những tình huống hay gặp khi dùng R8 để tối ưu, nhưng cần hướng dẫn kỹ hơn để viết keep rule. Cú pháp keep rule xem bài R8: Keep rule.
Reflection
Phần tiêu đề “Reflection”Nói chung, để có hiệu năng tốt nhất thì không nên dùng reflection. Nhưng có lúc không tránh được. Các ví dụ sau hướng dẫn viết keep rule cho những trường hợp dùng reflection phổ biến.
Nếu reflection nằm trong code app hoặc library của bạn, bạn có thể dùng annotation của Jetpack Keep thay vì tự viết keep rule. Một Gradle plugin sinh keep rule từ annotation trong mã nguồn, kể cả consumer keep rule cho library.
Reflection nạp class theo tên
Phần tiêu đề “Reflection nạp class theo tên”Library hay nạp class động bằng tên class dạng String. R8 không phát hiện được class nạp theo cách này và có thể xoá những class nó cho là không dùng.
Ví dụ có một library và một app dùng library đó: library có bộ nạp tạo instance của interface StartupTask do app implement.
Code của library:
// Interface cho task chạy một lần.interface StartupTask { fun run()}
// Object của library, nạp và chạy task.object TaskRunner { fun execute(className: String) { // R8 không giữ các class được chỉ bằng chuỗi này lúc chạy val taskClass = Class.forName(className) val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask task.run() }}App dùng library có code:
// Task của app để cache trước dữ liệu.// R8 sẽ xoá class này vì nó chỉ được nhắc tới qua một chuỗi.class PreCacheTask : StartupTask { override fun run() { // Log này không bao giờ xuất hiện nếu R8 xoá class. Log.d("AppTask", "Warming up the cache...") }}
fun onCreate() { // Library được bảo chạy task của app theo tên. TaskRunner.execute("com.example.app.PreCacheTask")}Trong tình huống này, library nên có file consumer keep rule với rule:
-keep class * implements com.example.library.StartupTask { <init>();}Không có rule này, R8 xoá PreCacheTask khỏi app vì app không dùng class đó trực tiếp, và việc tích hợp bị hỏng. Rule tìm các class implement interface StartupTask của library và giữ chúng cùng constructor không tham số, để library tạo instance và chạy được PreCacheTask.
Reflection với ::class.java
Phần tiêu đề “Reflection với ::class.java”Library có thể nạp class bằng cách để app truyền thẳng đối tượng Class, cách này chắc chắn hơn nạp theo tên. Nó tạo ra tham chiếu mạnh tới class mà R8 nhận ra được. Nhưng dù R8 không xoá class, bạn vẫn cần keep rule để khai báo rằng class được tạo instance bằng reflection và bảo vệ các member bị truy cập qua reflection, như constructor.
Ví dụ library tạo instance của StartupTask bằng cách nhận trực tiếp tham chiếu class.
Code của library:
// Interface cho task chạy một lần.interface StartupTask { fun run()}// Object của library, nạp và chạy task.object TaskRunner { fun execute(taskClass: Class<out StartupTask>) { // Class không bị xoá, nhưng constructor của nó có thể bị xoá. val task = taskClass.getDeclaredConstructor().newInstance() task.run() }}App dùng library có code:
// Task của app để cache trước dữ liệu.class PreCacheTask : StartupTask { override fun run() { Log.d("AppTask", "Warming up the cache...") }}
fun onCreate() { // Library nhận tham chiếu trực tiếp tới class task của app. TaskRunner.execute(PreCacheTask::class.java)}Library nên có file consumer keep rule với các rule:
# Cho phép xoá bất kỳ class implement StartupTask nào nếu không dùng.-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask# Giữ constructor mặc định, được gọi qua reflection.-keepclassmembers class * implements com.example.library.StartupTask { <init>();}Các rule này hợp hoàn toàn với kiểu reflection này, cho phép tối ưu tối đa mà code vẫn chạy đúng. Chúng cho R8 obfuscate tên class và shrink (xoá) các implementation của StartupTask nếu app không bao giờ dùng. Còn với implementation nào được dùng, như PreCacheTask trong ví dụ, constructor mặc định (<init>()) mà library cần gọi được giữ lại.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: rule nhắm vào mọi class implement interfaceStartupTask.-keep class * implements com.example.library.StartupTask: giữ mọi class (*) implement interface.,allowobfuscation: dù giữ class, R8 vẫn được đổi tên (obfuscate) nó. An toàn vì library không dựa vào tên class mà nhận thẳng đối tượngClass.,allowshrinking: R8 được xoá class nếu không dùng. Nhờ vậy R8 xoá an toàn được implementationStartupTasknào không bao giờ được truyền vàoTaskRunner.execute(). Tóm lại: nếu app dùng một class implementStartupTask, R8 giữ class đó; R8 được đổi tên để giảm kích thước và được xoá nếu app không dùng.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: rule nhắm vào member cụ thể của các class mà rule đầu xác định, ở đây là constructor.-keepclassmembers class * implements com.example.library.StartupTask: giữ các member cụ thể (method, field) của class implementStartupTask, nhưng chỉ khi bản thân class được giữ.{ <init>(); }: phần chọn member.<init>là tên nội bộ đặc biệt của constructor trong bytecode Java. Phần này nhắm đúng constructor mặc định, không tham số.- Rule này quan trọng vì code gọi
getDeclaredConstructor().newInstance()không tham số, tức gọi constructor mặc định qua reflection. Không có rule, R8 thấy không có code nào gọi trực tiếpnew PreCacheTask(), cho rằng constructor không dùng và xoá nó. App sẽ crash lúc chạy vớiInstantiationException.
Reflection dựa trên annotation của method
Phần tiêu đề “Reflection dựa trên annotation của method”Library hay định nghĩa annotation để developer gắn lên method hoặc field, rồi dùng reflection tìm các member có annotation đó lúc chạy. Ví dụ annotation @OnLifecycleEvent được dùng để tìm method cần gọi lúc chạy.
Ví dụ sau là một event bus tìm và gọi các method có annotation @OnEvent.
Code của library:
@Retention(AnnotationRetention.RUNTIME)@Target(AnnotationTarget.FUNCTION)annotation class OnEvent
class EventBus { fun dispatch(listener: Any) { // Tìm mọi method có @OnEvent và gọi chúng listener::class.java.declaredMethods.forEach { method -> if (method.isAnnotationPresent(OnEvent::class.java)) { try { method.invoke(listener) } catch (e: Exception) { /* ... */ } } } }}App dùng library có code:
class MyEventListener { @OnEvent fun onSomethingHappened() { // R8 sẽ xoá method này nếu không có keep rule Log.d(TAG, "Event received!") }}
fun onCreate() { // Tạo listener và event bus val listener = MyEventListener() val eventBus = EventBus()
// Đưa listener cho event bus eventBus.dispatch(listener)}Library nên có file consumer keep rule tự giữ mọi method dùng annotation của nó:
-keepattributes RuntimeVisibleAnnotations-keep @interface com.example.library.OnEvent-keepclassmembers class * { @com.example.library.OnEvent <methods>;}-keepattributes RuntimeVisibleAnnotations: giữ các annotation được đọc lúc chạy.-keep @interface com.example.library.OnEvent: giữ chính annotation classOnEvent.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}: giữ class và các member cụ thể, chỉ khi class được dùng và có các member đó.-keepclassmembers: giữ class và member cụ thể chỉ khi class được dùng và có các member đó.class *: rule áp dụng cho mọi class.@com.example.library.OnEvent <methods>;: giữ mọi class có một hoặc nhiều method (<methods>) gắn@com.example.library.OnEvent, đồng thời giữ chính các method đó.
Reflection dựa trên annotation của class
Phần tiêu đề “Reflection dựa trên annotation của class”Library có thể dùng reflection quét các class có một annotation cụ thể. Ở ví dụ này, task runner dùng reflection tìm mọi class có annotation ReflectiveExecutor và gọi method execute.
Code của library:
@Retention(AnnotationRetention.RUNTIME)@Target(AnnotationTarget.CLASS)annotation class ReflectiveExecutor
class TaskRunner { fun process(task: Any) { val taskClass = task::class.java if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) { val methodToCall = taskClass.getMethod("execute") methodToCall.invoke(task) } }}App dùng library có code:
// Trong app sử dụng library
@ReflectiveExecutorclass ImportantBackgroundTask { fun execute() { // R8 sẽ xoá class này nếu không có keep rule Log.e("ImportantBackgroundTask", "Executing the important background task...") }}
// Cách dùng ImportantBackgroundTask
fun onCreate(){ val task = ImportantBackgroundTask() val runner = TaskRunner() runner.process(task)}Vì library dùng reflection để lấy các class cụ thể, nó nên có file consumer keep rule với các rule:
# Giữ metadata annotation cho reflection lúc chạy.-keepattributes RuntimeVisibleAnnotations
# Giữ chính annotation interface.-keep @interface com.example.library.ReflectiveExecutor
# Giữ method execute trong các class đang được dùng-keepclassmembers @com.example.library.ReflectiveExecutor class * { public void execute();}Cấu hình này rất hiệu quả vì nó cho R8 biết chính xác cần giữ gì.
Reflection để hỗ trợ optional dependency
Phần tiêu đề “Reflection để hỗ trợ optional dependency”Một cách dùng reflection phổ biến là tạo phụ thuộc “mềm” giữa library lõi và một library add-on tuỳ chọn. Library lõi kiểm tra add-on có trong app không, nếu có thì bật thêm tính năng. Nhờ vậy bạn phát hành được các module add-on mà library lõi không cần phụ thuộc trực tiếp vào chúng.
Library lõi dùng reflection (Class.forName) tìm một class theo tên. Nếu tìm thấy, tính năng được bật; nếu không, nó bỏ qua êm ả.
Ví dụ AnalyticsManager lõi kiểm tra class tuỳ chọn VideoEventTracker để bật analytics cho video.
Code của library lõi:
object AnalyticsManager { private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
fun initialize() { try { // Thử nạp class của module tuỳ chọn bằng reflection Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance() Log.d(TAG, "Video tracking enabled.") } catch (e: ClassNotFoundException) { Log.d(TAG,"Video tracking module not found. Skipping.") } catch (e: Exception) { Log.e(TAG, e.printStackTrace()) } }}Code của library video tuỳ chọn:
package com.example.analytics.video
class VideoEventTracker { // Constructor này phải được giữ để lời gọi reflection thành công. init { /* ... */ }}Tác giả library tuỳ chọn chịu trách nhiệm cung cấp consumer keep rule cần thiết, để mọi app dùng library tuỳ chọn đều giữ lại code mà library lõi cần tìm.
# Trong file consumer keep rule của library video-keep class com.example.analytics.video.VideoEventTracker { <init>();}Không có rule này, R8 nhiều khả năng xoá VideoEventTracker khỏi library tuỳ chọn vì trong module đó không có gì dùng trực tiếp class này. Keep rule giữ class và constructor để library lõi tạo instance được.
Reflection truy cập private member
Phần tiêu đề “Reflection truy cập private member”Dùng reflection truy cập code private hoặc protected không thuộc API public của library có thể gây nhiều vấn đề. Code đó có thể đổi bất cứ lúc nào mà không báo trước, dẫn tới hành vi lạ hoặc crash.
Khi dựa vào reflection cho API không public, bạn có thể gặp:
- Không cập nhật được: thay đổi trong code private hoặc protected có thể khiến bạn không lên được phiên bản library mới hơn.
- Lỡ mất lợi ích: bạn có thể bỏ lỡ tính năng mới, bản sửa crash quan trọng hay cập nhật bảo mật.
Tối ưu của R8 và reflection
Phần tiêu đề “Tối ưu của R8 và reflection”Nếu buộc phải reflect vào code private hoặc protected của library, hãy để ý kỹ tới tối ưu của R8. Nếu không có tham chiếu trực tiếp nào tới các member đó, R8 có thể coi chúng là không dùng và xoá hoặc đổi tên. App có thể crash lúc chạy, thường với thông báo lỗi gây hiểu nhầm như NoSuchMethodException hay NoSuchFieldException.
Ví dụ truy cập một field private của class trong library.
Library bạn không sở hữu có code:
class LibraryClass { private val secretMessage = "R8 will remove me"}App của bạn có code:
fun accessSecretMessage(instance: LibraryClass) { // Dùng Java reflection từ Kotlin để truy cập field private val secretField = instance::class.java.getDeclaredField("secretMessage") secretField.isAccessible = true // Dòng này sẽ crash lúc chạy khi bật R8 val message = secretField.get(instance) as String}Thêm rule -keep vào app để R8 không xoá field private:
-keepclassmembers class com.example.LibraryClass { private java.lang.String secretMessage;}-keepclassmembers: giữ member cụ thể của class chỉ khi bản thân class được giữ.class com.example.LibraryClass: nhắm đúng class chứa field.private java.lang.String secretMessage;: xác định đúng field private theo tên và kiểu.
Java Native Interface (JNI)
Phần tiêu đề “Java Native Interface (JNI)”Tối ưu của R8 có thể gây lỗi với upcall từ native (C/C++) lên Java hoặc Kotlin. Chiều ngược lại, downcall từ Java/Kotlin xuống native, cũng có thể lỗi, nhưng file mặc định proguard-android-optimize.txt đã có rule sau để downcall chạy đúng. Rule này ngăn native method bị cắt bỏ.
-keepclasseswithmembernames,includedescriptorclasses class * { native <methods>;}Tương tác với native code qua JNI
Phần tiêu đề “Tương tác với native code qua JNI”Khi app dùng JNI để upcall từ native (C/C++) lên Java hoặc Kotlin, R8 không thấy được method nào được gọi từ native. Nếu trong app không có tham chiếu trực tiếp nào tới các method đó, R8 cho rằng chúng không dùng và xoá đi, khiến app crash.
Ví dụ sau là một class Kotlin có method để thư viện native gọi. Thư viện native tạo instance của một kiểu trong app và truyền dữ liệu từ native lên Kotlin.
package com.example.models
// Class này được dùng trong signature của method cầu nối JNIdata class NativeData(val id: Int, val payload: String)package com.example.app// Trong package com.example.appclass JniBridge { /** * Method này được gọi từ phía native. * R8 sẽ xoá nó nếu không được giữ. */ fun onNativeEvent(data: NativeData) { Log.d(TAG, "Received event from native code: $data") } // Dùng 'external' để khai báo native method external fun startNativeProcess()
companion object { init { // Nạp thư viện native System.loadLibrary("my-native-lib") } }}Trong trường hợp này, bạn phải bảo R8 không tối ưu kiểu đó của app. Ngoài ra, nếu method được gọi từ native dùng class của bạn trong signature (làm kiểu tham số hoặc kiểu trả về), bạn cũng phải đảm bảo các class đó không bị đổi tên.
Thêm keep rule sau vào app:
-keepclassmembers,includedescriptorclasses class com.example.app.JniBridge { public void onNativeEvent(com.example.models.NativeData);}
-keep class com.example.models.NativeData { <init>(int, java.lang.String);}Các rule này ngăn R8 xoá hoặc đổi tên method onNativeEvent và, quan trọng không kém, kiểu tham số của nó.
-keepclassmembers,includedescriptorclasses class com.example.app.JniBridge { public void onNativeEvent(com.example.models.NativeData); }: giữ member cụ thể của class chỉ khi class được tạo instance trong code Kotlin hoặc Java trước, tức là báo cho R8 biết app đang dùng class và cần giữ các member cụ thể của nó.-keepclassmembers: giữ member cụ thể của class chỉ khi class được tạo instance trong code Kotlin hoặc Java trước.class com.example.app.JniBridge: nhắm đúng class chứa method.includedescriptorclasses: modifier này giữ luôn mọi class xuất hiện trong signature (descriptor) của method. Ở đây nó ngăn R8 đổi tên hoặc xoá classcom.example.models.NativeDatadùng làm tham số. NếuNativeDatabị đổi tên (ví dụ thànha.a), signature của method sẽ không còn khớp với thứ native code mong đợi, và app crash.public void onNativeEvent(com.example.models.NativeData);: chỉ ra chính xác signature Java của method cần giữ.
-keep class com.example.models.NativeData { <init>(int, java.lang.String); }:includedescriptorclasseschỉ đảm bảo bản thân classNativeDatađược giữ; mọi member (field, method) trongNativeDatamà native JNI truy cập trực tiếp cần keep rule riêng.-keep class com.example.models.NativeData: nhắm vào classNativeData, khối phía sau chỉ ra member nào bên trong cần giữ.<init>(int, java.lang.String): signature của constructor, xác định duy nhất constructor nhận hai tham số: tham số đầu làIntcủa Kotlin (biên dịch thànhint), tham số thứ hai làString.
Bạn cũng có thể gắn annotation @UnconditionallyKeep của Jetpack Keep cho các method được native gọi, thay vì viết keep rule. Annotation này không giữ các class trong signature của method (như NativeData), nên bạn vẫn phải giữ các class đó riêng.
Lời gọi gián tiếp từ nền tảng
Phần tiêu đề “Lời gọi gián tiếp từ nền tảng”Truyền dữ liệu bằng Parcelable
Phần tiêu đề “Truyền dữ liệu bằng Parcelable”Android framework dùng reflection để tạo instance các đối tượng Parcelable của bạn. Trong Kotlin hiện đại, nên dùng plugin kotlin-parcelize, plugin này tự sinh phần cài đặt Parcelable cần thiết, gồm field CREATOR và các method mà framework cần.
Ví dụ dùng plugin kotlin-parcelize để tạo class Parcelable:
import android.os.Parcelableimport kotlinx.parcelize.Parcelize
// Thêm annotation @Parcelize vào data class@Parcelizedata class UserData( val name: String, val age: Int) : ParcelableTrường hợp này không có keep rule được khuyến nghị. Gradle plugin kotlin-parcelize tự sinh keep rule cần thiết cho các class có @Parcelize, đảm bảo CREATOR và constructor được sinh ra đều được giữ cho các lời gọi reflection của framework.
Nếu bạn tự viết class Parcelable trong Kotlin mà không dùng @Parcelize, bạn phải tự giữ field CREATOR và constructor nhận Parcel. Quên làm vậy sẽ khiến app crash khi hệ thống deserialize đối tượng. Dùng @Parcelize là cách chuẩn và an toàn hơn.
Khi dùng plugin kotlin-parcelize, lưu ý:
- Plugin tự tạo field
CREATORkhi biên dịch. - File
proguard-android-optimize.txtcó sẵn các rulekeepcần thiết để giữ các field này. - Developer app phải kiểm tra đủ mọi rule
keepcần thiết, nhất là với các cài đặt tự viết hoặc dependency bên thứ ba.
Library phổ biến
Phần tiêu đề “Library phổ biến”Library dùng reflection hoặc biến đổi bytecode sẽ truy cập code động lúc chạy. Nếu R8 xoá hoặc đổi tên class, field, method bị truy cập theo cách đó, app có thể crash.
Tuy vậy, các library bên thứ ba phổ biến (như Gson, Retrofit và Kotlinx Serialization) tự đóng gói consumer keep rule cho R8. Khi dùng phiên bản mới của các library này, bạn không cần thêm keep rule thủ công vào dự án.
Gson là library serialize và deserialize JSON dựa rất nhiều vào reflection. Khi dùng full mode, R8 xoá generic type signature, constructor mặc định và các field không có annotation, trừ khi được chỉ định khác.
Để Gson chạy đúng, cần rule giữ các field không transient trong data model và giữ cây kế thừa TypeToken:
# Giữ thông tin kiểu generic cần cho deserialize-keepattributes Signature
# Giữ mọi field không transient trong data model cho reflection-keepclassmembers class com.example.models.** { !transient <fields>;}
# Giữ chính TypeToken và mọi anonymous class kế thừa nó-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeTokenField có modifier transient bị Gson bỏ qua khi serialize và deserialize, nên rule chỉ nhắm vào field không transient (!transient).
Retrofit
Phần tiêu đề “Retrofit”Retrofit là library networking, dùng reflection đọc các method của service interface có annotation HTTP (như @GET hay @POST) để dựng request và chuyển đổi response.
Retrofit sinh implementation của API interface động lúc chạy bằng Proxy.newProxyInstance(). Vì R8 không thấy class nào implement các interface đó một cách tĩnh, nó có thể xoá method hoặc kiểu trả về generic của chúng.
Keep rule đóng gói sẵn
Phần tiêu đề “Keep rule đóng gói sẵn”Retrofit dựa vào reflection lúc chạy để đọc tham số generic, annotation của method và annotation của tham số. Nếu không cấu hình đúng, full mode có thể xoá sạch generic signature của kiểu trả về, của Kotlin continuation và response class, thậm chí thay giá trị interface bằng null vì interface Retrofit được tạo động bằng proxy.
Từ Retrofit 2.10.0, library tự đóng gói keep rule chính thức để giữ annotation default, tham số của service method và metadata cần thiết của class. Xem Rules used by Retrofit.
Giữ kiểu trả về generic
Phần tiêu đề “Giữ kiểu trả về generic”Retrofit đọc generic signature của kiểu trả về (ví dụ Observable<Data>) để deserialize response cho đúng. Nếu R8 xoá generic signature, Retrofit sẽ thay đối tượng được tạo bằng null.
Để full mode không xoá generic signature của kiểu trả về, dùng conditional rule:
# Giữ thông tin kiểu generic cho kiểu trả về Call/Observable-keepattributes Signature
# Nếu interface có annotation HTTP của Retrofit, giữ kiểu trả về của nó (class <3>)-if interface * { @retrofit2.http.* public *** *(...);}-keep,allowoptimization,allowshrinking,allowobfuscation class <3>Data model thực sự được trả về (ví dụ Data trong Observable<Data>) cũng phải được giữ, vì nó được converter (như Gson) tạo bằng reflection.
Coroutine
Phần tiêu đề “Coroutine”Khi dùng Kotlin coroutine, compiler Kotlin biến đổi hàm suspend bằng cách thêm tham số Continuation vào cuối signature đã biên dịch.
Khi library như Retrofit đọc generic signature của hàm suspend bằng reflection, chúng dựa vào tham số Continuation này. Ở full mode, attribute Signature chỉ được giữ cho class được keep tường minh. Vì Continuation là tham số synthetic, R8 mặc định xoá signature của nó và reflection bị hỏng.
Để signature không bị xoá và code chạy đúng ở full mode, thêm rule:
# Giữ attribute Signature toàn cục-keepattributes Signature
# Giữ tường minh class Continuation để signature của nó không bị xoá-keep class kotlin.coroutines.ContinuationSinh keep rule từ annotation (Jetpack Keep)
Phần tiêu đề “Sinh keep rule từ annotation (Jetpack Keep)”Jetpack Keep cho phép khai báo việc dùng reflection và JNI bằng annotation ngay trong mã nguồn Kotlin hoặc Java, thay vì trong file keep rule riêng. Gradle plugin của Jetpack Keep đọc các annotation này lúc build và sinh keep rule mà R8 cần.
Jetpack Keep giúp tối ưu của R8 luôn hiệu quả khi codebase thay đổi:
- Keep rule nằm cạnh code cần nó. Khi đổi tên hay xoá code dùng reflection, bạn sửa annotation ngay tại chỗ. Không có file rule riêng phải giữ đồng bộ.
- Rule sinh ra có điều kiện. R8 chỉ giữ đích của lời gọi reflection nếu nó cũng giữ đoạn code thực hiện lời gọi. Nếu code gọi không được dùng và bị R8 xoá (thường gặp với tính năng tuỳ chọn của library), R8 cũng xoá được đích, nên rule của tính năng không dùng không làm app phình ra.
- Lint tìm reflection giúp bạn. Library có lint check báo các lời gọi reflection chưa có annotation, và có thể tự thêm nhiều annotation.
Cách Jetpack Keep hoạt động
Phần tiêu đề “Cách Jetpack Keep hoạt động”Xét một hàm tạo instance của class theo tên. Nếu R8 không biết code của bạn dùng class đó, nó xoá constructor và app crash lúc chạy. Để tránh, bạn có thể thêm rule sau vào file keep rule:
# Vô điều kiện: R8 giữ constructor này kể cả khi đã xoá đoạn reflection gọi nó.-keep class com.example.ResourceLocatorImpl { <init>();}Với Jetpack Keep, bạn gắn annotation lên chính hàm thực hiện reflection:
// ResourceLocatorImpl không có trên compile classpath của module này, nên cả// annotation lẫn code đều nhắc tới nó theo tên.@UsesReflectionToConstruct( className = "com.example.ResourceLocatorImpl", // Bỏ trống parameterTypes thì giữ mọi constructor. Mảng rỗng chỉ giữ // constructor không tham số, là constructor mà code gọi. parameterTypes = [],)fun loadResourceLocator(): ResourceLocator { val implClass = Class.forName("com.example.ResourceLocatorImpl") return implClass.getDeclaredConstructor().newInstance() as ResourceLocator}Lúc build, Gradle plugin sinh keep rule cho constructor. Khác với rule viết tay ở trên, rule sinh ra có điều kiện: R8 chỉ giữ constructor nếu loadResourceLocator còn trong app đã tối ưu.
Yêu cầu
Phần tiêu đề “Yêu cầu”Cần Android Gradle Plugin (AGP) 8.7 trở lên. Gradle plugin hỗ trợ các loại module:
| Loại module | Plugin mà module áp dụng | Cấu hình thêm |
|---|---|---|
| App Android | com.android.application | Không |
| Library Android | com.android.library | Không |
| Library Kotlin Multiplatform có target Android | com.android.kotlin.multiplatform.library | Không |
| Library JVM (Java hoặc Kotlin) | java-library hoặc org.jetbrains.kotlin.jvm | Đăng ký JAR transform |
Cài đặt Jetpack Keep
Phần tiêu đề “Cài đặt Jetpack Keep”Thêm cả library lẫn Gradle plugin vào mỗi module dùng reflection hoặc JNI. Library cung cấp annotation và lint check, plugin biến annotation thành keep rule.
-
Trong
libs.versions.toml, thêm library và plugin. Hai thứ dùng chung phiên bản:[versions]# Library và plugin phát hành cùng nhau. Dùng một version cho cả hai để# chúng luôn đồng bộ khi nâng cấp.annotationKeep = "1.0.0-alpha01"[libraries]androidx-annotation-keep = { group = "androidx.annotation.keep", name = "keep", version.ref = "annotationKeep" }[plugins]androidx-annotation-keep = { id = "androidx.annotation.keep", version.ref = "annotationKeep" }Gradle tải plugin từ Google Maven, nên kiểm tra phần repository của
pluginManagementtrongsettings.gradle.ktscógoogle(). -
Trong
build.gradle.ktsgốc, khai báo plugin cạnh AGP nhưng chưa áp dụng:plugins {alias(libs.plugins.android.application) apply false// Các plugin khác.// Plugin dựa vào các class của AGP mà nó không đóng gói kèm, nên khai// báo cạnh AGP. Khai báo ở đây cũng giúp mọi module dùng cùng version.alias(libs.plugins.androidx.annotation.keep) apply false} -
Trong
build.gradle.ktscủa mỗi module dùng reflection hoặc JNI, áp dụng plugin và thêm dependency:plugins {alias(libs.plugins.android.application)// Plugin chỉ đọc annotation trong code của chính module này, nên// áp dụng ở mọi module dùng Jetpack Keep.alias(libs.plugins.androidx.annotation.keep)}dependencies {implementation(libs.androidx.annotation.keep)} -
Sync project với Gradle file.
Với mỗi build variant của app module, plugin đăng ký task <variant>ExtractKeepRules (ví dụ releaseExtractKeepRules) và đưa rule sinh ra cho R8 của variant đó. Với library module, plugin đóng gói rule sinh ra thành consumer keep rule trong output của library. Library module JVM cần cấu hình thêm.
Tìm reflection bằng lint
Phần tiêu đề “Tìm reflection bằng lint”Jetpack Keep có các lint check:
ReflectionWithoutKeepAnnotations: báo các lời gọi reflection nhưClass.forName,getDeclaredConstructor,getDeclaredMethodvàgetDeclaredFieldchưa có keep annotation tương ứng.MissingKeepAnnotationPlugin: báo các module phụ thuộcandroidx.annotation.keep:keepnhưng không áp dụng Gradle pluginandroidx.annotation.keep. Thiếu plugin thì build không sinh keep rule từ annotation.
Các bước dùng lint để gắn annotation cho một module Android:
-
Chạy lint trên module để tìm các lời gọi reflection cần annotation:
Terminal window ./gradlew :app:lint -
Áp dụng các sửa lỗi lint tự làm được:
Terminal window ./gradlew :app:lintFixLint tự thêm annotation được khi nó xác định được class và member mà code reflect vào.
-
Xem lại từng annotation được thêm để chắc nó khớp với việc code làm. Ví dụ kiểm tra kiểu tham số khớp với constructor hoặc method mà code gọi.
-
Xử lý các phát hiện còn lại trong Android Studio. Khi lint không xác định được class đích, quick fix chèn annotation với chỗ trống
TODOđể bạn điền. Bạn cũng có thể tự thêm annotation.Ví dụ hàm sau nhận tên class qua tham số nên lint không biết nó tạo class nào. Quick fix thêm annotation với chỗ trống:
@UsesReflectionToConstruct(className = TODO(),parameterTypes = [])fun createInstance(className: String): Any =Class.forName(className).getDeclaredConstructor().newInstance()Thay chỗ trống bằng class mà code tạo. Nếu class có ở compile time, dùng
classConstantthay choclassName. Trong Kotlin, code sẽ không biên dịch được cho tới khi bạn thayTODO(). -
Kiểm tra keep rule mà plugin sinh từ annotation trước khi xoá rule cũ. Với app module, chạy task trích xuất cho variant release:
Terminal window ./gradlew :app:releaseExtractKeepRulesPlugin ghi rule vào
app/build/generated/release/androidx.annotation.keep.rules.pro. Với library module, xem Kiểm tra rule được sinh ra. -
Xoá các keep rule mà annotation mới thay thế khỏi file keep rule. Một rule có thể còn bao cả reflection mà lint không báo, như reflection ở module khác hoặc trong library, nên kiểm tra từng rule trước khi xoá:
- Với rule cấp package, như
-keep class com.example.** { *; }, chắc chắn annotation hoặc rule khác đã bao hết reflection trên các class của package. - Với rule giữ class hoặc member cụ thể, chắc chắn nó khớp sát với một rule được sinh ra, hoặc không còn reflection nào khác dựa vào nó.
- Với rule cấp package, như
-
Kiểm thử bản release.
Gắn annotation vào code
Phần tiêu đề “Gắn annotation vào code”Gắn annotation Jetpack Keep lên đoạn code thực hiện reflection: hàm, constructor, field hoặc class chứa lời gọi reflection. Annotation mô tả đích của reflection, và rule sinh ra chỉ giữ đích đó nếu R8 giữ đoạn code có annotation.
Chọn annotation theo việc code làm:
| Annotation | Dùng khi code của bạn |
|---|---|
@UsesReflectionToConstruct | Gọi constructor qua reflection, ví dụ getDeclaredConstructor().newInstance(). |
@UsesReflectionToAccessMethod | Gọi method qua reflection, ví dụ getMethod hoặc getDeclaredMethod. |
@UsesReflectionToAccessField | Đọc hoặc ghi field qua reflection, ví dụ getField hoặc getDeclaredField. |
@UnconditionallyKeep | Được gọi từ bên ngoài code Kotlin/Java của app, ví dụ từ native code qua JNI. Chỉ dùng khi không dùng được các annotation @UsesReflection* (các annotation này sinh rule có điều kiện). |
Mọi annotation @UsesReflection* theo cùng quy ước:
- Xác định class đích bằng
classConstanthoặcclassName. Nếu class có ở compile time, dùngclassConstant: compiler kiểm tra tham chiếu, và các refactor của IDE như đổi tên sẽ cập nhật nó. Chỉ dùngclassNamekhi class không có ở compile time, như class trong optional dependency. Hai tham số này loại trừ nhau. - Nhắm vào class cụ thể nhất. Rule sinh ra giữ các member khớp trong class bạn chỉ ra và trong các subclass hoặc class implement interface đó. Class càng chung chung, R8 càng giữ nhiều code.
- Chỉ ra kiểu tham số để chỉ giữ một overload. Nếu không đặt
parameterTypeshoặcparameterTypeNames, rule giữ mọi constructor hoặc mọi overload của method. Muốn chỉ giữ constructor hoặc method không tham số, truyền mảng rỗng. - Lặp annotation cho nhiều đích. Nếu một hàm reflect vào nhiều class hoặc member, thêm một annotation cho mỗi đích.
Tạo đối tượng bằng reflection
Phần tiêu đề “Tạo đối tượng bằng reflection”Dùng @UsesReflectionToConstruct khi code tạo instance của class qua reflection. Ở ví dụ sau, task runner tạo instance của các implementation StartupTask theo tên. Vì annotation chỉ ra interface, rule sinh ra giữ constructor không tham số của mọi implementation StartupTask, miễn là R8 giữ execute:
interface StartupTask { fun run()}
object TaskRunner { // Class task chỉ biết lúc chạy, nên nhắm vào interface mà mọi task // đều implement. @UsesReflectionToConstruct(classConstant = StartupTask::class, parameterTypes = []) fun execute(className: String) { val task = Class.forName(className) .getDeclaredConstructor() .newInstance() as StartupTask task.run() }}Gọi method bằng reflection
Phần tiêu đề “Gọi method bằng reflection”Dùng @UsesReflectionToAccessMethod khi code gọi method qua reflection. Đặt methodName là tên method. Ở ví dụ sau, code gọi một method static trong optional dependency. Class không có ở compile time nên annotation dùng className thay cho classConstant:
object CrashReporting { @UsesReflectionToAccessMethod( className = "com.example.crashlib.CrashReporter", methodName = "log", // Bỏ trống parameterTypes thì giữ mọi overload của log. Chỉ giữ // overload mà code gọi. parameterTypes = [String::class], ) fun log(message: String) { val reporterClass = try { Class.forName("com.example.crashlib.CrashReporter") } catch (e: ClassNotFoundException) { // Crash reporting là tuỳ chọn, nên bỏ qua thay vì báo lỗi. return } reporterClass.getMethod("log", String::class.java).invoke(null, message) }}Truy cập field bằng reflection
Phần tiêu đề “Truy cập field bằng reflection”Dùng @UsesReflectionToAccessField khi code đọc hoặc ghi field qua reflection. Đặt fieldName là tên field:
class UserProfileFetcher { // User không public internalId, nên phải đọc bằng reflection. @UsesReflectionToAccessField(classConstant = User::class, fieldName = "internalId") fun readInternalId(user: User): Any? { val field = User::class.java.getDeclaredField("internalId") field.isAccessible = true return field.get(user) }}Giữ code được gọi từ bên ngoài app
Phần tiêu đề “Giữ code được gọi từ bên ngoài app”Dùng @UnconditionallyKeep cho code được gọi từ bên ngoài code Kotlin/Java của app, như method mà native code gọi qua JNI. Gắn annotation lên phần tử được gọi, không phải code gọi nó:
class NativeBridge { // Chỉ native code gọi method này, qua một lần tra JNI theo tên mà R8 // không thấy được. Giữ method và tên của nó vô điều kiện. @UnconditionallyKeep fun onNativeEvent(message: String) { Log.d("NativeBridge", "Received from native code: $message") }
// File proguard-android-optimize.txt mặc định đã giữ tên native method, // nên method này không cần annotation. external fun startNativeProcess()}Rule sinh ra giữ method có annotation và class chứa nó, với tên gốc. Rule không giữ các class khác mà signature của method nhắc tới. Nếu native code phụ thuộc vào các class đó, hãy gắn annotation cho chúng nữa, hoặc thêm keep rule. Xem thêm Tương tác với native code qua JNI.
Mặc định @UnconditionallyKeep giữ tên phần tử. Nếu bên gọi từ ngoài không phụ thuộc vào tên, đặt shouldPreserveName = false để R8 được đổi tên.
Kiểm tra rule được sinh ra
Phần tiêu đề “Kiểm tra rule được sinh ra”Với app module, plugin đưa rule thẳng cho R8. Với library module, plugin đóng gói rule thành consumer keep rule trong output của library. App phụ thuộc library sẽ tự áp dụng các rule này khi chạy R8, kể cả khi app không dùng Jetpack Keep. Xem thêm về consumer keep rule ở bài R8: Chọn và viết thư viện.
App Android. Chạy task trích xuất cho một build variant:
./gradlew :app:releaseExtractKeepRulesPlugin ghi rule của mỗi variant vào app/build/generated/<variant>/androidx.annotation.keep.rules.pro. Mỗi rule có comment # context: phía trước cho biết đoạn code có annotation nào sinh ra nó.
Để xem rule sinh ra ảnh hưởng tới tối ưu thế nào khi kết hợp với các keep rule khác, dùng R8 Configuration Analyzer.
Library Android. Không cần cấu hình thêm. Khi build AAR, plugin thêm rule sinh ra vào file proguard.txt của AAR, sau các consumer keep rule mà AAR đã có. Plugin đăng ký task <variant>KeepRulesTransformAar cho mỗi build variant để làm việc này.
Để kiểm tra, build AAR (ví dụ ./gradlew :mylibrary:assembleRelease) rồi mở proguard.txt trong AAR được sinh ra. Rule sinh ra nằm trong một phần có tiêu đề:
################################################################## Rules generated from androidx.annotation.keep annotations ##################################################################Trên macOS và Linux, bạn cũng có thể in rule ra bằng dòng lệnh:
unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txtLibrary Kotlin Multiplatform. Library KMP có target Android cũng không cần cấu hình thêm. Khi build AAR cho target Android, plugin thêm rule vào proguard.txt của AAR giống như library Android. Plugin đăng ký task androidMainKeepRulesTransformAar để làm việc này. Để kiểm tra, chạy task đó rồi mở proguard.txt trong AAR nó tạo ra. Trên macOS và Linux:
./gradlew :mylibrary:androidMainKeepRulesTransformAarunzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txtLibrary JVM. Trước khi kiểm tra được, bạn phải cấu hình module để plugin thêm rule vào một bản sao của JAR. Sau đó chạy task transform đã đăng ký và kiểm tra file rule trong JAR nó tạo ra. Ví dụ nếu đặt tên task là keepRulesJar, trên macOS và Linux:
./gradlew :mylibrary:keepRulesJarunzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \ "META-INF/com.android.tools/*" "META-INF/proguard/*"Cấu hình library JVM
Phần tiêu đề “Cấu hình library JVM”Với library module JVM tạo ra JAR, plugin không tự sửa JAR. Thay vào đó, dùng hàm annotationKeep.registerJavaArchiveTransform để đăng ký một task tạo bản sao JAR có kèm rule sinh ra, rồi phát hành JAR đó. Thêm vào build.gradle.kts của library module:
plugins { id("java-library") alias(libs.plugins.jetbrains.kotlin.jvm) alias(libs.plugins.androidx.annotation.keep)}
val jarTask = tasks.named<Jar>("jar")
// App chạy R8 trên code của library này, nên rule sinh ra phải nằm trong// JAR. Plugin chỉ tự làm việc này cho module Android.val keepRulesJarTask = annotationKeep.registerJavaArchiveTransform( taskName = "keepRulesJar", input = jarTask.flatMap { it.archiveFile }, fileName = jarTask.flatMap { it.archiveFileName }.map { it.replace(".jar", "-keep-rules.jar") },)
// Bên dùng library này resolve apiElements và runtimeElements, nên thêm// JAR có rule sinh ra vào hai configuration đó.configurations.configureEach { if (name == "apiElements" || name == "runtimeElements") { outgoing.artifact(keepRulesJarTask.flatMap { it.outputJar }) }}
dependencies { implementation(libs.androidx.annotation.keep)}Transform thêm rule sinh ra vào vị trí rule dành riêng cho R8 trong JAR, META-INF/com.android.tools/r8/proguard.txt. Nếu JAR đã có rule trong META-INF/com.android.tools/ hoặc META-INF/proguard/, transform nối rule sinh ra vào file rule có sẵn. Xem thêm về các vị trí này ở mục Hỗ trợ nhiều phiên bản R8.
Sau khi đăng ký transform, kiểm tra rule trong JAR.
Ví dụ: nạp library add-on tuỳ chọn
Phần tiêu đề “Ví dụ: nạp library add-on tuỳ chọn”Library hay dùng reflection để nạp add-on tuỳ chọn. Ví dụ một SDK analytics có library lõi và add-on video. Add-on phụ thuộc library lõi, nên library lõi không nhắc trực tiếp tới class của add-on được. Thay vào đó, library lõi tìm class của add-on theo tên lúc chạy và chỉ bật analytics video nếu tìm thấy.
Vì library lõi chỉ dùng class của add-on qua reflection, R8 có thể xoá class hoặc constructor của nó khỏi bản release của app, khiến việc tìm class thất bại. Lỗi chỉ xảy ra ở bản release của những app có add-on, nên khó bắt được khi kiểm thử.
Không có Jetpack Keep, add-on phải đóng gói consumer keep rule cho class của nó, như trong mục Reflection để hỗ trợ optional dependency:
# Trong file consumer keep rule của add-on.-keep class com.example.analytics.video.VideoEventTracker { <init>();}Rule này nằm ở library khác với đoạn code tìm class cần nó, nên hai thứ dễ lệch nhau. Với Jetpack Keep, bạn gắn annotation lên đoạn tìm class trong library lõi:
// Trong library lõi.interface AnalyticsPlugin { fun start()}
object AnalyticsManager { // Annotation và đoạn tìm class dùng chung hằng này để không thể // trỏ tới hai class khác nhau. private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
@UsesReflectionToConstruct( className = VIDEO_TRACKER_CLASS, parameterTypes = [], ) fun initialize() { val tracker = try { Class.forName(VIDEO_TRACKER_CLASS) .getDeclaredConstructor() .newInstance() as AnalyticsPlugin } catch (e: ClassNotFoundException) { // App không phát video thì không có add-on, nên thiếu class // không phải lỗi. return } // Đây là lời gọi interface bình thường, không phải reflection, nên R8 // giữ start() mà không cần annotation. tracker.start() }}Annotation này theo các nguyên tắc:
- Gắn annotation lên code thực hiện reflection. Annotation nằm trên
initialize, hàm gọiClass.forName, không nằm trên class của add-on. - Chọn annotation theo việc code làm. Đoạn tìm class tạo instance của class, nên dùng
@UsesReflectionToConstruct. - Dùng
classNamecho class không nhắc tới trực tiếp được. Class của add-on không có trên compile classpath của library lõi, nên annotation không dùng đượcclassConstant. - Chỉ giữ constructor mà code gọi. Đoạn tìm class gọi constructor không tham số, nên
parameterTypeslà mảng rỗng.
Khi build AAR của library lõi, plugin thêm một rule giữ VideoEventTracker và constructor không tham số của nó mỗi khi R8 giữ initialize. App dùng SDK tự áp dụng rule này kể cả khi không dùng Jetpack Keep, nên add-on không cần rule riêng. So với rule của add-on, cách này có các ưu điểm:
- Rule nằm cạnh đoạn tìm class. Library lõi sở hữu cả đoạn tìm class lẫn rule, nên bạn sửa chúng cùng lúc thay vì phải phối hợp thay đổi giữa hai library.
- R8 vẫn xoá được code không dùng. Nếu app có add-on nhưng không bao giờ gọi
initialize, rule sinh ra không có hiệu lực và R8 xoá đượcVideoEventTracker. Còn rule của add-on thì giữ nó trong mọi app có add-on. - Lint bắt được đoạn tìm class chưa có annotation. Nếu sau này library lõi thêm một đoạn reflection khác mà quên annotation, lint sẽ báo để bạn sửa trước khi phát hành library.
Để xác nhận kết quả, kiểm tra rule sinh ra có trong proguard.txt của AAR như hướng dẫn ở mục Kiểm tra rule được sinh ra. Sau đó kiểm thử bản release của một app có add-on, vì đó là nơi thiếu rule sẽ làm hỏng analytics video.
Tài liệu thêm
Phần tiêu đề “Tài liệu thêm”- Jetpack Keep sample: project Android Studio dùng Jetpack Keep trong một app, một library Android, một library Kotlin Multiplatform và một library JVM.