Bỏ qua để đến nội dung

Keep rule của R8

Khi bạn bật tối ưu app với thiết lập mặc định, R8 tối ưu rất sâu để đạt lợi ích hiệu năng cao nhất: nó đổi tên, di chuyển và xoá class, field, method. Nếu những thay đổi đó làm app lỗi, bạn cần chỉ cho R8 phần code nào không được đụng tới bằng cách khai báo trong keep rule.

R8 nhận ra và giữ lại mọi lời gọi trực tiếp trong code. Nhưng R8 không thấy được các cách dùng code gián tiếp, nên có thể xoá code mà app cần và làm app crash. Keep rule dùng để bảo R8 giữ những code được dùng gián tiếp như vậy. Vài tình huống hay cần keep rule:

  • Code được truy cập bằng reflection: R8 không biết khi nào class, field hay method được truy cập qua reflection. Ví dụ, R8 không nhận ra một method được tìm theo tên bằng Class.getDeclaredMethod() hay một annotation được lấy bằng Class.getAnnotation(). Khi đó R8 có thể đổi tên hoặc xoá hẳn method và annotation đó, gây ClassNotFoundException hay NoSuchMethodException lúc chạy.
  • Code được gọi qua Java Native Interface (JNI): khi code native (C hoặc C++) gọi một method Java/Kotlin, hoặc code Java/Kotlin gọi C++ qua JNI, lời gọi dựa trên việc tra tên method bằng chuỗi lúc chạy. R8 không thấy lời gọi dựa trên chuỗi này nên tối ưu có thể làm hỏng code.

Danh sách trên chưa đầy đủ nhưng bao phần lớn các trường hợp cần keep rule.

Nếu reflection hoặc JNI nằm trong code của chính bạn, bạn có thể gắn annotation Jetpack Keep thay vì tự viết keep rule. Gradle plugin của Jetpack Keep sẽ sinh keep rule từ annotation lúc build.

Cách cấu hình keep rule phụ thuộc phiên bản AGP.

App lớn thường có code nằm ở nhiều library module. Khi đó, tốt hơn là đặt keep rule ngay cạnh code mà nó áp dụng, trong chính library module đó. Điểm khác biệt khi quản lý keep rule cho library là cách khai báo rule trong build.gradle.kts (hoặc build.gradle) của library module, xem bài R8: Chọn và viết thư viện.

Đặt rule vào file có đuôi .keep trong thư mục src/<variant>/keepRules, ví dụ src/main/keepRules/custom-rules.keep. DSL mới của AGP 9.3 bật cả tối ưu code lẫn resource, và không cần khai báo tường minh file keep rule mặc định của Android (proguard-android-optimize.txt) vì nó được thêm sẵn, trừ khi bạn chủ động bỏ.

android {
buildTypes {
release {
optimization {
enable = true // Bật tối ưu code và resource.
}
}
}
}

Bỏ keep rule mặc định của nền tảng Android (nâng cao)

Phần tiêu đề “Bỏ keep rule mặc định của nền tảng Android (nâng cao)”

AGP cung cấp sẵn một bộ rule mặc định cho nền tảng Android. Để bỏ bộ rule này và tự quản lý toàn bộ keep rule, đặt optimization.keepRules.includeDefault thành false.

android {
buildTypes {
release {
optimization {
enable = true // Bật tối ưu code và resource.
keepRules {
includeDefault = false // File rule tối ưu cho nền tảng
// Android được thêm mặc định. Đặt false để bỏ file này.
}
}
}
}
}

Đặt rule vào file proguard-rules.pro ở thư mục gốc của app module (file có thể đã có sẵn, nếu chưa thì tạo mới). Để rule trong file được áp dụng, bạn phải khai báo file đó trong build.gradle.kts (hoặc build.gradle) cấp module.

Mặc định build file cũng đã có proguard-android-optimize.txt. File này chứa các rule cần cho hầu hết dự án Android, nên hãy giữ nó trong build file. Nó dựa trên và dùng chung nội dung với file proguard-common.txt.

android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
// File mặc định với các rule tối ưu mặc định.
getDefaultProguardFile("proguard-android-optimize.txt"),
...
)
}
}
}

Nếu app có nhiều phiên bản theo build variant, hãy tạo keep rule riêng cho từng variant. Ví dụ app có bản miễn phí và bản trả phí với tính năng và dependency khác nhau thì mỗi bản nên có keep rule riêng.

Với AGP 9.3 trở lên: đặt keep rule của từng flavor vào source set keep rule tương ứng. Ví dụ với flavor1, viết keep rule trong app/src/flavor1/keepRules/flavor1-rules.keep.

android {
buildTypes {
release {
optimization {
enable = true // Bật tối ưu code và resource.
}
}
}
flavorDimensions.add("version")
productFlavors {
create("flavor1") {
...
// Đặt keep rule trong app/src/flavor1/keepRules/, file đuôi .keep
}
create("flavor2") {
...
// Đặt keep rule trong app/src/flavor2/keepRules/, file đuôi .keep
}
}
}

Với AGP dưới 9.3: thêm thuộc tính proguardFiles (hoặc proguardFile) vào khối flavor tương ứng trong productFlavors. Build script sau thêm file flavor2-rules.pro cho product flavor flavor2:

android {
...
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
flavorDimensions.add("version")
productFlavors {
create("flavor1") {
...
}
create("flavor2") {
proguardFile("flavor2-rules.pro")
}
}
}

Keep rule cho resource (không phải code) xem bài R8: Kiểm thử và gỡ lỗi.

Khi thêm rule, bạn có thể dùng global option (chỉ thị chung ảnh hưởng tới cách R8 xử lý toàn bộ codebase, xem mục Global option) và keep rule của riêng bạn. Keep rule cần được thiết kế cẩn thận để cân bằng giữa tối ưu tối đa và không vô tình làm hỏng app.

