使用 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 控制摇树优化,以移除无法访问的类、字段和方法。对于 android_app 目标,默认值为 true(对于独立 java_library 和测试模块,默认值为 false,除非明确设置,否则会传递 -dontshrink)。
optimize 控制字节码优化,例如方法内联、类合并、常量传播和死分支移除。对于 android_app 目标,默认为 true(由 RELEASE_R8_OPTIMIZE_BY_DEFAULT 发布 build 标志控制)。
obfuscate 控制标识符缩减大小和重命名。默认为 false,以实现历史兼容性,但请尽可能将其设置为 true,以缩减 DEX 大小并实现更深层次的优化(请参阅尽可能启用混淆)。
shrink_resources 在代码缩减后,从打包的 APK 中移除未使用的资源(res/ 条目)。默认值为 false。
optimized_shrink_resources 运行集成的 R8 代码和资源缩减器流水线,以便 R8 在单次传递中联合跟踪代码和资源引用。设置 shrink_resources: true 后,默认值为 RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT(在标准平台 build 中为 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 keep 规则,并将其附加到所有已启用的 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 上自动启用此功能,以便使用 R8 优化链接 Jetpack Compose 的目标。

尽可能启用混淆功能

在 Soong 中,为了实现历史兼容性,obfuscate 默认为 false,但您应尽可能在 android_app 和独立 DEX 目标上明确设置 obfuscate: true:

  • 更小的 DEX 和 .vdex 占用空间:将软件包、类、字段和方法重命名为简短标识符(a、b)可缩减 DEX 字符串池(string_ids 和 string_data_item)和类型描述符。由于 Android 会将 .vdex 和 .odex 文件映射到进程内存中,因此较小的符号表会直接缩减系统分区大小和运行时内存占用。
  • 更深入的全程序优化:让 R8 重命名标识符可解锁类合并、软件包扁平化和跨软件包的合成类去重,否则 R8 必须跳过这些操作以避免名称冲突。
  • 完整的堆栈轨迹符号化解析:在 Soong 中启用混淆处理不会降低可调试性。对于每个 R8 编译的目标,Soong 都会在模块的中间目录中发出一个 proguard_dictionary 映射文件,将所有模块字典捆绑到 build 的 proguard-dict.zip 工件中,将映射哈希 (--map-id-template) 嵌入到 DEX 标头中,并重写类 SourceFile 属性 (--source-file-template),以便 retrace 可以自动对堆栈轨迹进行符号化。

仅针对以下情况保留 obfuscate: false(或明确保留公共 API 名称):

  • bootclasspath 或 system_server 类路径上的共享库: 如果模块(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 步:迁移测试驱动的 keep 规则),以便在混淆应用的其余部分时保留测试所需的内部符号。
  • 基于字符串的反射和 JNI:如果模块通过字面量字符串名称(Class.forName、getDeclaredMethod 或 JNI FindClass 和 GetMethodID)查找类、方法或字段,而没有保留规则或注解,混淆处理会重命名这些目标并破坏运行时查找。(请注意,未加注解的反射在 shrink: true 和 optimize: true 下也会失败。)使用保留注解指南(@UsesReflection、@UsedByReflection 或 @UsedByNative)为这些入口点添加注解,以便 R8 保留其名称并对模块的其余部分进行混淆处理。

遵循零自定义标志原则

理想的 Android 平台模块没有自定义 proguard.flags 或 keep.xml 文件。在 Android 平台 build 中,绝大多数自定义保留规则都是多余的:

  • 平台基准和 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})忽略无害警告,并在发布 build 中剥离 Kotlin DebugMetadata 注解。
  • build/make/core/proguard/checknotnull.flags:将常见的 null 检查辅助调用(com.google.common.base.Preconditions.checkNotNull 和 dagger.internal.Preconditions.checkNotNull*)替换为简洁的字节码 null 检查。此文件有意省略了 Objects.requireNonNull,以便在框架 API 边界之间保留显式异常消息。
  • build/make/core/proguard/enumvalues.flags:保留 enum 类型上的 values 和 valueOf 方法,除非 build 配置停用了这些方法。
  • 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 设置 build 文件的格式。清理软件包时,请检查子目录中的配套模块(例如 Kotlin 目标变体)是否引用了相同的标志文件,并一并更新它们。

第 2 步:迁移测试驱动的 keep 规则

使用以下模式之一将测试驱动的保留规则移出自定义生产 .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 子类,包括未使用的库组件和已停用的调试 activity。
  • 建议的解决方法:删除相应规则。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 保留应用和所有关联库(包括 AndroidX 和 Material 库)中每个 View 子类的每个 getter 和 setter,从而阻止移除死方法和内联界面代码。
  • 建议的解决方法:删除相应规则。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
  • 为什么会影响性能:将 -dontoptimize 或 -dontshrink 放在 .flags 文件中会静默替换模块的 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
  • 为何会影响 build:诊断标志会使 build 日志泛滥,或在沙盒化 Soong build 期间尝试将输出文件写入本地路径。
  • 建议的解决方法:从已提交的 .flags 文件中删除这些标志。Soong 会自动将 R8 映射和使用情况输出(例如 proguard_dictionary 和 proguard_usage.zip)写入 out/soong/.intermediates/ 下的模块中间目录。

测试代码的生产环境保留规则

  • 为何会影响性能:仅为测试访问权限添加自定义 -keep 规则会强制该代码在正式版 build 中保持未混淆状态并保留。虽然配置 trace_references_from(或将 allow_traced_references 与 trace_references: true 搭配使用)或使用 @VisibleForTesting 注释方法可避免维护手动 -keep 规则,并仅保留测试引用的内容,但这些符号仍会在正式版二进制文件中提供。
  • 建议的解决方法:通过启用 trace_references_from(或 allow_traced_references 和 trace_references: true)来消除自定义测试 -keep 规则,或者将应用构建为使用 static_libs 关联到自插桩测试 APK 的 .impl 库,以使仅限测试的入口点完全不出现在正式版 APK 中,如第 2 步:迁移测试驱动的 keep 规则中所述。

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 中列出确切的资源标识符(例如 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. 对于系统应用、特权服务或依赖于硬件的模块,请在目标实体设备上或在设备测试实验室中运行插桩测试和界面测试,以执行运行时反射路径、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 验证 build 是否会剥离未使用的资源并保留所需的资源:

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • 直接在模块的 R8 前 classes.jar 上运行 prebuilts/r8/r8.jar 以在不进行完整平台 build 的情况下离线验证标志更改时,请同时传递模块的 --lib 类路径 JAR(android.jar、framework.jar 和共享 libs)以及 AAPT2 从 AndroidManifest.xml 和布局 XML 生成的 proguard.options 规则。如果没有这些输入,R8 将无法解析框架超类型(Activity、Service、Phone、Handler),并且会保留过少的层次结构方法。

使用 R8 分析保留半径和规则包含关系

开源 R8 编译器包含一个保留半径分析器(在 Android Studio 和 Gradle 中也称为 R8 配置分析器),用于在编译期间精确衡量每条保留规则的影响。Soong 会将此分析器直接集成到 Android 平台 build 中(在 build/soong/java/dex.go 中配置)。

如需分析平台模块或整个 build 的保留规则,请执行以下操作:

  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,用于汇总整个 build 中的活跃项计数、保留项计数和最高半径保留规则。如需详细了解如何解读生成的报告中的缩减、优化和混淆处理得分,请参阅使用 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 平台 build 中作为 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 结合使用来衡量 build 中每条规则的确切类、字段和方法保留半径。