使用 R8 最佳化平台程式碼和資源

Android 平台建構系統 (Soong) 會在編譯 DEX 位元碼的目標上執行 R8 編譯器,包括 Android 應用程式 (android_app)、測試 (android_test),以及具有 compile_dex: true 的可安裝 Java 程式庫 (例如 services.jar),以縮減、最佳化及移除未使用的程式碼和資源。在靜態資料庫 (android_library 和靜態 java_library) 上,Soong 不會直接執行 R8,而是使用 optimize 屬性區塊,將消費者保留規則 (export_proguard_flags_files: true) 附加並傳播至靜態連結這些規則的下游 DEX 目標。

對於建構系統映像檔或供應商套件的平台工程師來說,消除過於廣泛的保留規則可直接提升系統健康度:

  • 系統分區體積更小:R8 會先移除無效類別、方法和資源,再將 APK 和 JAR 封裝至 /system、/system_ext、/product 和 /vendor。
  • 編譯成果較小:DEX 方法較少,代表 dex2oat 在建構期間或裝置端編譯時產生的 .odex 和 .vdex 檔案較小。
  • 降低執行階段記憶體壓力:由於 Android 使用隨選分頁 mmap 呼叫,將 .odex、.vdex 和 APK 資源表對應至程序記憶體,因此較小的二進位檔可減少應用程式啟動期間的主要分頁錯誤,並降低各程序中的常駐程式碼和資源記憶體。如要進一步瞭解編譯的應用程式程式碼對裝置記憶體的影響,請參閱「應用程式程式碼就是記憶體」。
  • 更有效的全程式最佳化:縮小保留限制可讓 R8 內嵌方法、去虛擬化介面和虛擬呼叫、刪除未使用的欄位,以及跨類別傳播常數。

在 Soong 中設定 R8 最佳化

在 Android.bp 檔案中,使用 android_app 和可安裝 java_library 目標 (或 android_library 和靜態 java_library 模組,用於匯出消費者保留規則) 上的 optimize 屬性區塊,設定 R8 最佳化:

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

下表匯總了 Soong 中最常見的 optimize 屬性 (定義於 build/soong/java/dex.go):

屬性 說明
enabled 控管是否要在目標上執行 R8。所有 android_app 目標預設為 true。
shrink 控制 tree shaking (移除沒用到的程式碼),移除無法連線的類別、欄位和方法。android_app 目標預設為 true (獨立 java_library 和測試模組為 false,除非明確設定,否則會傳遞 -dontshrink)。
optimize 控管位元碼最佳化,例如方法內嵌、類別合併、常數傳播和無效分支移除。android_app 目標預設為 true (由 RELEASE_R8_OPTIMIZE_BY_DEFAULT 發布子版本旗標控制)。
obfuscate 控管 ID 壓縮和重新命名。為了與舊版相容,預設值為 false,但請盡可能設為 true,以縮減 DEX 大小並進行更深入的最佳化 (請參閱「盡可能啟用模糊處理」)。
shrink_resources 在程式碼縮減後,從封裝的 APK 中移除未使用的資源 (res/ 項目)。預設為 false。
optimized_shrink_resources 執行整合式 R8 程式碼和資源縮減器管道,讓 R8 在單一階段中同時追蹤程式碼和資源參照。設定 shrink_resources: true 時,預設為 RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (標準平台建構作業中為 true)。
proguard_flags_files 列出包含自訂保留規則的模組專屬 .flags 或 .pro 檔案。如果標準註解或預設值已足夠,請避免新增自訂檔案。
export_proguard_flags_files 將這個程式庫的 proguard_flags_files 傳播至靜態依附於該程式庫的下游模組。
trace_references_from 列出直接 DEX 編譯依附目標 (透過 instrumentation_for 或 libs 連結),其位元碼參照至這個目標 R8 會自動追蹤並保留 (也會在 override_android_app 和 override_android_test 變體中傳播,並在與 .impl 分割配對時,支援非依附隨附 Java 程式庫)。
allow_traced_references 允許直接 DEX 編譯依附目標 (透過 instrumentation_for 或 libs 連結,例如無法列在 trace_references_from 中的跨存放區或供應商測試),透過設定 trace_references: true,選擇加入將其位元碼參照追蹤至此目標。
trace_references 如果目標具有 allow_traced_references: true,且其直接 DEX 編譯依附元件設為 true,則會在這個模組上執行 R8 tracereferences,並將產生的保留規則傳遞至目標模組。
protect_api_surface 將 --protect-api-surface 傳遞至 R8。在 java_sdk_library 上設定時,也會自動產生 Metalava API 保留規則,並將規則附加至所有已啟用 API 範圍 (公開、系統、module-lib、system-server 和測試) 的實作程式庫。

