Chọn và viết library tương thích R8
Library bạn dùng ảnh hưởng trực tiếp tới việc app được R8 tối ưu tốt tới đâu. Phần đầu bài dành cho developer app: chọn library thế nào. Phần sau dành cho tác giả library (kể cả library module nội bộ trong một app lớn chia nhiều module): viết library sao cho không cản trở tối ưu của app dùng nó.
Chọn library phù hợp
Phần tiêu đề “Chọn library phù hợp”Để bật tối ưu app, bạn phải dùng library tương thích với tối ưu của Android. Nếu một library không được cấu hình cho tối ưu Android, ví dụ dùng reflection mà không kèm keep rule tương ứng, có thể nó không hợp với app Android.
Ưu tiên codegen hơn reflection
Phần tiêu đề “Ưu tiên codegen hơn reflection”Chọn library dùng sinh code (codegen) thay vì reflection. Với codegen, công cụ tối ưu biết được code nào thực sự được dùng lúc chạy và code nào xoá được. Khó biết một library dùng codegen hay reflection, nhưng có vài dấu hiệu, xem mục dưới. Xem thêm so sánh codegen và reflection ở mục Tuân thủ yêu cầu tối ưu.
Kiểm tra library có dùng reflection không (nâng cao)
Phần tiêu đề “Kiểm tra library có dùng reflection không (nâng cao)”Bạn biết library có dùng reflection hay không bằng cách đọc code của nó. Nếu có, kiểm tra xem library có cung cấp keep rule tương ứng không. Library nhiều khả năng dùng reflection nếu nó:
-
Dùng class hoặc method trong package
kotlin.reflecthoặcjava.lang.reflect. -
Dùng hàm
Class.forNamehoặcclassLoader.getClass. -
Đọc annotation lúc chạy, ví dụ lưu giá trị annotation bằng
val value = myClass.getAnnotation()hoặcval value = myMethod.getAnnotation()rồi dùngvaluelàm gì đó. -
Gọi method bằng tên method dạng chuỗi, như ví dụ:
// Gọi API private `processData` bằng reflectionmyObject.javaClass.getMethod("processData", DataType::class.java)?.invoke(myObject, data)
Kiểm tra vấn đề về tối ưu
Phần tiêu đề “Kiểm tra vấn đề về tối ưu”Khi cân nhắc một library mới, hãy xem issue tracker và các thảo luận trên mạng xem có vấn đề liên quan tới minify hay cấu hình tối ưu không. Nếu có, nên tìm library thay thế. Lưu ý:
- Các library AndroidX và các library như Hilt chạy tốt với tối ưu vì chủ yếu dùng codegen thay vì reflection. Khi có dùng reflection, chúng cung cấp keep rule tối thiểu, chỉ giữ code cần thiết.
- Library serialization hay dùng reflection để khỏi phải viết code boilerplate khi tạo hoặc serialize đối tượng. Thay vì cách dựa trên reflection (như Gson cho JSON), hãy tìm library dùng codegen để tránh vấn đề, ví dụ Kotlin Serialization hoặc Moshi với codegen.
- Nếu được, tránh library có keep rule cấp package. Keep rule cấp package có thể giúp hết lỗi, nhưng rule rộng cuối cùng phải được thu hẹp lại để chỉ giữ code cần. Xem Bật tối ưu từng bước.
- Trước khi phát hành app dùng library bên thứ ba, dùng R8 Configuration Analyzer để rà keep rule mà library cung cấp. Báo cáo cho bạn biết keep rule của library có quá rộng, chặn R8 làm các tối ưu quan trọng trên codebase không. Bước này đảm bảo library bạn chọn khớp với mục tiêu hiệu năng của app và không làm cấu hình phình ra vô ích.
- Library không nên bắt bạn chép keep rule từ tài liệu vào một file trong project, nhất là keep rule cấp package. Lâu dài những rule này thành gánh nặng bảo trì cho developer app, và khó tối ưu hay thay đổi theo thời gian.
Bật tối ưu sau khi thêm library mới
Phần tiêu đề “Bật tối ưu sau khi thêm library mới”Mỗi khi thêm library mới, hãy bật tối ưu và kiểm tra có lỗi không. Nếu có, tìm library thay thế hoặc viết keep rule. Nếu library không tương thích với tối ưu, báo bug cho library đó.
Lọc bỏ keep rule xấu (nâng cao)
Phần tiêu đề “Lọc bỏ keep rule xấu (nâng cao)”Keep rule có tính cộng dồn. Nghĩa là một số rule mà library dependency đưa vào không thể gỡ bỏ, và có thể ảnh hưởng tới việc biên dịch các phần khác của app. Ví dụ nếu library có rule tắt tối ưu code, rule đó tắt tối ưu cho cả project.
Bạn nên tránh library có keep rule giữ lại code đáng lẽ phải bị xoá. Nhưng nếu buộc phải dùng, bạn có thể lọc bỏ rule của nó như sau:
// Nếu dùng AGP 8.4 trở lênbuildTypes { release { optimization.keepRules { it.ignoreFrom("com.somelibrary:somelibrary") } }}
// Nếu dùng AGP 7.3-8.3buildTypes { release { optimization.keepRules { it.ignoreExternalDependencies("com.somelibrary:somelibrary") } }}Vì sao Gson hỏng khi tối ưu
Phần tiêu đề “Vì sao Gson hỏng khi tối ưu”Gson là library serialization hay gây vấn đề với tối ưu vì dùng reflection rất nhiều. Đoạn code sau là cách dùng Gson điển hình, có thể gây crash lúc chạy. Để ý là khi dùng Gson lấy danh sách đối tượng User, bạn không hề gọi constructor hay truyền factory nào vào hàm fromJson(). Việc tạo hoặc dùng class do app định nghĩa mà không có một trong hai thứ sau là dấu hiệu library có thể đang dùng reflection không giới hạn (open-ended reflection):
- Class của app implement một interface hoặc class của library hoặc của chuẩn
- Plugin sinh code như KSP
class User(val name: String)class UserList(val users: List<User>)
// Code này chạy được ở bản debug, nhưng crash khi bật tối ưuGson().fromJson("""[{"name":"myname"}]""", User::class.java).toString()Để hiểu R8 xử lý Gson thế nào, xem consumer rule của Gson. Khi R8 phân tích code này và không thấy UserList hay User được tạo ở đâu, nó có thể đổi tên field, hoặc xoá các constructor có vẻ không được dùng, khiến app crash. Nếu bạn dùng library khác theo cách tương tự, hãy kiểm tra chúng không cản trở tối ưu; nếu có, tránh chúng.
Dùng @SerializedName với các bản Gson mới
Phần tiêu đề “Dùng @SerializedName với các bản Gson mới”Để data model tương thích với consumer rule của Gson, gắn @SerializedName cho các field:
import com.google.gson.annotations.SerializedName
class User(@SerializedName("name") val name: String)class UserList(@SerializedName("users") val users: List<User>)Dùng @SerializedName cho phép R8 khớp các model class với keep rule đóng gói trong Gson 2.11.0 trở lên. R8 tự giữ các field có annotation và constructor cần thiết, nên bạn dựa được vào rule của library mà không phải tự duy trì cấu hình ProGuard trong project.
Lưu ý Room, Hilt và Moshi với codegen cũng tạo đối tượng kiểu do app định nghĩa, nhưng dùng codegen để không cần reflection.
Dành cho tác giả library
Phần tiêu đề “Dành cho tác giả library”Là tác giả library, bạn phải đảm bảo developer app dễ dàng đưa library vào app mà vẫn giữ được trải nghiệm tốt cho người dùng cuối. Nghĩa là library phải tương thích với tối ưu Android (R8) mà developer không phải cấu hình thêm, hoặc phải ghi rõ library có thể không phù hợp để dùng trên Android. Library dùng cho Android tuyệt đối không được chặn các tối ưu quan trọng của app và phải tuân thủ các yêu cầu tối ưu.
Phần này viết cho developer của library được phát hành, nhưng cũng hữu ích cho developer của library module nội bộ trong một app lớn chia nhiều module.
Hai loại keep rule trong library
Phần tiêu đề “Hai loại keep rule trong library”Library có hai loại keep rule khác nhau:
- Consumer keep rule phải chỉ ra rule giữ mọi thứ mà library reflect vào. Nếu library dùng reflection hoặc JNI để gọi vào code của chính nó, hoặc code do app định nghĩa, các rule này phải mô tả code nào cần giữ. Library nên đóng gói consumer keep rule, dùng cùng định dạng với keep rule của app. Rule được đóng vào artifact của library (AAR hoặc JAR) và tự được áp dụng khi tối ưu app có dùng library. Rule này nằm trong file khai báo ở thuộc tính
consumerProguardFilestrongbuild.gradle.kts(hoặcbuild.gradle). Xem Đóng gói consumer keep rule. - Library build keep rule được áp dụng khi build chính library. Chỉ cần khi bạn quyết định tối ưu một phần library lúc build. Chúng phải giữ API public của library khỏi bị xoá, nếu không API public sẽ không có trong bản phát hành và developer app không dùng được library. Rule này nằm trong file khai báo ở thuộc tính
proguardFilestrongbuild.gradle.kts(hoặcbuild.gradle). Xem Tối ưu bản build AAR.
Yêu cầu và hướng dẫn về tối ưu
Phần tiêu đề “Yêu cầu và hướng dẫn về tối ưu”Cấu hình R8 trong library ảnh hưởng toàn cục tới kích thước và hiệu năng của app dùng nó. Ngoài các best practice chung cho keep rule, tác giả library phải tuân thủ một số yêu cầu riêng và cân nhắc thêm vài hướng dẫn.
Tuân thủ yêu cầu tối ưu
Phần tiêu đề “Tuân thủ yêu cầu tối ưu”Library kém hiệu quả là nguyên nhân lớn khiến app phình to, tốn bộ nhớ, khởi động chậm và bị ANR (Application Not Responding). Library phải tránh vi phạm các yêu cầu sau để không làm giảm đáng kể chất lượng app và trải nghiệm người dùng.
-
Không có keep rule rộng hoặc cấp package: library không được có keep rule rộng giữ phần lớn code của nó hay của library khác. Rule rộng có thể giải quyết crash trước mắt nhưng làm phình kích thước của mọi app dùng library.
Đừng đưa keep rule cấp package (như
-keep class com.mylibrary.** {*; }) cho package trong library của bạn hay của library khác được tham chiếu. Những rule này hạn chế tối ưu trên các package đó ở mọi app dùng library. -
Không có global rule không phù hợp: không bao giờ dùng global option như
-dontobfuscatehay-allowaccessmodification. -
Ưu tiên codegen hơn reflection khi có thể: khi có thể, dùng sinh code (codegen) thay vì reflection. Cả hai đều là cách phổ biến để tránh code boilerplate, nhưng codegen tương thích với công cụ tối ưu như R8 hơn.
Với codegen, code được phân tích và chỉnh sửa trong quá trình build. Vì không có thay đổi lớn nào sau khi biên dịch, công cụ tối ưu biết code nào cuối cùng cần và code nào xoá an toàn được.
Với reflection, code được phân tích và thao tác lúc chạy. Vì code chưa thật sự “chốt” cho tới khi chạy, công cụ tối ưu không biết code nào xoá an toàn được. Nó nhiều khả năng xoá code được dùng động qua reflection lúc chạy, khiến app crash với người dùng.
Nhiều library hiện đại dùng codegen thay vì reflection. Xem KSP, entry point chung được Room, Hilt và nhiều library khác dùng. Cũng có trường hợp dùng reflection là hợp lý, xem Khi nào dùng reflection được.
-
Hỗ trợ R8 full mode: library không được crash khi bật R8 full mode. Full mode là cách dùng R8 được khuyến nghị và là mặc định từ AGP 8.0 (bản stable từ 2023). Nếu library crash dưới R8, cách sửa là tìm đúng entry point reflection hoặc JNI và thêm rule nhắm đúng chỗ, không phải keep cả package.
Khuyến nghị thêm
Phần tiêu đề “Khuyến nghị thêm”Ngoài các yêu cầu trên, có thêm các khuyến nghị:
- Đừng dùng
-repackageclassestrong file consumer keep rule. Tuy vậy, để tối ưu bản build library, bạn có thể dùng-repackageclassesvới tên package nội bộ, như<your.library.package>.internal, trong file library build keep rule. Cách này giúp library hiệu quả hơn trong app không được tối ưu, nhưng thường không cần vì app cũng nên được tối ưu. - Khai báo mọi attribute library cần trong các file keep rule của library, kể cả khi chúng có thể trùng với attribute đã định nghĩa trong
proguard-android-optimize.txt. - Nếu bản phát hành library cần các attribute sau, khai báo chúng trong file library build keep rule, không phải file consumer keep rule:
AnnotationDefaultEnclosingMethodExceptionsInnerClassesRuntimeInvisibleAnnotationsRuntimeInvisibleParameterAnnotationsRuntimeInvisibleTypeAnnotationsRuntimeVisibleAnnotationsRuntimeVisibleParameterAnnotationsRuntimeVisibleTypeAnnotationsSignature
- Tác giả library nên giữ attribute
RuntimeVisibleAnnotationstrong consumer keep rule nếu annotation được dùng lúc chạy. - Tác giả library không nên dùng các global option sau trong consumer keep rule:
-include-basedirectory-injars-outjars-libraryjars-repackageclasses-flattenpackagehierarchy-allowaccessmodification-renamesourcefileattribute-ignorewarnings-addconfigurationdebugging-printconfiguration-printmapping-printusage-printseeds-applymapping-obfuscationdictionary-classobfuscationdictionary-packageobfuscationdictionary
Khi nào dùng reflection được
Phần tiêu đề “Khi nào dùng reflection được”Nếu buộc phải dùng reflection, chỉ nên reflect vào một trong hai loại:
- Các kiểu được nhắm cụ thể (class implement một interface cụ thể, hoặc subclass của một class cụ thể)
- Code có một runtime annotation cụ thể
Dùng reflection theo cách này giới hạn chi phí lúc chạy và cho phép viết consumer keep rule nhắm đúng chỗ.
Kiểu reflection cụ thể, có mục tiêu này là mẫu bạn thấy ở cả Android framework (ví dụ khi inflate activity, view và drawable) lẫn các library AndroidX (ví dụ khi tạo ListenableWorker của WorkManager hay RoomDatabase). Ngược lại, reflection không giới hạn của Gson không phù hợp cho app Android.
Các hiểu lầm hay gặp
Phần tiêu đề “Các hiểu lầm hay gặp”Vài hiểu lầm phổ biến có thể khiến bạn cấu hình R8 sai:
- Hiểu sai về tối ưu của R8: trái với suy nghĩ phổ biến, tối ưu của R8 không chỉ là obfuscation, mà còn gồm code shrinking và các tối ưu logic như inline method và class merging. Xem Tổng quan về tối ưu của R8.
- Bỏ qua tối ưu cho library đã obfuscate: một lỗi hay gặp là loại library khỏi tối ưu vì library đã được tối ưu hoặc obfuscate khi biên dịch thành AAR (Android Archive) hay JAR (Java Archive). Tối ưu lúc build library rất hạn chế, và app không nên tắt tối ưu cho library bằng cách đưa nó vào keep rule. Xem Tối ưu bản build AAR.
- Hiểu sai về option
-keep: rule-keepchặn R8 chạy mọi lượt tối ưu. Xem Chọn keep option phù hợp.
Kiểm tra chất lượng consumer keep rule
Phần tiêu đề “Kiểm tra chất lượng consumer keep rule”Để kiểm tra library không chặn quá nhiều tối ưu khi được nhúng vào app, dùng một app mẫu hoặc app integration test của library. Bật R8, rồi xem chất lượng cấu hình R8 tổng thể của app bằng R8 Configuration Analyzer.
Nếu báo cáo cho thấy consumer rule hạn chế tối ưu trên phần lớn code của library, đó là dấu hiệu consumer rule cần tinh chỉnh. Thu hẹp consumer rule và kiểm thử các phần bị ảnh hưởng của library trong một app có bật R8.
Đóng gói consumer keep rule
Phần tiêu đề “Đóng gói consumer keep rule”Để consumer keep rule được áp dụng đúng, bạn phải đóng gói chúng cho phù hợp với định dạng library.
Nếu dùng Jetpack Keep để khai báo việc dùng reflection và JNI của library, Gradle plugin của nó có thể đóng gói rule sinh ra thành consumer keep rule cho bạn.
Library AAR
Phần tiêu đề “Library AAR”Để thêm consumer rule cho library AAR, dùng option consumerProguardFiles trong build script của Android library module. Xem thêm hướng dẫn tạo library module.
android { defaultConfig { consumerProguardFiles("consumer-proguard-rules.pro") } ...}android { defaultConfig { consumerProguardFiles 'consumer-proguard-rules.pro' } ...}Library JAR
Phần tiêu đề “Library JAR”Để đóng rule kèm library Kotlin hoặc Java phát hành dạng JAR, đặt file rule vào thư mục META-INF/proguard/ của JAR cuối cùng, tên file tuỳ ý. Ví dụ nếu code ở <libraryroot>/src/main/kotlin, đặt file consumer rule ở <libraryroot>/src/main/resources/META-INF/proguard/consumer-proguard-rules.pro, rule sẽ được đóng vào đúng vị trí trong JAR output.
Kiểm tra JAR cuối cùng đóng gói rule đúng bằng cách xem rule có nằm trong thư mục META-INF/proguard không.
Tối ưu bản build AAR (nâng cao)
Phần tiêu đề “Tối ưu bản build AAR (nâng cao)”Nói chung bạn không cần tối ưu trực tiếp bản build library vì tối ưu được lúc build library rất hạn chế. Là developer library, bạn phải tính tới nhiều giai đoạn tối ưu và hành vi keep, cả lúc build library lẫn lúc build app, trước khi tối ưu library.
Nếu vẫn muốn tối ưu library lúc build, Android Gradle Plugin có hỗ trợ:
android { buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } configureEach { consumerProguardFiles("consumer-rules.pro") } }}android { buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } configureEach { consumerProguardFiles "consumer-rules.pro" } }}Lưu ý hành vi của proguardFiles khác hẳn consumerProguardFiles:
proguardFilesdùng lúc build, thường cùng vớigetDefaultProguardFile("proguard-android-optimize.txt"), để xác định phần nào của library được giữ khi build library. Tối thiểu là API public của bạn.consumerProguardFilesthì được đóng vào library để ảnh hưởng tới tối ưu về sau, khi build app có dùng library.
Ví dụ nếu library dùng reflection để tạo các class nội bộ, có thể bạn phải khai báo keep rule ở cả proguardFiles lẫn consumerProguardFiles.
Nếu dùng -repackageclasses khi build library, hãy repackage class vào một package con bên trong package của library. Ví dụ dùng -repackageclasses 'com.example.mylibrary.internal' thay vì -repackageclasses 'internal'.
Hỗ trợ nhiều phiên bản R8 (nâng cao)
Phần tiêu đề “Hỗ trợ nhiều phiên bản R8 (nâng cao)”Bạn có thể viết rule nhắm vào các phiên bản R8 cụ thể. Nhờ vậy library chạy tối ưu trong project dùng R8 mới, mà rule cũ vẫn dùng được trong project dùng R8 cũ.
Để khai báo rule theo phiên bản R8, đặt chúng trong thư mục META-INF/com.android.tools bên trong classes.jar của AAR, hoặc trong thư mục META-INF/com.android.tools của JAR.
Trong library AAR: proguard.txt (vị trí cũ, tên file phải là "proguard.txt") classes.jar └── META-INF └── com.android.tools (vị trí rule theo phiên bản R8) ├── r8-from-<X>-upto-<Y>/<R8-rule-files> └── ... (thêm các thư mục cùng định dạng tên)
Trong library JAR: META-INF ├── proguard/<ProGuard-rule-files> (vị trí cũ) └── com.android.tools (vị trí rule theo phiên bản R8) ├── r8-from-<X>-upto-<Y>/<R8-rule-files> └── ... (thêm các thư mục cùng định dạng tên)Trong thư mục META-INF/com.android.tools có thể có nhiều thư mục con tên dạng r8-from-<X>-upto-<Y> để cho biết rule viết cho phiên bản R8 nào. Mỗi thư mục con có một hoặc nhiều file chứa rule R8, tên và đuôi file tuỳ ý.
Phần -from-<X> và -upto-<Y> là tuỳ chọn, phiên bản <Y> không bao gồm, và các khoảng phiên bản thường liên tục nhưng cũng có thể chồng nhau.
Ví dụ r8, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 và r8-from-8.2.0 là tên các thư mục chứa rule theo phiên bản R8. Rule trong thư mục r8 dùng được cho mọi phiên bản R8. Rule trong thư mục r8-from-8.0.0-upto-8.2.0 dùng được cho R8 từ 8.0.0 tới trước 8.2.0.
Android Gradle plugin dùng thông tin này để chọn mọi rule dùng được với phiên bản R8 hiện tại. Nếu library không khai báo rule theo phiên bản R8, Android Gradle plugin chọn rule ở vị trí cũ (proguard.txt với AAR, hoặc META-INF/proguard/<ProGuard-rule-files> với JAR).