Nhìn tổng quát, một keep rule chỉ ra một class (hoặc subclass, hoặc class implement một interface), rồi chỉ ra các member (method, constructor, field) trong class đó cần giữ.

Cú pháp chung:

-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>

Ví dụ rule dùng keep option keepclassmembers, modifier allowoptimization, giữ someSpecificMethod() của com.example.MyClass:

-keepclassmembers,allowoptimization class com.example.MyClass {
void someSpecificMethod();
}

Keep option là phần đầu tiên của keep rule, xác định khía cạnh nào của class được giữ. Có sáu keep option: keep, keepclassmembers, keepclasseswithmembers, keepnames, keepclassmembernames và keepclasseswithmembernames.

Keep optionMô tả
keepclassmembersGiữ các member được chỉ định, chỉ khi R8 không xoá class chứa chúng.
keepGiữ class và các member (field, method) được chỉ định, không cho tối ưu chúng.

Lưu ý: nói chung chỉ nên dùng keep kèm keep option modifier, vì keep đứng một mình chặn mọi loại tối ưu trên các class khớp.
keepclasseswithmembersGiữ class và các member được chỉ định, chỉ khi class có đủ tất cả member trong class specification.
keepclassmembernamesKhông cho đổi tên các member được chỉ định, nhưng không ngăn class hay member bị xoá.

Lưu ý: ý nghĩa option này hay bị hiểu nhầm, nên dùng dạng tương đương -keepclassmembers,allowshrinking.
keepnamesKhông cho đổi tên class và member, nhưng không ngăn chúng bị xoá hẳn nếu được coi là không dùng.

Lưu ý: ý nghĩa option này hay bị hiểu nhầm, nên dùng dạng tương đương -keep,allowshrinking.
keepclasseswithmembernamesKhông cho đổi tên class và các member được chỉ định, nhưng chỉ khi các member đó còn trong code cuối cùng. Không ngăn việc xoá code.

Lưu ý: ý nghĩa option này hay bị hiểu nhầm, nên dùng dạng tương đương -keepclasseswithmembers,allowshrinking.

Chọn đúng keep option quyết định mức tối ưu của app. Có option cho phép shrink (xoá code không được tham chiếu), có option cho phép obfuscate (đổi tên). Bảng sau cho biết mỗi option còn cho R8 làm gì (✓ là R8 vẫn được làm, ✗ là bị chặn):

Keep optionShrink classObfuscate classShrink memberObfuscate member
keep✗✗✗✗
keepclassmembers✓✓✗✗
keepclasseswithmembers✗✗✗✗
keepnames✓✗✓✗
keepclassmembernames✓✓✓✗
keepclasseswithmembernames✓✗✓✗

Keep option modifier điều chỉnh phạm vi và hành vi của keep rule. Một rule có thể có 0 hoặc nhiều modifier.

Giá trịMô tả
allowoptimizationCho phép tối ưu các phần tử được chỉ định, nhưng không đổi tên hay xoá chúng.
allowobfuscationCho phép đổi tên các phần tử được chỉ định, nhưng không xoá hay tối ưu kiểu khác.
allowshrinkingCho phép xoá các phần tử được chỉ định nếu R8 không thấy tham chiếu nào tới chúng, nhưng không đổi tên hay tối ưu kiểu khác.
includedescriptorclassesBảo R8 giữ mọi class xuất hiện trong descriptor của method (kiểu tham số, kiểu trả về) và field (kiểu field) đang được giữ.
allowaccessmodificationCho R8 đổi (thường là nới rộng) access modifier (public, private, protected) của class, method và field trong quá trình tối ưu.
allowrepackageCho R8 chuyển class sang package khác, kể cả package mặc định (gốc).

Mọi keep rule đều phải chỉ ra một class (bao gồm interface, enum và annotation class). Bạn có thể giới hạn thêm theo annotation, theo superclass hoặc interface được implement, hoặc theo access modifier của class. Mọi class, kể cả class trong java.lang như java.lang.String, phải viết bằng tên Java đầy đủ. Để biết tên cần dùng, xem bytecode bằng các công cụ ở mục Xem tên Java được sinh ra.

Ví dụ với class MaterialButton:

  • Đúng: com.google.android.material.button.MaterialButton
  • Sai: MaterialButton

Class specification cũng chỉ ra các member cần giữ trong class. Ví dụ rule sau giữ class MyClass và method someSpecificMethod():

-keep class com.example.MyClass {
void someSpecificMethod();
}

Để chọn class theo annotation, thêm @ trước tên Java đầy đủ của annotation:

-keep class @com.example.MyAnnotation com.example.MyClass

Nếu rule có nhiều annotation, nó chỉ giữ class có đủ tất cả annotation được liệt kê. Ví dụ rule sau giữ mọi class được gắn cả Annotation1 lẫn Annotation2:

-keep class @com.example.Annotation1 @com.example.Annotation2 *

Để nhắm vào subclass hoặc class implement interface, dùng extends và implements.

Ví dụ có class Bar với subclass Foo:

class Foo : Bar()

Rule sau giữ mọi subclass của Bar. Lưu ý rule không giữ bản thân superclass Bar.

-keep class * extends Bar

Nếu class Foo implement interface Bar:

class Foo : Bar

Rule sau giữ mọi class implement Bar, không giữ bản thân interface Bar.

-keep class * implements Bar

Bạn có thể dùng access modifier như public, private, static và final để rule chính xác hơn.

Ví dụ rule sau giữ mọi class public trong package api và các package con, cùng mọi member public và protected trong các class đó.

-keep public class com.example.api.** { public protected *; }

Bạn cũng có thể dùng modifier cho member. Ví dụ rule sau chỉ giữ các method public static của class Utils:

-keep class com.example.Utils {
public static void *(...);
}