靜態資料庫 (android_library、java_library 和 AAR 匯入) 也可以在 Android.bp 中設定 required_downstream_optimization: { enabled: true, optimize: true },要求任何以靜態方式連結 (直接或間接) 的下游 DEX 編譯目標啟用 R8 縮減和位元碼最佳化。Soong 會在 androidx.compose.runtime_runtime 上自動啟用這項功能,因此連結 Jetpack Compose 的目標會使用 R8 進行最佳化。

盡可能啟用模糊處理

在 Soong 中,obfuscate 預設為 false,以確保與舊版相容,但您應盡可能在 android_app 和獨立 DEX 目標上明確設定 obfuscate: true:

  • 縮減 DEX 和 .vdex 體積:將套件、類別、欄位和方法重新命名為簡短 ID (a、b),可縮減 DEX 字串集區 (string_ids 和 string_data_item) 和類型描述元。由於 Android 會將 .vdex 和 .odex 檔案對應至程序記憶體,因此較小的符號表會直接減少系統分區大小和執行階段記憶體用量。
  • 更深入的最佳化整個程式:讓 R8 重新命名 ID,即可在 R8 必須略過以避免名稱衝突的套件中,合併類別、扁平化套件,以及簡化合成類別。
  • 完整堆疊追蹤符號化:在 Soong 中啟用模糊處理功能不會降低偵錯能力。針對每個 R8 編譯目標,Soong 會在模組的中繼目錄中發出 proguard_dictionary 對應檔案、將所有模組字典彙整至建構作業的 proguard-dict.zip 構件、將對應雜湊 (--map-id-template) 嵌入 DEX 標頭,並重新編寫類別 SourceFile 屬性 (--source-file-template),讓 retrace 自動符號化堆疊追蹤。

僅在下列情況下保留 obfuscate: false (或明確保留公開 API 名稱):

  • bootclasspath 或 system_server classpath 上的共用程式庫: 如果模組 (java_library 或 java_sdk_library) 公開的外部 API 介面在執行階段由其他模組動態連結 (例如 framework.jar、services.jar 或 <uses-library> 目標),則必須保留 obfuscate: false,或使用 protect_api_surface: true、@KeepForApi 或 -keep 規則明確保留其 API 介面,以便針對其虛設常式編譯的呼叫端在執行階段解析類別和成員名稱。在 java_sdk_library 模組上,設定 protect_api_surface: true 會自動產生 Metalava ProGuard 保留規則,並將規則附加至所有已啟用的 API 範圍 (公開、系統、module-lib、system-server 和測試) 的實作程式庫。在不可更新的 bootclasspath 目標 (framework-minus-apex、ext 和 framework-location) 上啟用一般 R8 最佳化功能時,這些目標也會包含 framework-hiddenapi-proguard-rules (由 hiddenapi_to_proguard 產生),以保留有效 boot/hiddenapi/*.txt 檔案中列出的成員 (省略 hiddenapi-max-target-o.txt)。

在應用程式上啟用 obfuscate: true 之前,請先確認下列遷移必要條件:

  • 外部測試 APK 依附元件:如果外部 android_test APK 設定 instrumentation_for: "MyApp",並直接叫用 MyApp 的內部類別或方法,重新命名這些符號會導致測試執行時發生 NoSuchMethodError 或 NoClassDefFoundError。設定 trace_references_from (或 allow_traced_references 搭配 trace_references: true),將測試結構化為靜態連結 MyApp.impl 的自我檢測 APK,或使用 @VisibleForTesting 註解測試掛鉤 (請參閱「步驟 2:遷移測試導向的保留規則」),以便保留測試所需的內部符號,同時對應用程式的其他部分進行模糊處理。
  • 字串式反射和 JNI:如果模組依常值字串名稱 (Class.forName、getDeclaredMethod 或 JNI FindClass 和 GetMethodID) 查詢類別、方法或欄位,但沒有保留規則或註解,則模糊處理會重新命名這些目標,並中斷執行階段查詢。(請注意,未加上註解的反射在 shrink: true 和 optimize: true 下也會失敗)。請使用保留註解指南 (@UsesReflection、@UsedByReflection 或 @UsedByNative) 為這些進入點加上註解,讓 R8 保留其名稱,並對模組的其他部分進行模糊處理。

遵循零自訂標記原則

理想的 Android 平台模組沒有自訂 proguard.flags 或 keep.xml 檔案。在 Android 平台建構中,絕大多數自訂保留規則都是多餘的:

  • 平台基準和 AAPT2:大多數進入點已由全域平台基準 (例如 @Keep、JNI native 方法和 @VisibleForTesting) 自動保留,或由 AAPT2 從 AndroidManifest.xml 和版面配置資源產生 (請參閱「瞭解預設平台保留規則」)。
  • 指定替代方案:如果必須保留進入點,建議使用宣告網站註解 (keepanno、@VisibleForTesting)、匯出的程式庫規則 (export_proguard_flags_files: true) 或靜態測試連結,而非分離的 .flags 檔案 (請參閱「稽核及遷移現有的保留規則」)。

新增或保留自訂 proguard.flags 或 keep.xml 檔案前,請先確認預設基準是否已處理該規則,或是否可遷移至程式碼註解。

瞭解預設平台保留規則

Soong 會自動將下列全域基準規則傳遞至 DEX 編譯目標 (android_app、android_test 和可安裝的 java_library 模組) 上的每個 R8 叫用,並在 build/soong/java/dex.go 中設定:

  • build/make/core/proguard.flags:保留所有套件中以 @com.android.internal.annotations.VisibleForTesting 註解的類別和成員,以及 android.**、com.android.** 和 com.google.android.** 套件中以 @VisibleForTesting (androidx.annotation.VisibleForTesting 或 com.google.common.annotations.VisibleForTesting) 註解的類別和成員。此外,也會保留 @TestApi、@Keep (androidx.annotation、android.support.annotation、com.android.internal.annotations)、@KeepForWeakReference、@WeaklyReferencedCallback 和 @dalvik.annotation.optimization.**。
  • build/make/core/proguard_basic_keeps.flags:保留堆疊追蹤、執行階段瀏覽權限註解 (RuntimeVisible*Annotations)、Exceptions 和 AnnotationDefault 屬性、native 方法、Serializable 成員、@JavascriptInterface 方法、Throwable(String) 建構函式、Parcelable$CREATOR 欄位和 protobuf MessageLite 欄位的 SourceFile 屬性。
  • build/make/core/proguard/kotlin.flags:針對特定 Kotlin 中繼註解 (kotlin.Metadata 和 kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) 隱藏無害警告,並在發布子版本中移除 Kotlin DebugMetadata 註解。
  • build/make/core/proguard/checknotnull.flags:將常見的空值檢查輔助呼叫 (com.google.common.base.Preconditions.checkNotNull 和 dagger.internal.Preconditions.checkNotNull*) 替換為簡潔的位元碼空值檢查。這個檔案刻意省略 Objects.requireNonNull,以便在架構 API 邊界保留明確的例外狀況訊息。
  • build/make/core/proguard/enumvalues.flags:保留 enum 類型的 values 和 valueOf 方法,除非建構設定停用。
  • AAPT2 自動產生的規則:AAPT2 會檢查合併的 AndroidManifest.xml、版面配置 XML 和偏好設定 XML 檔案,為每個已註冊的 Activity、Service、BroadcastReceiver、ContentProvider、BackupAgent、Application、自訂 View 子類別、Preference 子類別和 XML android:onClick 方法產生確切的保留規則。

稽核及遷移現有的保留規則

稽核平台存放區中現有的 proguard.flags 或 keep.xml 檔案時,請按照下列四個步驟的階層,依序評估每項規則:

步驟 1:刪除多餘或過時的規則

刪除已由全域基準、AAPT2 資訊清單和版面配置規則或 R8 預設值涵蓋的規則,以及參照已不存在類別或套件的規則。

如果 .flags 檔案中的所有規則都是多餘的,請刪除該檔案,並從 Android.bp 中移除 proguard_flags_files。如果剩餘的 optimize 區塊只是重述預設設定,請移除多餘的區塊,並使用 bpfmt -w Android.bp 設定建構檔案格式。清理套件時,請檢查子目錄中的隨附模組 (例如 Kotlin 目標變體) 是否參照相同的旗標檔案,並一併更新。

步驟 2:遷移測試導向的保留規則

使用下列其中一種模式,將測試導向的保留規則移出自訂實際工作環境 .flags 檔案:

  • 追蹤測試或隨附目標 (trace_references_from 或 allow_traced_references) 的參照:當目標和直接依附模組都編譯自己的 DEX (android_app、android_test 或 dexed java_library),並透過 instrumentation_for 或 libs 連結時,Soong 可以在依附模組上執行 R8 tracereferences,並將產生的保留規則提供給目標的 R8 編譯,而不會建立依附元件循環,或需要手動分割 .impl 程式庫:

    • 相同存放區測試 (trace_references_from):列出目標模組的直接 instrumentation_for 或 libs 依附元件 (這也會自動在 override_android_app 和 override_android_test 變化版本之間傳播,並在與 .impl 分割配對時,繼續支援非依附元件的隨附 Java 程式庫):

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              obfuscate: true,
              shrink_resources: true,
              trace_references_from: ["MyAppTests"],
          },
      }
      
      android_test {
          name: "MyAppTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
      }
      
    • 跨存放區或供應商測試 (allow_traced_references 和 trace_references):如果目標模組無法直接在 trace_references_from 中列出依附元件 (例如其他存放區或供應商樹狀結構中的測試),請在目標模組上設定 allow_traced_references: true,並使用 trace_references: true 從每個依附元件選擇加入:

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              allow_traced_references: true,
          },
      }
      
      android_test {
          name: "MyAppVendorTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
          optimize: {
              trace_references: true,
          },
      }
      
    • 測試中的字串反射: tracereferences 會檢查位元碼常數集區參照,無法偵測測試中的字串反射 (Class.forName、getDeclaredField、getDeclaredMethod 或反射欄位插入輔助程式)。使用 @VisibleForTesting (或 @UsedByReflection) 為反射存取的正式版欄位和方法加上註解。

  • 將實作程式庫連結至測試 (static_libs):如果您不想在正式版 APK 中保留任何僅限測試的進入點,也不想讓測試變更在正式版目標上重新觸發 R8,請將應用程式來源檔案放在 android_library (MyApp.impl) 中,並將 MyApp.impl 靜態連結至 MyApp 和 MyAppTests:

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    這種 3 模組模式可讓 MyAppTests 做為自我檢測設備測試執行,並具備內部類別的完整存取權,同時允許 MyApp 僅針對正式版進入點進行縮減和模糊處理。

  • 使用 @VisibleForTesting 註解測試掛鉤:如果外部測試 APK 只在 android_app 目標上呼叫少數內部方法或建構函式,您可以使用 @VisibleForTesting 註解這些宣告。全域 build/make/core/proguard.flags 基準會自動保留 android.**、com.android.** 和 com.google.android.** 套件中的 @VisibleForTesting 項目,不需要自訂 .flags 檔案。(對於這些命名空間以外的供應商模組,請使用 trace_references_from、@UsedByReflection 或匯出的程式庫規則)。使用 @VisibleForTesting 手動加上註解,而非使用 trace_references_from 或 trace_references 時,也請為傳遞至 Mockito mock(...) 或 spy(...) 的類別加上註解 (不只是虛設常式方法),因為裝置端 mockito-target (dexmaker-mockito) 會產生執行階段子類別,而 R8 可以將非子類別標示為 final (MockitoException: Can't mock/spy class ... - final class)。

  • 避免或重新封裝重疊的靜態資料庫 (jarjar_rules):當 android_app 及其 instrumentation_for 測試 APK 靜態連結相同的資料庫 (直接連結,例如 guava,或間接連結),tracereferences 會看到測試中定義的類別,且不會為應用程式發出保留規則,而在執行階段,應用程式的父項類別載入器會使用 R8 縮減類別遮蓋測試副本,導致 IllegalAccessError 或 NoSuchMethodError。如果測試不需要自己的套裝組合副本,請從測試的 static_libs 中移除重複項目 (或透過 libs 連結),以免發生衝突;否則,請使用 jarjar_rules,在模組專屬前置字元下重新封裝應用程式的重疊靜態程式庫套件。

步驟 3:從擁有規則的程式庫匯出規則

如果共用 java_library 或 android_library 需要保留規則,用於其內部反射或 JNI 回呼,請在程式庫目標上定義規則,並設定 export_proguard_flags_files: true:

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

所有靜態連結 my-shared-library 的下游 android_app 和程式庫目標都會自動沿用這些規則,因此下游應用程式不需要複製這些規則。如果不同目錄中的多個模組必須共用不與單一程式碼庫繫結的規則集,請透過 Android.bp 中的明確 filegroup 發布 .flags 檔案,讓模組可以跨套件邊界乾淨地參照 :my-shared-flags。如需編寫程式庫消費者保留規則的一般指南,且不限制下游應用程式最佳化,請參閱「程式庫作者最佳化」。

步驟 4:在原始碼中加上宣告註解

如果類別、方法、建構函式或欄位是透過反射或 JNI 存取,且未涵蓋在 AAPT2 或全域基準中,請將 .flags 檔案中分離的 -keep 規則,替換為保留註解指南中的來源註解 (請參閱 keepanno Javadoc 參考資料):

  • @UsesReflection:如果您能控制執行反射的程式碼,建議使用這個註解。請將其放在反射呼叫位置,宣告要動態存取哪些目標類別、方法或欄位。由於 @UsesReflection 會自動編碼註解呼叫網站本身可連線的前提條件,因此如果呼叫網站未使用,R8 會同時刪除呼叫端和反射目標。
  • @UsedByReflection:用於由外部程式碼或程式庫反射例項化或叫用的類別、方法、欄位或建構函式,例如使用 Class.forName 從 Bundle 或 Settings 鍵載入的類別,或跨動態類別載入器載入的外掛程式類別。指定 kind (例如 KeepItemKind.CLASS_AND_METHODS)、preconditions (@KeepCondition) 和參數限制,讓 R8 只保留確切的反射合約,並仍能最佳化或剪除類別中未使用的成員。請勿將 @UsedByReflection 新增至資訊清單註冊的元件 (例如 JobService 或 BroadcastReceiver),因為 AAPT2 已保留這些元件。
  • @UsedByNative:使用 GetMethodID、GetStaticMethodID 或 GetFieldID,在從 C 或 C++ JNI 程式碼存取的方法或欄位上放置。
  • @KeepForApi:如果程式庫本身在發布前會縮減,請將此註解放在必須保持完整的程式庫 API 類別或成員上。
  • @Keep (androidx.annotation.Keep):如果 keepanno 不適用,請使用這個方法做為備用方案。將 @Keep 套用至類別,會無條件保留該類別及其所有成員。將 @Keep 套用至方法或欄位,可做為無條件進入點,即使類別從未例項化,也能保留成員及其包含的類別,而 keepanno 則表示條件式可連線。

如要在 Soong 模組中使用 R8 keepanno 註解,請按照下列步驟操作:

  1. 在 Android.bp 中,將 "keepanno-annotations" 新增至 libs:

    libs: [
        "keepanno-annotations",
    ],
    
  2. 匯入 com.android.tools.r8.keepanno.annotations.*,並在 Java 或 Kotlin 原始碼中為宣告或呼叫網站加上註解:

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    R8 會直接將 keepanno 註解轉譯為內部保留規則模型,並從最終 DEX 輸出中移除註解,不會增加執行階段位元碼負擔。

避免常見錯誤

以下各節說明平台程式碼中常見的 proguard.flags 和 keep.xml 陷阱,以及修正方式。

廣泛元件保留規則

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • 對效能的影響:這項規則與 AAPT2 完全重複。由於它使用萬用字元,而不檢查元件是否已在最終合併的 AndroidManifest.xml 中註冊,因此會強制 R8 保留類別路徑上找到的所有 Activity、Service 或 BroadcastReceiver 子類別,包括未使用的程式庫元件和停用的偵錯活動。
  • 建議修正方式:刪除規則。AAPT2 會檢查合併的資訊清單,並為已註冊的元件產生精確的 -keep 規則。

廣泛 XML 點擊處理常式保留規則

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • 效能受影響的原因:保留模組中每個類別的所有 public void *(View) 方法,會導致 R8 無法移除或內嵌任何剛好接受單一 View 參數的方法。
  • 建議修正方式:刪除規則。AAPT2 會掃描版面配置 XML 檔案,並針對 android:onClick 屬性參照的方法,產生指定的保留規則。

廣泛 View getter 和 setter 保留規則

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • 對效能的影響:這項規則會強制 R8 保留應用程式中每個 View 子類別和所有連結程式庫 (包括 AndroidX 和 Material 程式庫) 的每個 getter 和 setter,導致無法移除無效方法,也無法在 UI 程式碼中內嵌。
  • 建議修正方式:刪除規則。AAPT2 已保留從版面配置 XML 檔案加載的自訂 View 類別建構函式。如果程式碼使用反射字串名稱 (例如 ObjectAnimator.ofFloat(view, "translationZ", ...)) 為檢視畫面屬性建立動畫,請將字串名稱替換為類型化屬性參照 (View.TRANSLATION_Z 或自訂 FloatProperty 或 IntProperty 實作),完全避免反射。如果無法避免反射屬性存取,請使用 @UsedByReflection 註解特定 getter 或 setter。

全面最佳化或模糊處理會停用

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • 對效能造成負面影響的原因:在 .flags 檔案中放置 -dontoptimize 或 -dontshrink 會在不顯示任何訊息的情況下覆寫模組的 Android.bp 設定,並停用整個目標的最佳化傳遞。更糟的是,如果程式庫匯出含有 -dontoptimize 的 .flags 檔案,則會停用連結該程式庫的每個下游 android_app 的 R8 最佳化功能。
  • 建議修正方式:從 .flags 檔案中移除 -dontoptimize、-dontshrink 和 -dontobfuscate。在葉目標上使用 optimize 區塊 (shrink、optimize、obfuscate),在 Android.bp 中明確控制最佳化行為。

已提交規則中的診斷標記

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • 對建構作業的影響:在沙箱 Soong 建構作業中,診斷標記會填滿建構記錄,或嘗試將輸出檔案寫入本機路徑。
  • 建議修正方式:從已修訂的 .flags 檔案中刪除這些標記。Soong 會自動將 R8 對應和使用輸出內容 (例如 proguard_dictionary 和 proguard_usage.zip) 寫入 out/soong/.intermediates/ 下方的模組中繼目錄。

測試程式碼的正式環境保留規則

  • 對效能的影響:如果只為了測試存取權而新增自訂 -keep 規則,會導致程式碼無法經過模糊處理,並保留在正式版建構中。雖然設定 trace_references_from (或 allow_traced_references 搭配 trace_references: true) 或使用 @VisibleForTesting 註解方法可避免維護手動 -keep 規則,並僅保留測試參照的內容,但這些符號仍會隨附在正式版二進位檔中。
  • 建議修正方式:啟用 trace_references_from (或 allow_traced_references 和 trace_references: true),消除自訂測試 -keep 規則,或將應用程式結構化為 .impl 程式庫,並使用 static_libs 連結至自我檢測測試 APK,將僅限測試的進入點完全排除在正式版 APK 之外,如「步驟 2:遷移測試導向的保留規則」所述。

keep.xml 中的綜合資源萬用字元

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • 對效能的影響:萬用字元會保留模組及其依附元件中相符類型的所有資源,導致資源縮減 (shrink_resources: true) 功能失效,並使 APK 和對應的 resources.arsc 表格膨脹。
  • 建議修正方式:
    • 建議使用靜態資源參照,而非 keep.xml:請避免使用 Resources.getIdentifier 方法動態查詢一組有界限的資源,例如編號實驗字串或主題可繪項目。請改用編譯時間 switch 陳述式,或對應靜態 R.id、R.string 或 R.drawable 常數。靜態參照可讓 R8 和 AAPT2 追蹤確切的即時資源、消除執行階段字串查詢負擔,並完全不需要 keep.xml。
    • 列出特定資源名稱:如果需要動態查詢 (例如由第三方授權讀取器執行),請在 tools:keep 中列出確切的資源 ID (例如 tools:keep="@raw/third_party_licenses")。
    • 刪除多餘的 keep.xml 檔案:如果列出的資源已在程式碼 (R.raw.foo) 或 XML (@raw/foo) 中以靜態方式參照,縮減器會自動保留這些資源,因此您可以刪除 res/raw/keep.xml。

驗證及稽核規則變更

每當您在 proguard.flags 或 keep.xml 中移除或縮小保留規則時,請確認模組建構作業順利完成、通過單元和檢測設備測試,並保留所有必要的進入點。

建構及測試模組

  1. 完整建構模組,確認 R8 和 AAPT2 完成建構,且沒有遺漏參照警告:

    m <MODULE_NAME>
    
  2. 使用 atest 執行模組的單元和檢測設備測試:

    atest <TEST_MODULE_NAME>
    
  3. 如果是系統應用程式、特殊權限服務或硬體依附模組,請在目標實體裝置或裝置測試實驗室中執行檢測和 UI 測試,以便執行執行階段反射路徑、IPC 繫結和資源膨脹。

檢查 DEX 和資源差異

比較規則變更前後編譯的 APK 或 JAR,確認 R8 移除無效程式碼和資源,但未移除預期的進入點:

  • 使用 apkanalyzer 或 dexdump 檢查輸出 APK 或 DEX 檔案中保留的類別、方法和欄位:

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • 修改 keep.xml 或啟用 shrink_resources 時,請使用 aapt2 dump resources 驗證建構作業是否會移除未使用的資源,並保留必要的資源:

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • 如要直接在模組的 R8 前 classes.jar 上執行 prebuilts/r8/r8.jar,以便在不建構完整平台的情況下,離線驗證旗標變更,請傳遞模組的 --lib 類別路徑 JAR (android.jar、framework.jar 和共用 libs),以及 AAPT2 從 AndroidManifest.xml 和版面配置 XML 產生的 proguard.options 規則。如果沒有這些輸入內容,R8 就無法解析架構超類型 (Activity、Service、Phone、Handler),且會保留過少的階層方法。

使用 R8 分析保留半徑和規則歸類

開放原始碼 R8 編譯器包含 Keep Radius 分析器 (在 Android Studio 和 Gradle 中也稱為 R8 設定分析器),可測量編譯期間每項保留規則的確切影響。Soong 會將這個分析器直接整合至 Android 平台建構作業 (在 build/soong/java/dex.go 中設定)。

如要分析平台模組或整個建構作業的保留規則,請按照下列步驟操作:

  1. 使用 R8_DUMP_KEEP_RADIUS=true 環境變數執行建構作業:

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    設定 R8_DUMP_KEEP_RADIUS=true 後,Soong 會指示 R8 將保留規則指標記錄在 out/soong/.intermediates/ 下每個已編譯模組的中繼 r8keepradius.pb 檔案中。針對每項保留規則和 keepanno 註解,R8 會記錄:

    • 立即保留半徑:規則保留的確切類別、欄位和方法,以及規則對縮減、最佳化或模糊處理強制執行的特定限制。
    • 規則歸類:哪些其他保留規則或註解已保留相同項目。如果自訂規則完全由 AAPT2 規則或全域基準規則涵蓋,則可放心刪除。
    • 套件範圍和全域規則:使用廣泛套件範圍萬用字元或套用全域設定指令的規則。
  2. 使用 KeepRadiusHtmlReportGenerator (隨附於 prebuilts/r8/r8.jar) 將 r8keepradius.pb 輸出內容轉換為互動式 HTML 報表:

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    如果提供目錄,KeepRadiusHtmlReportGenerator 會檢查所有 *keepradius*.pb 檔案,為每個模組產生 HTML 報表,並建立 out/keep_radius_reports/keepradius.html,匯總整個建構作業中的有效項目數量、保留項目數量,以及最高半徑保留規則。如要進一步瞭解如何解讀產生的報表中的縮減、最佳化和模糊處理分數,請參閱「使用 R8 設定分析器」。

Soong 也支援 build/soong/java/dex.go 中的其他 R8 診斷和內部迴圈環境變數:

  • R8_DUMP_INPUT=true:將 r8inputs.zip 寫入模組的中繼目錄,其中包含所有輸入 JAR、程式庫 JAR,以及用於獨立 R8 重現的合併 ProGuard 設定。
  • R8_DUMP_PERFETTO_TRACE=true:將 r8trace.ptrace 寫入模組的中繼目錄,以便在 Perfetto 中檢查 R8 編譯階段。
  • SOONG_SKIP_TRACE_REFERENCES=true:略過執行 trace_references_from 或 trace_references 設定的 tracereferences 規則,在測試程式碼疊代時,加快本機內部迴圈建構作業。

驗證程式庫消費者規則

當 java_library 或 android_library 將保留規則匯出至下游消費者 (export_proguard_flags_files: true) 時,這些規則不得包含會變更或停用消費者應用程式最佳化設定的全域旗標。

R8 提供開放原始碼保留規則分析器 (ProcessKeepRules),在 Android 平台建構中公開為 process-keep-rules 主機工具 (定義於 prebuilts/r8/Android.bp):

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

process-keep-rules 工具會剖析每個設定檔,如果遇到程式庫消費者規則不允許的指令,就會失敗,並顯示檔案和行診斷。不允許的指令包括全域最佳化、縮減或模糊化停用 (例如 -dontoptimize 或 -dontshrink)、套件重新封裝和存取修改標記、診斷或對應標記,以及應用程式層級 -keepattributes。

使用 pgaudit.py 稽核來源樹狀結構

如要稽核未建構的來源目錄,以及 R8 的建構階段分析器,Android 平台樹狀結構會在 build/make/core/proguard/tools/ 下方加入 pgaudit.py 指令碼。這項靜態分析工具會掃描存放區中的 Android.bp、proguard.flags 和 keep.xml 檔案,標記 AAPT2 或全域平台基準已涵蓋的規則、廣泛萬用字元、-dontoptimize 覆寫,以及僅限測試的保留規則:

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

平台和原始設備製造商 (OEM) 團隊可以結合 pgaudit.py 進行快速來源樹狀結構分類、process-keep-rules 驗證匯出的程式庫規則,以及 R8_DUMP_KEEP_RADIUS=true 與 KeepRadiusHtmlReportGenerator 測量建構中每個規則的確切類別、欄位和方法保留半徑。