發揮 R8 最佳化工具的最大潛力

R8 提供兩種模式:相容模式和完整模式。完整模式提供強大的最佳化功能,可提升應用程式效能。

本指南適用於想使用 R8 最強大最佳化功能的 Android 開發人員。本文將探討相容模式和完整模式的主要差異,並提供明確的設定,協助您安全地遷移專案,避免常見的執行階段當機問題。

啟用完整模式

如要啟用完整模式,請從 gradle.properties 檔案中移除下列程式碼:

android.enableR8.fullMode=false // Remove this line to enable full mode

保留與屬性相關聯的類別

屬性是儲存在已編譯類別檔案中的中繼資料,不屬於可執行程式碼。不過,某些類型的反射可能需要這些屬性。常見的範例包括 Signature (在型別抹除後保留泛型型別資訊)、InnerClassesEnclosingMethod (用於反映類別結構),以及執行階段可見的註解。

以下程式碼顯示位元碼中欄位的 Signature 屬性。欄位:

List<User> users;

編譯後的類別檔案會包含下列位元組碼:

.field public static final users:Ljava/util/List;
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "Ljava/util/List<",
            "Lcom/example/package/User;",
            ">;"
        }
    .end annotation
.end field

大量使用反射的程式庫 (例如 Gson) 通常會依賴這些屬性,動態檢查及瞭解程式碼的結構。在 R8 的完整模式中,只有在明確保留相關聯的類別、欄位或方法時,屬性才會保留。

以下範例說明為何需要屬性,以及從相容性模式遷移至完整模式時,需要新增哪些保留規則。

請參考以下範例,瞭解如何使用 Gson 程式庫還原序列化使用者清單。


import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

data class User(
    @SerializedName("username")
    var username: String? = null,
    @SerializedName("age")
    var age: Int = 0
)

fun GsonRemoteJsonListExample() {
    val gson = Gson()

    // 1. The JSON string for a list of users returned from remote
    val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""

    // 2. Deserialize the JSON string into a List<User>
    // We must use TypeToken for generic types like List
    val listType = object : TypeToken<List<User>>() {}.type
    val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)

    // Print the list
    println("First user from list: ${deserializedList}")
}

在編譯期間,Java 的型別清除作業會移除泛型型別引數。這表示在執行階段,List<String>List<User> 都會顯示為原始 List。因此,Gson 等依賴反射的程式庫在還原序列化 JSON 清單時,無法判斷 List 宣告要包含的特定物件型別,這可能會導致執行階段問題。

為保留型別資訊,Gson 會使用 TypeToken。包裝 TypeToken 會保留必要的還原序列化資訊。

Kotlin 運算式 object:TypeToken<List<User>>() {}.type 會建立擴充 TypeToken 的匿名內部類別,並擷取泛型型別資訊。在本範例中,匿名類別名為 $GsonRemoteJsonListExample$listType$1

Java 程式設計語言會將超類別的泛型簽章儲存為中繼資料,也就是已編譯類別檔案中的 Signature 屬性。TypeToken 接著會使用這項 Signature 中繼資料,在執行階段復原型別。這樣 Gson 就能使用反射讀取 Signature,並順利探索反序列化所需的完整 List<User> 型別。

在相容模式下啟用 R8 時,即使未明確定義特定保留規則,R8 仍會保留類別的 Signature 屬性,包括 $GsonRemoteJsonListExample$listType$1 等匿名內部類別。因此,R8 相容模式不需要任何其他明確的保留規則,這個範例就能正常運作。

// keep rule for compatibility mode
-keepattributes Signature

在完整模式下啟用 R8 時,系統會移除匿名內部類別 $GsonRemoteJsonListExample$listType$1Signature 屬性。如果沒有這項型別資訊,Gson 就無法找到正確的應用程式型別,因此會導致 IllegalStateExceptionSignature如要避免這種情況,必須遵守以下保留規則:

// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
  • -keepattributes Signature:這項規則會指示 R8 保留 Gson 需要讀取的屬性。在完整模式下,R8 只會保留 Signature 規則明確比對的類別、欄位或方法 keep 屬性。

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken:這項規則是必要的,因為 TypeToken 會包裝要還原序列化的物件類型。型別清除後,系統會建立匿名內部類別,保留泛型型別資訊。如果沒有明確保留 com.google.gson.reflect.TypeToken,R8 在完整模式下不會在還原序列化所需的 Signature 屬性中加入這個類別型別。

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken:這項規則會保留擴充 TypeToken 的匿名類別類型資訊,例如這個範例中的 $GsonRemoteJsonListExample$listType$1。如果沒有這項規則,完整模式的 R8 會移除必要的型別資訊,導致還原序列化失敗。

從 Gson 2.11.0 版開始,程式庫會組合完整模式下還原序列化所需的必要保留規則。啟用 R8 建構應用程式時,R8 會自動從程式庫找出並套用這些規則。這樣一來,應用程式就能獲得所需保護,您也不必在專案中手動新增或維護這些特定規則。

請務必瞭解,先前分享的規則只能解決探索泛型型別的問題 (例如 List<User>)。R8 也會重新命名類別的欄位。如果不在資料模型中使用 @SerializedName 註解,Gson 就無法還原序列化 JSON,因為欄位名稱不再與 JSON 金鑰相符。

不過,如果您使用的 Gson 版本舊於 2.11,或模型未使用 @SerializedName 註解,則必須為這些模型新增明確的保留規則。

保留預設建構函式

在 R8 完整模式中,即使類別本身已保留,系統也不會隱含保留無引數/預設建構函式。如果您使用 class.getDeclaredConstructor().newInstance()class.newInstance() 建立類別例項,就必須在完整模式中明確保留無引數建構函式。相較之下,相容性模式一律會保留無引數建構函式。

舉例來說,假設使用反射建立 PrecacheTask 的例項,以動態呼叫其 run 方法。雖然這種情況不需要相容模式中的額外規則,但在完整模式中,PrecacheTask 的預設建構函式會遭到移除。因此必須設定特定的保留規則。

// In library
interface StartupTask {
    fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
    fun execute(taskClass: Class<out StartupTask>) {
        // The class isn't removed, but its constructor might be.
        val task = taskClass.getDeclaredConstructor().newInstance()
        task.run()
    }
}

// In app
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("Pre cache task", "Warming up the cache...")
    }
}

fun runTaskRunner() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified

-keep class com.example.fullmoder8.PreCacheTask {
    <init>();
}

存取權修改功能預設為啟用

在相容模式下,R8 不會變更類別中方法和欄位的可見度。不過,在完整模式下,R8 會變更方法和欄位的顯示狀態 (例如從私有變更為公開),藉此提升最佳化成效。這樣就能進行更多內嵌作業。

如果程式碼使用反射,且特別依賴具有特定可見度的成員,這項最佳化作業可能會導致問題。R8 無法辨識這種間接用法,可能導致應用程式當機。如要避免這種情況,您必須新增特定 -keep 規則來保留成員,這也會保留成員的原始可見度。

詳情請參閱這個範例,瞭解為何不建議使用反射存取私人成員,以及保留這些欄位/方法的保留規則。