R8 không hỗ trợ modifier riêng của Kotlin như internal và suspend. Làm theo hướng dẫn sau để giữ những phần tử này.

  • internal: coi như public. Ví dụ với mã Kotlin:

    package com.example
    internal class ImportantInternalClass {
    internal val f: Int
    internal fun m() {}
    }

    Class, method và field internal là public trong file .class do Kotlin compiler sinh ra, nên bạn phải dùng từ khoá public:

    -keepclassmembers public class com.example.ImportantInternalClass {
    public int f;
    public void m();
    }
  • suspend: khớp với signature bytecode sau khi biên dịch. Ví dụ một repository class viết bằng Kotlin:

    package com.example.repository
    import com.example.model.User
    class UserRepository {
    suspend fun fetchUser(id: String): User {
    // Chi tiết cài đặt...
    }
    }

    Khi biên dịch sang bytecode, hàm suspend được biến đổi theo kiểu Continuation-Passing Style (CPS): modifier suspend bị bỏ, kiểu trả về đổi thành java.lang.Object, và một tham số kotlin.coroutines.Continuation được thêm vào cuối signature để quản lý state machine bất đồng bộ.

    Signature của method trong bytecode trông như sau:

    public final Object fetchUser(String id, Continuation<? super User> continuation);

    Để viết keep rule cho hàm này, bạn không dùng cú pháp suspend của Kotlin được. Bạn phải khớp đúng signature bytecode (nhắc tới kotlin.coroutines.Continuation), hoặc dùng ....

    Khớp đúng signature bytecode:

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(java.lang.String, kotlin.coroutines.Continuation);
    }

    Dùng ...:

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(...);
    }

Class specification có thể kèm danh sách member cần giữ. Nếu bạn chỉ ra một hoặc nhiều member, rule không áp dụng cho các member khác.

Tương tự class, bạn thêm @ trước tên Java đầy đủ của annotation để chỉ giữ những member được gắn annotation đó. Ví dụ giữ các method và field có @com.example.MyAnnotation:

-keep class com.example.MyClass {
@com.example.MyAnnotation <methods>;
@com.example.MyAnnotation <fields>;
}

Có thể kết hợp với annotation cấp class để có rule nhắm rất chính xác:

-keep class @com.example.ClassAnnotation * {
@com.example.MethodAnnotation <methods>;
@com.example.FieldAnnotation <fields>;
}

Rule này giữ các class có @ClassAnnotation, và trong các class đó, giữ method có @MethodAnnotation và field có @FieldAnnotation.

Nên dùng keep rule theo annotation khi có thể. Cách này tạo liên kết rõ ràng giữa code và keep rule, và thường cho cấu hình chắc chắn hơn. Thư viện annotation androidx.annotation chẳng hạn dùng đúng cơ chế này.

Với reflection trong code của chính bạn, bạn cũng có thể dùng Jetpack Keep: thay vì viết keep rule nhắm vào annotation, bạn gắn annotation vào code và một Gradle plugin sinh keep rule cho bạn.

Cú pháp chỉ ra một method:

[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);

Ví dụ rule sau giữ method public getUserId() trả về String:

-keep class com.example.model.UserData {
public java.lang.String getUserId();
}

Dùng <methods> làm cách viết tắt để khớp mọi method trong class:

-keep class com.example.model.UserData {
<methods>;
}

Cách viết kiểu trả về và kiểu tham số xem mục Kiểu dữ liệu.

Constructor được viết bằng <init>. Cú pháp:

[<access_modifier>] <init>(parameter_types);

Ví dụ rule sau giữ constructor của một UI state holder nhận vào một instance repository:

-keep class com.example.ui.state.UserViewModel {
public <init>(com.example.repository.UserDataRepository);
}

Giữ mọi constructor public:

-keep class com.example.ui.state.UserViewModel {
public <init>(...);
}

Cú pháp chỉ ra một field:

[<access_modifier>...] [<type>] <field_name>;

Ví dụ rule sau giữ field private kiểu chuỗi userId và field public static kiểu int STATUS_ACTIVE:

-keep class com.example.models.User {
private java.lang.String userId;
public static int STATUS_ACTIVE;
}

Dùng <fields> để khớp mọi field trong class:

-keep class com.example.models.User {
<fields>;
}

Mục này nói cách viết kiểu trả về, kiểu tham số và kiểu field trong member specification. Nhớ dùng tên Java được sinh ra nếu chúng khác với mã nguồn Kotlin.

Kiểu nguyên thuỷ. Dùng từ khoá Java. R8 nhận các kiểu nguyên thuỷ: boolean, byte, short, char, int, long, float, double.

# Giữ method nhận một int và một float.
-keepclassmembers class com.example.Calculator {
public void setValues(int, float);
}

Kiểu generic. Khi biên dịch, compiler Kotlin/Java xoá thông tin kiểu generic, nên keep rule liên quan tới generic phải nhắm vào dạng đã biên dịch, không phải mã nguồn. Xem thêm Type erasure.

Ví dụ có kiểu generic không giới hạn trong Box.kt:

package com.myapp.data
class Box<T>(val item: T) {
fun getItem(): T {
return item
}
}

Sau type erasure, T được thay bằng Object. Để giữ constructor và method, rule phải dùng java.lang.Object thay cho T:

# Giữ constructor và method của class Box.
-keep class com.myapp.data.Box {
public <init>(java.lang.Object);
public java.lang.Object getItem();
}

Với kiểu generic có giới hạn trong NumberBox.kt:

package com.myapp.data
// T bị giới hạn là kiểu con của Number
class NumberBox<T : Number>(val number: T)

Type erasure thay T bằng giới hạn của nó, java.lang.Number:

-keep class com.myapp.data.NumberBox {
public <init>(java.lang.Number);
}

Khi dùng kiểu generic riêng của app làm giới hạn, cần thêm keep rule cho cả class giới hạn. Ví dụ:

