從高階層面來看,保留規則會指定類別 (或子類別或實作),以及要保留的該類別成員 (方法、建構函式或欄位)。
保留規則的一般語法如下,但特定保留選項不接受選用的 keep_option_modfier
。
-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>
以下範例顯示使用 keepclassmembers
做為保留選項、allowoptimization
做為修飾符的保留規則,並保留 com.example.MyClass
中的 someSpecificMethod()
:
-keepclassmembers,allowoptimization class com.example.MyClass {
void someSpecificMethod();
}
保留選項
保留選項是保留規則的第一部分。這項註解會指定要保留類別的哪些層面。共有六種保留選項,分別是 keep
、keepclassmembers
、keepclasseswithmembers
、keepnames
、keepclassmembernames
和 keepclasseswithmembernames
。
下表說明這些保留選項:
保留選項 | 說明 |
---|---|
keepclassmembers |
只有在最佳化後類別仍存在時,才會保留指定成員。 |
keep |
保留指定類別和指定成員 (欄位和方法),避免遭到最佳化。 注意:一般來說, keep 應只搭配 keep 選項修飾符使用,因為 keep 本身會禁止系統對相符類別進行任何最佳化。 |
keepclasseswithmembers |
只有在類別包含類別規格中的所有成員時,才會保留類別及其指定成員。 |
keepclassmembernames |
禁止重新命名指定班級成員,但不會禁止移除班級或成員。 注意:這個選項的意義經常遭到誤解,建議改用對等的 -keepclassmembers,allowshrinking 。 |
keepnames |
防止重新命名課程和成員,但如果系統判定課程未使用,仍會完全移除。 注意:這個選項的意義經常遭到誤解,建議改用對等的 -keep,allowshrinking 。 |
keepclasseswithmembernames |
防止重新命名類別及其指定成員,但前提是成員存在於最終程式碼中。但不會阻止移除程式碼。 注意:這個選項的意義經常遭到誤解,建議改用對應的 -keepclasseswithmembers,allowshrinking 。 |
選擇合適的保留選項
選擇合適的保留選項,對決定應用程式的最佳化方式至關重要。某些保留選項會縮減程式碼 (移除未參照的程式碼),其他選項則會混淆或重新命名程式碼。下表說明各種保留選項的動作:
保留選項 | 縮減類別 | 混淆類別 | 縮減成員 | 模糊處理成員 |
---|---|---|---|---|
keep |
||||
keepclassmembers |
||||
keepclasseswithmembers |
||||
keepnames |
||||
keepclassmembernames |
||||
keepclasseswithmembernames |
保留選項修飾符
保留選項修飾符可用來控管保留規則的範圍和行為。您可以在保留規則中新增 0 個以上的保留選項修飾符。
下表說明 keep 選項修飾符的可能值:
值 | 說明 |
---|---|
allowoptimization |
可最佳化指定元素。但系統不會重新命名或移除指定元素。 |
allowobfucastion |
允許重新命名指定元素。不過,這些元素不會遭到移除或以其他方式最佳化。 |
allowshrinking |
如果 R8 找不到指定元素的參照,即可移除這些元素。但系統不會重新命名或以其他方式最佳化元素。 |
includedescriptorclasses |
指示 R8 保留方法 (參數類型和傳回類型) 和欄位 (欄位類型) 描述元中出現的所有類別。 |
allowaccessmodification |
允許 R8 在最佳化程序期間,變更 (通常是擴大) 類別、方法和欄位的存取修飾符 (public 、private 、protected )。 |
allowrepackage |
允許 R8 將類別移至不同套件,包括預設 (根) 套件。 |
類別規格
您必須指定類別、父類別或實作的介面,做為保留規則的一部分。所有類別 (包括 java.lang
命名空間中的類別,例如 java.lang.String
) 都必須使用完整的 Java 名稱指定。如要瞭解應使用的名稱,請使用「取得產生的 Java 名稱」一節所述的工具檢查位元碼。
以下範例說明如何指定 MaterialButton
類別:
- 正確:
com.google.android.material.button.MaterialButton
- 錯誤:
MaterialButton
類別規格也會指定類別中應保留的成員。下列規則會保留 MaterialButton
類別和所有成員:
-keep class com.google.android.material.button.MaterialButton { *; }
子類別和實作項目
如要指定實作介面的子類別或類別,請分別使用 extend
和 implements
。
舉例來說,如果您有類別 Bar
,且子類別為 Foo
,如下所示:
class Foo : Bar()
下列保留規則會保留 Bar
的所有子類別。請注意,保留規則不包含超類別 Bar
本身。
-keep class * extends Bar
如果您有實作 Bar
的 Foo
類別 Foo
:
class Foo : Bar
下列保留規則會保留實作 Bar
的所有類別。請注意,保留規則不包含 Bar
介面本身。
-keep class * implements Bar
存取修飾符
您可以指定 public
、private
、static
和 final
等存取修飾符,讓保留規則更加精確。
舉例來說,下列規則會保留 api
套件及其子套件中的所有 public
類別,以及這些類別中的所有公開和受保護成員。
-keep public class com.example.api.** { public protected *; }
您也可以使用類別中成員的修飾符。舉例來說,下列規則只會保留 Utils
類別的 public static
方法:
-keep class com.example.Utils {
public static void *(...);
}
Kotlin 專屬修飾符
R8 不支援 Kotlin 專屬修飾符,例如 internal
和 suspend
。請按照下列規範保留這類欄位。
如要保留
internal
類別、方法或欄位,請將其視為公開。舉例來說,請參考下列 Kotlin 來源:package com.example internal class ImportantInternalClass { internal f: Int internal fun m() {} }
Kotlin 編譯器產生的
.class
檔案中,internal
類別、方法和欄位都是public
,因此您必須使用public
關鍵字,如下列範例所示:-keepclassmembers public class com.example.ImportantInternalClass { public int f; public void m(); }
編譯
suspend
成員時,請在保留規則中比對其編譯簽章。舉例來說,如果您定義的
fetchUser
函式如下列程式碼片段所示:suspend fun fetchUser(id: String): User
編譯後,位元組碼中的簽章如下所示:
public final Object fetchUser(String id, Continuation<? super User> continuation);
如要為這個函式編寫保留規則,您必須比對這個已編譯的簽章,或使用
...
。以下是使用已編譯簽章的範例:
-keepclassmembers class com.example.repository.UserRepository { public java.lang.Object fetchUser(java.lang.String, kotlin.coroutines.Continuation); }
以下是使用
...
的範例:-keepclassmembers class com.example.repository.UserRepository { public java.lang.Object fetchUser(...); }
成員規格
類別規格可選擇性地包含要保留的類別成員。如果您為課程指定一或多名成員,規則只會套用至這些成員。
舉例來說,如要保留特定課程及其所有成員,請使用下列指令:
-keep class com.myapp.MyClass { *; }
如要只保留類別,不要保留成員,請使用下列指令:
-keep class com.myapp.MyClass
大多數情況下,您會想要指定部分成員。舉例來說,下列範例會保留類別 MyClass
中的公開欄位 text
和公開方法 updateText()
。
-keep class com.myapp.MyClass {
public java.lang.String text;
public void updateText(java.lang.String);
}
如要保留所有公開欄位和公開方法,請參閱下列範例:
-keep public class com.example.api.ApiClient {
public *;
}
方法
在保留規則的成員規格中指定方法的語法如下:
[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);
舉例來說,下列保留規則會保留名為 setLabel()
的公開方法,該方法會傳回空白並採用 String
。
-keep class com.example.MyView {
public void setLabel(java.lang.String);
}
您可以使用 <methods>
做為捷徑,比對類別中的所有方法,如下所示:
-keep class com.example.MyView {
<methods>;
}
如要進一步瞭解如何指定傳回型別和參數型別,請參閱「型別」。
建構函式
如要指定建構函式,請使用 <init>
。在保留規則的成員規格中指定建構函式的語法如下:
[<access_modifier>] <init>(parameter_types);
舉例來說,下列保留規則會保留採用 Context
和 AttributeSet
的自訂 View
建構函式。
-keep class com.example.ui.MyCustomView {
public <init>(android.content.Context, android.util.AttributeSet);
}
如要保留所有公開建構函式,請參考下列範例:
-keep class com.example.ui.MyCustomView {
public <init>(...);
}
欄位
在保留規則的成員規格中指定欄位的語法如下:
[<access_modifier>...] [<type>] <field_name>;
舉例來說,下列保留規則會保留名為 userId
的私有字串欄位,以及名為 STATUS_ACTIVE
的公開靜態整數欄位:
-keep class com.example.models.User {
private java.lang.String userId;
public static int STATUS_ACTIVE;
}
您可以使用 <fields>
做為捷徑,比對類別中的所有欄位,如下所示:
-keep class com.example.models.User {
<fields>;
}
套件層級函式
如要參照在類別外部定義的 Kotlin 函式 (通常稱為頂層函式),請務必使用 Kotlin 編譯器隱含新增的類別產生的 Java 名稱。類別名稱是 Kotlin 檔案名稱,後面加上 Kt
。舉例來說,如果您有名為 MyClass.kt
的 Kotlin 檔案,定義如下:
package com.example.myapp.utils
// A top-level function not inside a class
fun isEmailValid(email: String): Boolean {
return email.contains("@")
}
如要為 isEmailValid
函式編寫保留規則,類別規格必須以產生的 MyClassKt
類別為目標:
-keep class com.example.myapp.utils.MyClassKt {
public static boolean isEmailValid(java.lang.String);
}
類型
本節說明如何在保留規則成員規格中指定回傳型別、參數型別和欄位型別。如果型別與 Kotlin 原始碼不同,請記得使用產生的 Java 名稱指定型別。
原始型別
如要指定基本型別,請使用 Java 關鍵字。R8 可辨識下列基本型別:boolean
、byte
、short
、char
、int
、long
、float
、double
。
以下是含有原始型別的規則範例:
# Keeps a method that takes an int and a float as parameters.
-keepclassmembers class com.example.Calculator {
public void setValues(int, float);
}
一般類型
編譯期間,Kotlin/Java 編譯器會清除泛型型別資訊,因此在編寫涉及泛型型別的保留規則時,您必須以程式碼的編譯表示法為目標,而非原始原始碼。如要進一步瞭解泛型型別的變更方式,請參閱「型別抹除」。
舉例來說,如果您有下列程式碼,且 Box.kt
中定義了無界泛型型別:
package com.myapp.data
class Box<T>(val item: T) {
fun getItem(): T {
return item
}
}
型別抹除後,T
會替換為 Object
。如要保留類別建構函式和方法,規則必須使用 java.lang.Object
,取代泛型 T
。
保留規則範例如下:
# Keep the constructor and methods of the Box class.
-keep class com.myapp.data.Box {
public init(java.lang.Object);
public java.lang.Object getItem();
}
如果您在 NumberBox.kt
中有下列程式碼,且具有繫結的泛型型別:
package com.myapp.data
// T is constrained to be a subtype of Number
class NumberBox<T : Number>(val number: T)
在本例中,型別抹除會將 T
替換為其界限 java.lang.Number
。
保留規則範例如下:
-keep class com.myapp.data.NumberBox {
public init(java.lang.Number);
}
使用應用程式專屬泛型做為基底類別時,也必須加入基底類別的保留規則。
舉例來說,針對下列程式碼:
package com.myapp.data
data class UnpackOptions(val useHighPriority: Boolean)
// The generic Box class with UnpackOptions as the bounded type
class Box<T: UnpackOptions>(val item: T) {
}
您可以使用保留規則搭配 includedescriptorclasses
,透過單一規則保留 UnpackOptions
類別和 Box
類別方法,如下所示:
-keep,includedescriptorclasses class com.myapp.data.Box {
public <init>(com.myapp.data.UnpackOptions);
}
如要保留處理物件清單的特定函式,您必須編寫與函式簽章完全相符的規則。請注意,由於泛型型別會遭到清除,因此 List<Product>
這類參數會視為 java.util.List
。
舉例來說,假設您有一個公用程式類別,其中包含可處理 Product
物件清單的函式,如下所示:
package com.myapp.utils
import com.myapp.data.Product
import android.util.Log
class DataProcessor {
// This is the function we want to keep
fun processProducts(products: List<Product>) {
Log.d("DataProcessor", "Processing ${products.size} products.")
// Business logic ...
}
}
// The data class used in the list (from the previous example)
package com.myapp.data
data class Product(val id: String, val name: String)
您可以透過下列保留規則,只保護 processProducts
函式:
-keep class com.myapp.utils.DataProcessor {
public void processProducts(java.util.List);
}
陣列類型
如要指定陣列型別,請在陣列每個維度的元件型別後方附加 []
。這適用於類別型別和原始型別。
- 一維類別陣列:
java.lang.String[]
- 二維原始陣列:
int[][]
舉例來說,如果您有以下程式碼:
package com.example.data
class ImageProcessor {
fun process(): ByteArray {
// process image to return a byte array
}
}
您可以採用下列保留規則:
# Keeps a method that returns a byte array.
-keepclassmembers class com.example.data.ImageProcessor {
public byte[] process();
}
萬用字元
下表說明如何使用萬用字元,將保留規則套用至符合特定模式的多個類別或成員。
萬用字元 | 適用於課程或成員 | 說明 |
---|---|---|
** | 兩者皆有 | 最常用的選項。比對任何型別名稱,包括任意數量的套件分隔符。這項功能有助於比對套件及其子套件中的所有類別。 |
* | 兩者皆有 | 如果是類別規格,則會比對不含套件分隔符 (. ) 的任何型別名稱部分 如果是成員規格,則會比對任何方法或欄位名稱。單獨使用時,這個函式也是 ** 的別名。 |
? | 兩者皆有 | 比對類別或成員名稱中的任何單一字元。 |
*** | 會員 | 比對任何型別,包括原始型別 (例如 int )、類別型別 (例如 java.lang.String ),以及任何維度的陣列型別 (例如 byte[][] )。 |
... | 會員 | 比對方法的所有參數清單。 |
% | 會員 | 與任何基本型別 (例如 `int`、`float`、`boolean` 或其他型別) 相符。 |
以下列舉幾個使用特殊萬用字元的範例:
如果您有多個名稱相同的方法,但輸入的原始型別不同,可以使用
%
編寫保留規則,將這些方法全部保留。舉例來說,這個DataStore
類別有多個setValue
方法:class DataStore { fun setValue(key: String, value: Int) { ... } fun setValue(key: String, value: Boolean) { ... } fun setValue(key: String, value: Float) { ... } }
下列保留規則會保留所有方法:
-keep class com.example.DataStore { public void setValue(java.lang.String, %); }
如果有多個類別的名稱只差一個字元,請使用
?
編寫保留規則,將所有類別保留下來。舉例來說,如果您有下列類別:com.example.models.UserV1 {...} com.example.models.UserV2 {...} com.example.models.UserV3 {...}
下列保留規則會保留所有類別:
-keep class com.example.models.UserV?
如要比對
Example
和AnotherExample
類別 (如果這些是根層級類別),但不要比對com.foo.Example
,請使用下列保留規則:-keep class *Example
如果單獨使用 *,則會做為 ** 的別名。舉例來說,下列保留規則是等效的:
-keepclasseswithmembers class * { public static void main(java.lang.String[];) } -keepclasseswithmembers class ** { public static void main(java.lang.String[];) }
檢查產生的 Java 名稱
編寫保留規則時,您必須使用類別和其他參照類型在編譯為 Java 位元組碼後的名稱 (請參閱「類別規格」和「型別」中的範例)。如要查看程式碼產生的 Java 名稱,請在 Android Studio 中使用下列任一工具:
- APK 分析工具
- 開啟 Kotlin 來源檔案,然後依序前往「Tools」>「Kotlin」>「Show Kotlin Bytecode」>「Decompile」,即可檢查位元碼。