新增保留規則

從高階層面來看,保留規則會指定類別 (或子類別或實作),以及要保留的該類別成員 (方法、建構函式或欄位)。

保留規則的一般語法如下,但特定保留選項不接受選用的 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();
}

保留選項

保留選項是保留規則的第一部分。這項註解會指定要保留類別的哪些層面。共有六種保留選項,分別是 keepkeepclassmemberskeepclasseswithmemberskeepnameskeepclassmembernameskeepclasseswithmembernames

下表說明這些保留選項:

保留選項 說明
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 在最佳化程序期間,變更 (通常是擴大) 類別、方法和欄位的存取修飾符 (publicprivateprotected)。
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 { *; }

子類別和實作項目

如要指定實作介面的子類別或類別,請分別使用 extendimplements

舉例來說,如果您有類別 Bar,且子類別為 Foo,如下所示:

class Foo : Bar()

下列保留規則會保留 Bar 的所有子類別。請注意,保留規則不包含超類別 Bar 本身。

-keep class * extends Bar

如果您有實作 BarFoo 類別 Foo

class Foo : Bar

下列保留規則會保留實作 Bar 的所有類別。請注意,保留規則不包含 Bar 介面本身。

-keep class * implements Bar

存取修飾符

您可以指定 publicprivatestaticfinal 等存取修飾符,讓保留規則更加精確。

舉例來說,下列規則會保留 api 套件及其子套件中的所有 public 類別,以及這些類別中的所有公開和受保護成員。

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

您也可以使用類別中成員的修飾符。舉例來說,下列規則只會保留 Utils 類別的 public static 方法:

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

Kotlin 專屬修飾符

R8 不支援 Kotlin 專屬修飾符,例如 internalsuspend。請按照下列規範保留這類欄位。

  • 如要保留 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);

舉例來說,下列保留規則會保留採用 ContextAttributeSet 的自訂 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 可辨識下列基本型別:booleanbyteshortcharintlongfloatdouble

以下是含有原始型別的規則範例:

# 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?
    
  • 如要比對 ExampleAnotherExample 類別 (如果這些是根層級類別),但不要比對 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」,即可檢查位元碼。