package com.myapp.data
data class UnpackOptions(val useHighPriority: Boolean)
// Class Box generic với UnpackOptions là kiểu giới hạn
class Box<T: UnpackOptions>(val item: T) {
}

Dùng includedescriptorclasses để giữ cả class UnpackOptions lẫn constructor của Box bằng một rule:

-keep,includedescriptorclasses class com.myapp.data.Box {
public <init>(com.myapp.data.UnpackOptions);
}

Để giữ một hàm xử lý danh sách đối tượng, rule phải khớp chính xác signature của hàm. Do kiểu generic bị xoá, tham số List<Product> được nhìn thấy là java.util.List. Ví dụ:

package com.myapp.utils
import com.myapp.data.Product
import android.util.Log
class DataProcessor {
// Hàm cần giữ
fun processProducts(products: List<Product>) {
Log.d("DataProcessor", "Processing ${products.size} products.")
// Logic nghiệp vụ...
}
}
// Data class dùng trong danh sách
package com.myapp.data
data class Product(val id: String, val name: String)

Rule chỉ bảo vệ hàm processProducts:

-keep class com.myapp.utils.DataProcessor {
public void processProducts(java.util.List);
}

Kiểu mảng. Thêm [] sau kiểu phần tử cho mỗi chiều của mảng, áp dụng cho cả kiểu class lẫn kiểu nguyên thuỷ:

  • Mảng một chiều kiểu class: java.lang.String[]
  • Mảng hai chiều kiểu nguyên thuỷ: int[][]

Ví dụ:

package com.example.data
class ImageProcessor {
fun process(): ByteArray {
// xử lý ảnh, trả về mảng byte
}
}

Rule tương ứng:

# Giữ method trả về mảng byte.
-keepclassmembers class com.example.data.ImageProcessor {
public byte[] process();
}

Giữ một class cùng mọi member:

-keep class com.myapp.MyClass { *; }

Chỉ giữ class cùng constructor mặc định, không giữ member khác:

-keep class com.myapp.MyClass

Nên luôn chỉ ra member. Ví dụ rule sau giữ field public text và method public updateText() trong MyClass:

-keep class com.myapp.MyClass {
public java.lang.String text;
public void updateText(java.lang.String);
}

Giữ mọi field và method public:

-keep public class com.example.api.ApiClient {
public *;
}

Nếu bỏ trống member specification, R8 giữ constructor mặc định của class. Viết -keep class com.example.MyClass hay -keep class com.example.MyClass {} đều được R8 hiểu như:

-keep class com.example.MyClass{
void <init>();
}

Từ AGP 9.2.0, bạn có thể phủ định mẫu tên member trong keep rule bằng cách thêm dấu chấm than (!) trước mẫu tên. Tính năng này hữu ích khi muốn giữ phần lớn member khớp một mẫu rộng nhưng loại trừ vài member, ví dụ method chỉ dùng cho test.

Ví dụ giữ mọi method public trong com.example.MyClass trừ những method có tên kết thúc bằng ForTesting:

-keepclassmembers class com.example.MyClass {
public *** !*ForTesting(...);
}

Để nhắc tới một hàm Kotlin định nghĩa bên ngoài class (top-level function), dùng tên Java được sinh ra của class mà Kotlin compiler ngầm tạo. Tên class là tên file Kotlin cộng thêm Kt. Ví dụ file MyClass.kt:

package com.example.myapp.utils
// Hàm top-level, không nằm trong class nào
fun isEmailValid(email: String): Boolean {
return email.contains("@")
}

Rule cho hàm isEmailValid phải nhắm vào class được sinh ra MyClassKt:

-keep class com.example.myapp.utils.MyClassKt {
public static boolean isEmailValid(java.lang.String);
}

Bảng sau cho biết cách dùng wildcard để áp keep rule cho nhiều class hoặc member khớp một mẫu.

WildcardÁp dụng choMô tả
**Cả haiHay dùng nhất. Khớp mọi tên kiểu, kể cả có bao nhiêu dấu phân tách package. Dùng để khớp mọi class trong một package và các package con.
*Cả haiTrong class specification: khớp một phần tên kiểu không chứa dấu phân tách package (.).
Trong member specification: khớp mọi tên method hoặc field. Khi đứng một mình, * cũng là bí danh của **.
?Cả haiKhớp đúng một ký tự bất kỳ trong tên class hoặc member.
***MemberKhớp mọi kiểu, gồm kiểu nguyên thuỷ (như int), kiểu class (như java.lang.String) và kiểu mảng nhiều chiều (như byte[][]).
...MemberKhớp mọi danh sách tham số của method.
%MemberKhớp mọi kiểu nguyên thuỷ (như int, float, boolean…).

Vài ví dụ dùng wildcard đặc biệt:

  • Có nhiều method cùng tên nhận các kiểu nguyên thuỷ khác nhau, dùng % để giữ tất cả. Ví dụ class DataStore có nhiều method setValue:

    class DataStore {
    fun setValue(key: String, value: Int) { ... }
    fun setValue(key: String, value: Boolean) { ... }
    fun setValue(key: String, value: Float) { ... }
    }

    Rule sau giữ tất cả các method đó:

    -keep class com.example.DataStore {
    public void setValue(java.lang.String, %);
    }
  • Có nhiều class tên chỉ khác nhau một ký tự, dùng ?:

    com.example.models.UserV1 {...}
    com.example.models.UserV2 {...}
    com.example.models.UserV3 {...}

    Rule sau giữ cả ba class:

    -keep class com.example.models.UserV?
  • Khớp class Example và AnotherExample (nếu chúng ở package gốc) nhưng không khớp com.foo.Example:

    -keep class *Example
  • * đứng một mình là bí danh của **. Hai rule sau tương đương:

    -keepclasseswithmembers class * { public static void main(java.lang.String[]); }
    -keepclasseswithmembers class ** { public static void main(java.lang.String[]); }

