以下範例是根據您使用 R8 進行最佳化時的常見情境,但需要進階指引才能草擬保留規則。
反射
一般來說,為達到最佳效能,不建議使用反射。不過,在某些情況下,這可能無法避免。以下範例提供在常見情境中使用反射的保留規則指引。
如果反射位於您自己的應用程式或程式庫程式碼中,您可以使用保留註解,而不必手動編寫保留規則。Gradle 外掛程式會從原始碼中的註解產生保留規則,包括程式庫的消費者保留規則。
透過名稱所載入類別進行反射
程式庫通常會使用類別名稱做為 String,以動態方式載入類別。不過,R8 無法偵測以這種方式載入的類別,且可能會移除其認為未使用的類別。
舉例來說,假設您有一個程式庫和一個使用該程式庫的應用程式,程式碼會示範程式庫載入器,該載入器會例項化由應用程式實作的 StartupTask 介面。
程式庫程式碼如下:
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(className: String) {
// R8 won't retain classes specified by this string value at runtime
val taskClass = Class.forName(className)
val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
task.run()
}
}
使用程式庫的應用程式包含下列程式碼:
// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
override fun run() {
// This log will never appear if the class is removed by R8.
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is told to run the app's task by its name.
TaskRunner.execute("com.example.app.PreCacheTask")
}
在這種情況下,程式庫應包含消費者保留規則檔案,其中包含下列保留規則:
-keep class * implements com.example.library.StartupTask {
<init>();
}
如果沒有這項規則,R8 會從應用程式中移除 PreCacheTask,因為應用程式不會直接使用該類別,導致整合中斷。這項規則會找出實作程式庫 StartupTask 介面的類別,並保留這些類別及其無引數建構函式,讓程式庫順利例項化及執行 PreCacheTask。
使用 ::class.java 進行反映
程式庫可以讓應用程式直接傳遞 Class 物件來載入類別,這種方法比依名稱載入類別更穩健。這會建立 R8 可偵測到的類別強烈參照。不過,雖然這可防止 R8 移除類別,您仍須使用保留規則,宣告類別是以反射方式例項化,並保護以反射方式存取的成員,例如建構函式。
舉例來說,假設您有一個程式庫和一個使用該程式庫的應用程式,程式庫載入器會直接傳遞類別參照,將 StartupTask 介面例項化。
程式庫程式碼如下:
// The interface for a task that runs once.
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()
}
}
使用程式庫的應用程式包含下列程式碼:
// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
override fun run() {
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
在這種情況下,程式庫應包含消費者保留規則檔案,其中包含下列保留規則:
# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
<init>();
}
這些規則旨在與這類反射完美搭配,在確保程式碼正常運作的同時,盡可能進行最佳化。如果應用程式從未使用 StartupTask 類別,這些規則可讓 R8 模糊處理類別名稱,並縮減或移除該類別的實作。不過,對於任何實作項目 (例如範例中使用的 PrecacheTask),它們會保留程式庫需要呼叫的預設建構函式 (<init>())。
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask:這項規則會以實作StartupTask介面的任何類別為目標。-keep class * implements com.example.library.StartupTask:這會保留實作介面的任何類別 (*)。,allowobfuscation:這會指示 R8 雖然保留類別,但可以重新命名或模糊化。這樣做很安全,因為程式庫不依賴類別名稱,而是直接取得Class物件。,allowshrinking:這個修飾符會指示 R8,如果類別未使用,可以移除該類別。這樣一來,R8 就能安全地刪除從未傳遞至TaskRunner.execute()的StartupTask實作項目。簡而言之,這項規則代表:如果應用程式使用的類別會實作StartupTask,R8 就會保留該類別。R8 可以重新命名類別來縮減大小,如果應用程式未使用該類別,則可將其刪除。
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }:這項規則會指定第一個規則中識別的類別特定成員,在本例中為建構函式。-keepclassmembers class * implements com.example.library.StartupTask:這會保留實作StartupTask介面的類別特定成員 (方法、欄位),但前提是實作的類別本身會保留。{ <init>(); }:這是成員選取器。<init>是 Java 位元碼中建構函式的特殊內部名稱。這個部分特別針對預設的無引數建構函式。- 這項規則至關重要,因為您的程式碼會呼叫
getDeclaredConstructor().newInstance(),但不會傳遞任何引數,這會以反射方式叫用預設建構函式。如果沒有這項規則,R8 會發現沒有程式碼直接呼叫new PreCacheTask(),並假設建構函式未被使用,進而將其移除。這會導致應用程式在執行階段當機,並顯示InstantiationException。
根據方法註解進行反射
程式庫通常會定義註解,供開發人員標記方法或欄位。程式庫接著會使用反射,在執行階段找出這些註解成員。舉例來說,@OnLifecycleEvent 註解可用於在執行階段尋找必要方法。
舉例來說,假設您有一個程式庫和一個使用該程式庫的應用程式,以下範例示範了事件匯流排如何尋找並叫用加上 @OnEvent 註解的方法。
程式庫程式碼如下:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent
class EventBus {
fun dispatch(listener: Any) {
// Find all methods annotated with @OnEvent and invoke them
listener::class.java.declaredMethods.forEach { method ->
if (method.isAnnotationPresent(OnEvent::class.java)) {
try {
method.invoke(listener)
} catch (e: Exception) { /* ... */ }
}
}
}
}
使用程式庫的應用程式包含下列程式碼:
class MyEventListener {
@OnEvent
fun onSomethingHappened() {
// This method will be removed by R8 without a keep rule
Log.d(TAG, "Event received!")
}
}
fun onCreate() {
// Instantiate the listener and the event bus
val listener = MyEventListener()
val eventBus = EventBus()
// Dispatch the listener to the event bus
eventBus.dispatch(listener)
}
程式庫應包含消費者保留規則檔案,自動保留使用其註解的任何方法:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations:這項規則會保留要在執行階段讀取的註解。-keep @interface com.example.library.OnEvent:這項規則會保留OnEvent註解類別本身。-keepclassmembers class * {@com.example.library.OnEvent <methods>;}:只有在類別正在使用中,且類別包含這些成員時,這項規則才會保留類別和特定成員。-keepclassmembers:只有在使用類別且類別包含這些成員時,這項規則才會保留類別和特定成員。class *:規則適用於任何類別。@com.example.library.OnEvent <methods>;:這會保留一或多個方法 (<methods>) 加上@com.example.library.OnEvent註解的類別,並保留加上註解的方法本身。
根據類別註解進行反射
程式庫可以使用反射功能,掃描具有特定註解的類別。在這種情況下,工作執行器類別會使用反射功能,找出所有以 ReflectiveExecutor 註解的類別,並執行 execute 方法。
舉例來說,假設您有一個程式庫和一個使用該程式庫的應用程式。
程式庫包含下列程式碼:
@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)
}
}
}
使用程式庫的應用程式包含下列程式碼:
// In consumer app
@ReflectiveExecutor
class ImportantBackgroundTask {
fun execute() {
// This class will be removed by R8 without a keep rule
Log.e("ImportantBackgroundTask", "Executing the important background task...")
}
}
// Usage of ImportantBackgroundTask
fun onCreate(){
val task = ImportantBackgroundTask()
val runner = TaskRunner()
runner.process(task)
}
由於程式庫會以反射方式取得特定類別,因此程式庫應包含消費者保留規則檔案,並加入下列保留規則:
# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations
# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor
# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
public void execute();
}
這項設定非常有效率,因為它會明確告知 R8 要保留哪些內容。
反映以支援選用依附元件
反射的常見用途是在核心程式庫和選用外掛程式庫之間建立軟性依附元件。核心程式庫可以檢查應用程式是否包含外掛程式,如果包含,則可啟用額外功能。這樣一來,您就能運送外掛程式模組,而不必強制核心程式庫直接依附這些模組。
核心程式庫會使用反射 (Class.forName) 依名稱尋找特定類別。如果找到該類別,系統就會啟用這項功能。如果沒有,則會正常失敗。
舉例來說,請看下列程式碼,其中核心 AnalyticsManager 會檢查選用的 VideoEventTracker 類別,以啟用影片分析功能。
核心程式庫包含下列程式碼:
object AnalyticsManager {
private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
fun initialize() {
try {
// Attempt to load the optional module's class using 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())
}
}
}
選用的影片庫包含下列程式碼:
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
選用程式庫的開發人員有責任提供必要的消費者保留規則。這項保留規則可確保任何使用選用程式庫的應用程式,都會保留核心程式庫需要尋找的程式碼。
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
如果沒有這項規則,R8 可能會從選用程式庫中移除 VideoEventTracker,因為該模組中沒有任何項目直接使用該程式庫。保留規則會保留類別及其建構函式,讓核心程式庫成功例項化。
透過反射存取私有成員
使用反射存取不屬於程式庫公用 API 的私人或受保護程式碼,可能會導致重大問題。這類程式碼可能隨時變更,恕不另行通知,這可能會導致應用程式發生非預期行為或當機。
如果您依賴反射來存取非公開 API,可能會遇到下列問題:
- 更新遭封鎖:私人或受保護程式碼的變更可能會導致您無法更新至較高的程式庫版本。
- 錯失好處:您可能會錯過新功能、重要當機修正或必要安全性更新。
R8 最佳化和反射
如果您必須反映到程式庫的私人或受保護程式碼,請密切注意 R8 的最佳化。如果沒有直接參照這些成員,R8 可能會假設這些成員未使用,並隨後移除或重新命名。這可能會導致執行階段當機,且通常會顯示誤導性錯誤訊息,例如 NoSuchMethodException 或 NoSuchFieldException。
舉例來說,請參考以下情境,瞭解如何從程式庫類別存取私人欄位。
不屬於您的程式庫包含下列程式碼:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
應用程式包含下列程式碼:
fun accessSecretMessage(instance: LibraryClass) {
// Use Java reflection from Kotlin to access the private field
val secretField = instance::class.java.getDeclaredField("secretMessage")
secretField.isAccessible = true
// This will crash at runtime with R8 enabled
val message = secretField.get(instance) as String
}
在應用程式中新增 -keep 規則,防止 R8 移除私人欄位:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers:只有在保留類別本身時,才會保留類別的特定成員。class com.example.LibraryClass:這會指定包含欄位的確切類別。private java.lang.String secretMessage;:依名稱和型別識別特定私有欄位。
Java Native Interface (JNI)
從原生 (C/C++ 程式碼) 向上呼叫 Java 或 Kotlin 時,R8 的最佳化功能可能會發生問題。反過來說,從 Java 或 Kotlin 到原生程式碼的向下呼叫也可能發生問題,但預設檔案 proguard-android-optimize.txt 包含下列規則,可確保向下呼叫正常運作。這項規則可防止原生方法遭到裁剪。
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
透過 Java Native Interface (JNI) 與原生程式碼互動
如果應用程式使用 JNI 從原生 (C/C++) 程式碼向上呼叫 Java 或 Kotlin,R8 就無法判斷原生程式碼呼叫了哪些方法。如果應用程式中沒有直接參照這些方法,R8 會誤以為這些方法未被使用並將其移除,導致應用程式異常終止。
以下範例顯示 Kotlin 類別,其中包含要從原生資料庫呼叫的方法。原生資料庫會例項化應用程式類型,並將資料從原生程式碼傳遞至 Kotlin 程式碼。
package com.example.models
// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
/**
* This method is called from the native side.
* R8 will remove it if it's not kept.
*/
fun onNativeEvent(data: NativeData) {
Log.d(TAG, "Received event from native code: $data")
}
// Use 'external' to declare a native method
external fun startNativeProcess()
companion object {
init {
// Load the native library
System.loadLibrary("my-native-lib")
}
}
}
在這種情況下,您必須通知 R8,避免最佳化應用程式類型。此外,如果從原生程式碼呼叫的方法在簽章中將自有類別做為參數或回傳型別,您也必須確認這些類別不會重新命名。
在應用程式中新增下列保留規則:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
這些保留規則可防止 R8 移除或重新命名 onNativeEvent 方法,以及其參數類型 (這點非常重要)。
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}:只有在類別先於 Kotlin 或 Java 程式碼中例項化時,才會保留類別的特定成員,這會告知 R8 應用程式正在使用該類別,且應保留類別的特定成員。-keepclassmembers:只有在 Kotlin 或 Java 程式碼中先將類別例項化,才會保留類別的特定成員。這會告知 R8 應用程式正在使用該類別,且應保留該類別的特定成員。class com.example.JniBridge:這會指定包含欄位的確切類別。includedescriptorclasses:這個修飾符也會保留在方法簽章或描述元中找到的任何類別。在本例中,這項規則可防止 R8 重新命名或移除做為參數使用的com.example.models.NativeData類別。如果NativeData重新命名 (例如a.a),方法簽章就不會再與原生程式碼預期的內容相符,導致當機。public void onNativeEvent(com.example.models.NativeData);:指定要保留的方法確切 Java 簽章。
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: 雖然includedescriptorclasses可確保保留NativeData類別本身,但從原生 JNI 程式碼直接存取的NativeData內任何成員 (欄位或方法) 都需要自己的保留規則。-keep class NativeData:這會指定名為NativeData的類別,而區塊會指定要保留NativeData類別中的哪些成員。<init>(java.lang.Integer, java.lang.String):這是建構函式的簽章。它會明確識別採用兩個參數的建構函式:第一個是Integer,第二個是String。
您也可以使用 Jetpack Keep 的 @UnconditionallyKeep 註解,為原生程式碼呼叫的方法加上註解,而不必為這些方法編寫 Keep 規則。註解不會保留方法簽章中的類別 (例如 NativeData),因此您仍需個別保留這些類別。
間接平台通話
透過 Parcelable 的實作來轉移資料
Android 架構會使用反射方法建立 Parcelable 物件的例項。在現代 Kotlin 開發中,您應使用 kotlin-parcelize 外掛程式,該外掛程式會自動產生必要的 Parcelable 實作,包括架構所需的 CREATOR 欄位和方法。
舉例來說,請參考以下範例,其中使用 kotlin-parcelize 外掛程式建立 Parcelable 類別:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
val name: String,
val age: Int
) : Parcelable
在這個情境中,沒有建議的保留規則。kotlin-parcelize Gradle 外掛程式會自動為您使用 @Parcelize 註解的類別產生必要的保留規則。它會為您處理複雜的問題,確保產生的 CREATOR 和建構函式會保留給 Android 架構的反射呼叫。
如果您在 Kotlin 中手動編寫 Parcelable 類別,而不使用 @Parcelize,則必須保留 CREATOR 欄位和接受 Parcel 的建構函式。如果忘記這麼做,系統嘗試還原序列化物件時,應用程式就會異常終止。使用 @Parcelize 是標準做法,也比較安全。
使用 kotlin-parcelize 外掛程式時,請注意下列事項:
- 外掛程式會在編譯期間自動建立
CREATOR欄位。 proguard-android-optimize.txt檔案包含必要的keep規則,可保留這些欄位,確保功能正常運作。- 應用程式開發人員必須確認所有必要的
keep規則都存在,尤其是任何自訂實作或第三方依附元件。
熱門圖書館
使用反射或位元碼轉換的程式庫,會在執行階段動態存取程式碼。如果 R8 移除或重新命名以這種方式存取的類別、欄位或方法,應用程式可能會當機。
不過,熱門的第三方程式庫 (例如 Gson、Retrofit 和 Kotlinx Serialization) 會自動將自己的 R8 消費者保留規則組合在一起。使用這些程式庫的近期版本時,您不需要在專案中手動新增保留規則。
Gson
Gson 是 JSON 序列化和反序列化程式庫,高度依賴反射。使用完整模式最佳化應用程式時,除非另有明確指示,否則系統會移除泛型型別簽章、預設建構函式和未註解的欄位。
為確保 Gson 函式正常運作,請新增特定規則,將非暫時性欄位保留在資料模型類別中,並保留 TypeToken 階層:
# Preserve generic type information required for deserialization
-keepattributes Signature
# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
!transient <fields>;
}
# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
標示 transient 修飾符的欄位會在序列化和還原序列化期間遭到 Gson 忽略,因此保留規則會特別指定非暫時性欄位 (!transient)。
Retrofit
Retrofit 是一個網路程式庫,會使用反射檢查以 HTTP 註解 (例如 @GET 或 @POST) 註解的服務介面方法,建構網路要求並轉換回應。
Retrofit 會在執行階段使用 Proxy.newProxyInstance() 動態產生 API 介面的實作。由於 R8 沒有看到任何類別以靜態方式實作這些介面,因此可能會移除方法或其一般傳回類型。
已組合的保留規則
Retrofit 會依賴執行階段反射來檢查一般參數、方法註解和參數註解。如果未正確設定,R8 完整模式可能會徹底從傳回類型、Kotlin 續傳和回應類別中移除泛型簽章,甚至將介面值替換為空值,因為 Retrofit 介面會透過 Proxy 動態例項化。
從 Retrofit 2.10.0 開始,程式庫會自動綁定官方保留規則,以保留註解預設值、服務方法參數和必要的類別中繼資料。詳情請參閱「Retrofit 使用的規則」。
保留一般傳回類型
Retrofit 會檢查傳回類型的泛型簽章 (例如 Observable<Data>),正確地將網路回應反序列化。如果 R8 移除一般簽章,Retrofit 會將例項化物件替換為 null。
如要避免 R8 完整模式剝除傳回類型的泛型簽章,請使用下列條件式規則:
# Preserve generic type information for Call/Observable return types
-keepattributes Signature
# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
@retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
傳回的實際資料模型類別 (例如 Observable<Data> 中的 Data) 也必須保留,因為轉換器 (例如 Gson) 會以反射方式建構該類別。
協同程式
使用 Kotlin 協同程式時,Kotlin 編譯器會將 suspend 函式附加 Continuation 參數至編譯方法簽章,藉此轉換函式。
當 Retrofit 等程式庫以反射方式讀取 suspend 函式的泛型簽章時,會依賴該 Continuation 參數。使用完整模式時,只有明確保留的類別會保留 Signature 屬性。由於 Continuation 是合成參數,R8 預設會剝除其簽章,導致反射中斷。
為避免簽章遭到移除,並確保完整模式下的執行階段相容性,請加入下列規則:
# Keep the signature attribute globally
-keepattributes Signature
# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation