R8 提供兩種模式:相容模式和完整模式。完整模式提供強大的最佳化功能,可提升應用程式效能。
本指南適用於想使用 R8 最強大最佳化功能的 Android 開發人員。本文將探討相容模式和完整模式的主要差異,並提供明確的設定,協助您安全地遷移專案,避免常見的執行階段當機問題。
啟用完整模式
如要啟用完整模式,請從 gradle.properties 檔案中移除下列程式碼:
android.enableR8.fullMode=false // Remove this line to enable full mode
保留與屬性相關聯的類別
屬性是儲存在已編譯類別檔案中的中繼資料,不屬於可執行程式碼。不過,某些類型的反射可能需要這些屬性。常見的範例包括 Signature (在型別抹除後保留泛型型別資訊)、InnerClasses 和 EnclosingMethod (用於反映類別結構),以及執行階段可見的註解。
以下程式碼顯示位元碼中欄位的 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$1 的 Signature 屬性。如果沒有這項型別資訊,Gson 就無法找到正確的應用程式型別,因此會導致 IllegalStateException。Signature如要避免這種情況,必須遵守以下保留規則:
// 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 規則來保留成員,這也會保留成員的原始可見度。
詳情請參閱這個範例,瞭解為何不建議使用反射存取私人成員,以及保留這些欄位/方法的保留規則。