Ngoài keep rule thường, bạn có thể dùng conditional keep rule, rule chỉ có hiệu lực khi một điều kiện được thoả. Điều kiện được viết bằng cờ -if: keep rule đứng sau -if chỉ có hiệu lực nếu class specification trong -if có class khớp.

Conditional keep rule đặc biệt hữu ích với library hoặc kiểu code dùng reflection, khi một keep rule chỉ cần thiết nếu có một class hay member nào đó tồn tại hoặc khớp mẫu. Rule có điều kiện giúp app nhỏ hơn vì không giữ code thừa.

Cú pháp chung:

-if <class_specification_if> <keep_rule>

Nếu class specification trong -if có wildcard (như * hay **), chuỗi ký tự khớp với wildcard sẽ được ghi lại. Bạn nhắc lại các chuỗi này trong keep rule phía sau bằng backreference: <1> là chuỗi khớp với wildcard đầu tiên, <2> là chuỗi khớp với wildcard thứ hai, cứ thế.

Ví dụ, Jetpack Navigation sinh các class NavArgs để truyền tham số type-safe giữa các destination. Delegate NavArgsLazy dùng reflection tìm và gọi method static fromBundle trên class NavArgs được sinh ra để deserialize tham số. Nếu app dùng NavArgs, bạn chỉ cần giữ method fromBundle cho các class implement interface NavArgs.

Conditional keep rule sau nói: nếu có class implement androidx.navigation.NavArgs, R8 giữ method fromBundle của đúng class đó:

# Nếu một class implement NavArgs...
-if public class ** implements androidx.navigation.NavArgs
# ...thì giữ method fromBundle của class khớp đó (<1>).
-keepclassmembers public class <1> {
public static ** fromBundle(android.os.Bundle);
}

Ở đây ** là wildcard đầu tiên và duy nhất trong điều kiện -if. Nó khớp tên của mọi class implement androidx.navigation.NavArgs. Chuỗi mà ** khớp (tên class) được ghi lại và nhắc lại bằng <1> trong rule phía sau. Như vậy rule -keepclassmembers áp dụng cho mọi class implement NavArgs khớp với điều kiện -if. Nếu không có class nào implement NavArgs, R8 bỏ qua rule này. Đây cũng chính là rule mà Jetpack Navigation tự đóng gói sẵn: nhờ nó, fromBundle chỉ được giữ ở những class cần, thay vì giữ ở mọi class hay bắt bạn tự liệt kê.

Một trường hợp hay gặp khác là thư viện serialization JSON như Gson. Nếu data model dùng annotation @SerializedName của Gson trên field nào đó, bạn có thể dùng conditional rule để bảo vệ class và các member mà Gson cần cho reflection:

# Nếu một class có field gắn @SerializedName...
-if class ** { @com.google.gson.annotations.SerializedName <fields>; }
# ...thì giữ class đó (<1>), các field @SerializedName,
# và constructor của nó cho Gson.
-keep class <1> {
@com.google.gson.annotations.SerializedName <fields>;
<init>(...);
}

Backreference ghi lại chuỗi, nên có thể là một phần của tên class nếu wildcard chỉ khớp một phần tên. Ví dụ với -if class com.example.*X*, R8 ghi chuỗi trước X vào <1> và chuỗi sau X vào <2>. Rule sau dùng điều này để tìm mọi class có X trong tên và giữ class tương ứng với X thay bằng Y:

# Nếu có class kiểu com.example.PrefixXPostfix...
-if class com.example.*X*
# ...thì giữ com.example.PrefixYPostfix.
-keep class com.example.<1>Y<2>

Khi viết keep rule, class và các kiểu tham chiếu phải được viết bằng tên sau khi biên dịch sang bytecode Java (xem ví dụ ở Class specification và Kiểu dữ liệu). Để xem tên Java được sinh ra từ code của bạn, dùng một trong hai công cụ trong Android Studio:

  • APK Analyzer
  • Mở file Kotlin, vào Tools > Kotlin > Show Kotlin Bytecode > Decompile để xem bytecode.

Global option là các option thay đổi tối ưu của R8 trên toàn app hoặc ảnh hưởng tới mọi keep rule. Chúng được viết trong proguard-rules.pro cùng với keep rule. Có option bật thêm tối ưu, có option tắt bớt một phần tối ưu.

R8 cũng chấp nhận nhưng bỏ qua một số option cũ từ ProGuard, xem Gỡ option ProGuard cũ.

  • -repackageclasses [<optional-package-name>]: gom các class vào một package duy nhất để giảm kích thước app. Nếu không ghi tên package, class được chuyển vào unnamed package (package mặc định). Đây là thiết lập được khuyến nghị cho app vì file DEX nhỏ hơn khi tên class không còn tiền tố package. Từ AGP 9.1, đây là cấu hình mặc định cho app. Muốn tắt, dùng -dontrepackage.

  • -allowaccessmodification: cho R8 đổi (thường là nới rộng) visibility của class, field và method để tối ưu sâu hơn. Được bật khi dùng proguard-android-optimize.txt. Từ AGP 8.2, đây là cấu hình mặc định nếu bật full mode.

  • -processkotlinnullchecks [level]: cho R8 đổi các lệnh null check Intrinsics của Kotlin, hoặc chỉ bỏ thông báo lỗi, hoặc bỏ hẳn lệnh kiểm tra null. Các giá trị level, từ yếu tới mạnh:

    • keep: không đổi các lệnh kiểm tra.
    • remove_message: viết lại mỗi lời gọi kiểm tra thành lời gọi getClass() trên tham số đầu tiên (vẫn kiểm tra null nhưng không có thông báo).
    • remove: bỏ hẳn các lệnh kiểm tra.

    Mặc định R8 dùng remove_message. Khai báo -processkotlinnullchecks sẽ ghi đè mặc định này. Nếu khai báo nhiều lần, giá trị mạnh nhất được dùng. Option này được hỗ trợ từ AGP 9.0.0.

Ví dụ cấu hình bật thêm tối ưu:

-repackageclasses
-allowaccessmodification

Các option sau tắt một phần tối ưu, hữu ích khi bạn đang tinh chỉnh keep rule hoặc mới bật R8 lần đầu.

  • -dontoptimize: không tối ưu code (ví dụ không inline method). Dùng được khi phát triển nhưng không nên dùng ở bản production.
  • -dontshrink: không xoá code không được tham chiếu và không tối ưu code. Dùng được khi phát triển nhưng không nên dùng ở bản production.
  • -dontobfuscate: không rút ngắn tên class và method. Đặc biệt hữu ích khi debug vì stack trace dễ đọc hơn. Dùng được khi phát triển nhưng không nên dùng ở bản production.
  • -keepattributes <attributes>: nhận danh sách attribute cần giữ, cách nhau bằng dấu phẩy. Nếu không dùng proguard-android-optimize.txt mặc định, R8 xoá mọi attribute kể cả RuntimeVisibleAnnotations và Signature, nên giữ chúng có thể cần thiết trong các trường hợp như reflection. Danh sách attribute xem Keep attribute.

Attribute là thông tin bổ sung gắn vào các phần của code, lưu những thứ như annotation và generic signature.

Một số thao tác reflection cần giữ attribute cụ thể mới chạy được. Ví dụ:

  • Truy cập cấu trúc inner class hoặc outer class bằng getEnclosingMethod() hay getDeclaredClasses() cần attribute EnclosingMethod và InnerClasses.
  • Truy cập generic signature bằng getTypeParameters() cần attribute Signature.
  • Truy cập annotation bằng getAnnotation() cần attribute RuntimeVisibleAnnotations.

Khi dùng file Proguard mặc định (proguard-android-optimize.txt hoặc proguard-android.txt), AGP giữ các attribute sau. Một số attribute cần AGP mới hơn:

AttributeMô tả
AnnotationDefaultCó trên chính các kiểu annotation, lưu giá trị mặc định của một phần tử annotation.
Được giữ mặc định từ AGP 7.1, chỉ cần keep tường minh với AGP cũ hơn.
EnclosingMethodCó ở inner class không phải local class hay anonymous class. Cho biết method hoặc initializer trực tiếp chứa class.
InnerClassesGhi thông tin về class lồng (inner class, static nested class, local class, anonymous class) định nghĩa bên trong class khác.
LineNumberTableÁnh xạ lệnh bytecode tới số dòng trong file mã nguồn gốc.
Được giữ mặc định từ AGP 8.6, chỉ cần keep tường minh với AGP cũ hơn.
RuntimeVisibleAnnotationsLưu các annotation thấy được qua reflection lúc chạy.
Thường nếu dùng annotation lúc chạy thì đây là attribute duy nhất trong nhóm *Annotation mà app và consumer rule của library cần.
RuntimeVisibleParameterAnnotationsLưu annotation trên tham số của method, thấy được qua reflection lúc chạy.
RuntimeVisibleTypeAnnotationsLưu annotation áp dụng cho chỗ dùng kiểu (type use) chứ không chỉ cho khai báo. Thấy được lúc chạy.
SignatureLưu signature kiểu generic hơn cho class, method và field, nhất là khi chúng dùng generic (như List<String>).
SourceFileLưu tên file mã nguồn (.kt hoặc .java) mà class được biên dịch ra. Chủ yếu dùng cho debugger để hiển thị dòng mã nguồn gốc khi chạy từng bước, giúp lần ngược quá trình thực thi về code đã viết.
Được giữ mặc định từ AGP 8.2, chỉ cần keep tường minh với AGP cũ hơn.

Với app dùng proguard-android-optimize.txt, keep rule do AGP định nghĩa là đủ trong hầu hết trường hợp. Nhưng nếu bạn viết library, hãy khai báo mọi attribute library cần trong consumer keep rule, kể cả khi chúng đã có trong danh sách trên. Như vậy library vẫn chạy đúng nếu developer không dùng proguard-android-optimize.txt.

Bạn có thể giữ thêm các attribute khác, nhưng chúng không cần cho đại đa số trường hợp reflection hay JNI. Một số vẫn hay dùng khi tối ưu library.

AttributeMô tả
MethodParametersThông tin về tham số của method, cụ thể là tên và access flag.
ExceptionsDanh sách checked exception mà method khai báo có thể ném.
Thường không dùng cho app. Với tác giả library, thường không dùng trong consumer keep rule nhưng hay dùng khi build library.
RuntimeInvisibleAnnotationsLưu annotation không thấy được qua reflection lúc chạy trên class, field hoặc method.
Developer app không nên giữ attribute này. Với tác giả library, attribute này không liên quan tới consumer keep rule nhưng hay dùng khi build library.
RuntimeInvisibleParameterAnnotationsLưu annotation không thấy được qua reflection lúc chạy trên tham số của method.
Developer app không nên giữ. Với tác giả library, không liên quan tới consumer keep rule nhưng hay dùng khi build library.
RuntimeInvisibleTypeAnnotationsLưu annotation áp dụng cho chỗ dùng kiểu, không thấy được lúc chạy.
Developer app không nên giữ. Với tác giả library, không liên quan tới consumer keep rule nhưng hay dùng khi build library.

R8 chấp nhận nhưng bỏ qua một số option chỉ có tác dụng với ProGuard. Chúng thường còn sót lại trong cấu hình chuyển từ ProGuard sang hoặc chép từ dự án cũ. Vì không ảnh hưởng gì tới output của R8, hãy xoá chúng khỏi proguard-rules.pro để cấu hình dễ hiểu và dễ bảo trì hơn.

R8 bỏ qua các option sau mà không cảnh báo:

  • -adaptkotlinmetadata
  • -android
  • -dontpreverify
  • -dontskipnonpubliclibraryclasses
  • -dontskipnonpubliclibraryclassmembers
  • -dontusemixedcaseclassnames
  • -forceprocessing
  • -mergeinterfacesaggressively
  • -optimizationpasses
  • -optimizations
  • -overloadaggressively
  • -target
  • -verbose

R8 bỏ qua các option sau và có cảnh báo khi build:

  • -addconfigurationdebugging (hỗ trợ đã bị gỡ ở AGP 9.0)
  • -assumenoescapingparameters
  • -assumenoexternalreturnvalues
  • -assumenoexternalsideeffects
  • -dump
  • -useuniqueclassmembernames

R8 không hỗ trợ -skipnonpubliclibraryclasses và báo lỗi nếu cấu hình có option này.

Ngoài keep rule, R8 cho thêm các rule ảnh hưởng tới tối ưu. Chúng cũng được viết trong proguard-rules.pro cùng keep rule. Có hai nhóm:

  • Assumption: -assumevalues, -assumenosideeffects
  • Tối ưu khác: -convertchecknotnull, -maximumremovedandroidloglevel

Các rule này cho R8 biết nó được phép giả định một hành vi cụ thể của code lúc chạy.

-assumevalues cho R8 biết giá trị của một field, hoặc giá trị trả về của một method, lúc chạy luôn là một hằng số cụ thể hoặc nằm trong một khoảng xác định. Rule này dành cho những thứ như giá trị cờ (flag) mà lúc build đã biết chắc giá trị lúc chạy.

Phân tích tĩnh thông thường của R8 có thể không xác định được giá trị lúc chạy của member. Với -assumevalues, bạn bảo R8 giả định giá trị hoặc khoảng đã cho khi tối ưu, giúp R8 tối ưu mạnh tay.

Cú pháp giống cú pháp member specification, thêm mệnh đề return:

<member_specification> return <value> | <range>

<value> và <range> hỗ trợ các loại giá trị:

  • Giá trị đặc biệt: true, false, null, @NonNull
  • Giá trị nguyên thuỷ: int
  • Hằng chuỗi
  • Tham chiếu field static (kể cả field của enum)

Khoảng được viết theo dạng bao gồm hai đầu min..max. Ví dụ rule sau cho biết biến CUSTOM_VAL nhận giá trị từ 26 tới 2147483647:

-assumevalues public class com.example.Foo {
public static int CUSTOM_VAL return 26..2147483647;
}

Dùng rule này khi:

  • Với library: đảm bảo khi app được tối ưu, mọi hook debug nội bộ đều bị xoá khỏi code public của library.
  • Với app: xoá những thứ như code debug khỏi bản release. Nên ưu tiên dùng build variant và source set hoặc hằng số riêng cho từng variant; nhưng nếu cách đó không phù hợp, hoặc bạn cần đảm bảo chắc chắn hơn rằng nhánh code bị xoá hoàn toàn, hãy dùng -assumevalues.

Ví dụ một class mà R8 xoá các công cụ debug khỏi bản app đã tối ưu:

package com.example;
public class MyConfig {
// Field khởi tạo là false nhưng được ghi đè bằng giá trị resource
// hoặc cơ chế khác trong quá trình build cuối. Phân tích tĩnh của R8
// có thể chỉ thấy giá trị 'false' ban đầu, trong khi giá trị lúc chạy
// được biết chắc là 'true'.
public static final boolean IS_OPTIMIZED_VERSION = false;
}
// Ở class khác:
public void initFeatures() {
if (MyConfig.IS_OPTIMIZED_VERSION) {
System.out.println("Starting optimized features...");
android.util.Log.d(TAG, "Starting optimized features...");
initOptimizedService();
} else {
android.util.Log.d(TAG, "Starting debug/logging features...");
initDebugTools();
}
}

Rule sau cho R8 biết IS_OPTIMIZED_VERSION luôn là true:

-assumevalues class com.example.MyConfig {
public static final boolean IS_OPTIMIZED_VERSION return true;
}

-assumenosideeffects cho R8 biết các member được chỉ định không có side effect. R8 có thể xoá hẳn lời gọi tới các method đó nếu chúng không trả về giá trị hoặc trả về giá trị cố định.

Cú pháp giống cú pháp member specification. Ví dụ sau cho R8 biết mọi method public static tên log trong class DebugLogger không có side effect, nhờ đó R8 xoá được lời gọi tới chúng:

-assumenosideeffects class com.example.DebugLogger {
public static void log(...);
}

Đây là các tối ưu nâng cao không bật mặc định. Khi bật, bạn cho phép R8 tối ưu theo chỉ dẫn, ngoài các tối ưu mặc định.

Dùng -convertchecknotnull để tối ưu null check. Rule áp dụng cho mọi method nhận một tham số object và ném exception nếu object đó null, giống assertion chuẩn của Kotlin. Loại exception và thông báo có thể khác, nhưng hành vi “crash khi null” thì giống nhau.

Nếu một rule -convertchecknotnull khớp với một method, mọi lời gọi method đó được thay bằng lời gọi getClass() trên tham số đầu tiên. Lời gọi getClass() đóng vai trò null check thay thế và cho phép R8 bỏ các tham số thừa của null check gốc, như việc tạo chuỗi tốn kém.

Cú pháp:

-convertchecknotnull <class_specification> {
<member_specification>;
}

Ví dụ có class Preconditions với method checkNotNull:

class Preconditions {
fun <T> checkNotNull(value: T?): T {
if (value == null) {
throw NullPointerException()
} else {
return value
}
}
}

Dùng rule:

-convertchecknotnull class com.example.package.Preconditions {
void checkNotNull(java.lang.Object);
}

Rule chuyển mọi lời gọi checkNotNull() thành lời gọi getClass trên tham số đầu tiên. Trong ví dụ, checkNotNull(bar) được thay bằng bar.getClass(). Nếu bar là null, bar.getClass() ném NullPointerException, cho hiệu quả kiểm tra null tương tự nhưng gọn hơn.

Rule này xoá các lệnh log Android (như Log.w(...) và Log.isLoggable(...)) ở một mức log trở xuống.

Cú pháp:

-maximumremovedandroidloglevel <log_level> [<class_specification>]

Nếu không ghi class_specification, R8 xoá log trên toàn app.

Nhãn logMức log
VERBOSE2
DEBUG3
INFO4
WARNING5
ERROR6
ASSERT7

Ví dụ với code:

class Foo {
private static final String TAG = "Foo";
void logSomething() {
if (Log.isLoggable(TAG, WARNING)) {
Log.e(TAG, "Won't be logged");
}
Log.w(TAG, "Won't be logged");
Log.e(TAG, "Will be logged");
}
}

Và rule:

# Mức 5 tương ứng với mức log WARNING.
-maximumremovedandroidloglevel 5 class Foo { void logSomething(); }

Code sau khi tối ưu:

class Foo {
private static final String TAG = "Foo";
void logSomething() {
Log.e(TAG, "Will be logged");
}
}

Nếu khai báo nhiều mức log tối đa cho cùng một method, R8 dùng mức nhỏ nhất. Ví dụ với hai rule:

-maximumremovedandroidloglevel 7 class ** { void foo(); }
-maximumremovedandroidloglevel 4 class ** { void foo(); }

thì mức log tối đa bị xoá của foo() là 4.

Khi làm việc với keep rule, cần viết cụ thể vừa đủ để vừa có lợi ích tối ưu vừa giữ đúng hành vi của app.

Keep rule tốt là rule cụ thể nhất có thể, theo các mẫu sau:

  • Trong class specification, luôn chỉ rõ một class cụ thể, một base class, hoặc class có annotation nếu được:

    -keepclassmembers class com.example.MyClass {
    void someSpecificMethod();
    }
    -keepclassmembers ** extends com.example.MyBaseClass {
    void someSpecificMethod();
    }
    -keepclassmembers @com.example.MyAnnotation class ** {
    void someSpecificMethod();
    }
  • Khi có thể, gắn annotation vào mã nguồn rồi nhắm keep rule vào annotation đó. Cách này tạo liên kết rõ ràng giữa code và rule giữ nó, khiến cấu hình chắc chắn, dễ hiểu và ít hỏng khi code thay đổi.

    Ví dụ đoạn sau giữ class MyClass cùng mọi class khác có annotation @com.example.DisplayComponent:

    // Trong mã nguồn
    @com.example.DisplayComponent
    class MyClass { /* ... */ }
    // Trong keep rule
    -keep @com.example.DisplayComponent class * {*;}

    Nên đặt tên annotation sao cho nói lên lý do code được giữ. Ví dụ dùng @DisplayComponent cho app có các display component cần giữ một số phần.

  • Khi có thể, khai báo member specification và chỉ nhắc tới những phần của class bắt buộc phải giữ để app chạy được. Không nên áp rule cho cả class bằng phạm vi member { *; } trừ khi thật sự cần.

    -keepclassmembers class com.example.MyClass {
    void someSpecificMethod();
    @com.example.MyAnnotation *;
    }
  • Nếu dùng global option repackageclasses, đừng ghi tên package tuỳ chọn. File DEX sẽ nhỏ hơn vì tên class sau khi repackage không có tiền tố package.

  • Duy trì keep rule cho mọi thứ được truy cập bằng reflection. Kể cả khi R8 tự giữ chúng mà không cần rule, việc có rule thật vẫn rất quan trọng, vì thay đổi code sau này có thể khiến R8 không giữ chúng nữa.

    Với reflection trong code của bạn, có thể dùng Jetpack Keep để sinh keep rule dễ bảo trì hơn. Mỗi annotation nằm trên chính đoạn code thực hiện reflection, nên khi đổi code đó bạn cập nhật hoặc xoá annotation ngay tại chỗ. Nhờ vậy rule không giữ những code mà app đã thôi reflect. Lint cũng báo các lời gọi reflection chưa có annotation.

  • Khi có thể, dùng -assumenosideeffects hoặc -convertchecknotnull để tối ưu null check, vì việc này có thể giảm đáng kể kích thước DEX. Xem Các loại rule khác.

Nếu chưa làm theo được các hướng dẫn trên, bạn có thể tạm gom code cần giữ vào một package riêng và áp keep rule cho package đó. Đây không phải giải pháp lâu dài, xem Bật tối ưu từng bước. Keep rule cho package viết như sau:

-keepclassmembers class com.example.pkg.** { *; }

Cú pháp keep rule có rất nhiều lựa chọn, nhưng để có lợi ích hiệu năng đo được và bền vững, không nên dùng những cách sau:

  • Đừng dùng keep rule cấp package như -keep class com.example.pkg.** { *; } lâu dài. Chúng chỉ nên dùng tạm để vượt qua lỗi khi cấu hình R8, xem Giới hạn phạm vi tối ưu. Nói chung hãy cẩn thận với wildcard, chắc chắn bạn chỉ giữ đúng phần code cần.
  • Khi có thể, tránh các library yêu cầu bạn chép keep rule vào dự án, nhất là keep rule cấp package. Library được thiết kế tốt cho Android nên hạn chế reflection và tự đóng gói consumer keep rule khi cần.
  • Tránh toán tử phủ định ! trong keep rule, vì dễ vô tình áp rule lên gần như mọi class trong app.

Nếu không làm theo được các nguyên tắc này, có thể bạn đang dùng nhiều reflection không giới hạn (open-ended reflection), và nên bỏ reflection hoặc bỏ library dùng reflection đó (xem ví dụ Gson).