产品资讯

配置 R8 Keep 规则并排查相关问题

阅读用时:7 分钟

在现代 Android 开发中,提供体积小、速度快且安全的应用是用户的基本期望。Android 构建系统实现此目标的主要工具是 R8  优化器,该编译器可处理死代码和资源移除(以缩减大小)、代码重命名或缩减大小,以及应用优化。

启用 R8 是准备发布应用的关键步骤,但需要开发者以“保留规则”的形式提供指导。

阅读本文后,请观看 YouTube 上的 Performance Spotlight Week 视频,了解如何启用 R8 优化器、如何使用 R8 优化器进行调试和问题排查。

 

 

为什么需要保留规则

之所以需要编写 Keep 规则,是因为存在一个核心冲突:R8 是一个静态分析工具,但 Android 应用通常依赖于动态执行模式,例如反射或使用 JNI(Java 原生接口)调用原生代码。

R8 通过分析直接调用来构建已使用代码的图表。当以动态方式访问代码时,R8 的静态分析无法预测到这一点,因此会将该代码标识为未使用并将其移除,从而导致运行时崩溃。

保留规则是向 R8 编译器发出的明确指令,内容为:“这个特定的类、方法或字段是一个入口点,将在运行时被动态访问。即使您找不到对它的直接引用,也必须保留它。”

如需详细了解保留规则,请参阅官方指南。

在哪里编写保留规则

应用的自定义保留规则会写入文本文件。按照惯例,此文件名为 proguard-rules.pro,位于应用或库模块的根目录中。然后,在模块的 build.gradle.kts 文件的 release build 类型中指定此文件。

release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile("proguard-android-optimize.txt"),

        "proguard-rules.pro",

    )

}

使用正确的默认文件

getDefaultProguardFile 方法会导入 Android SDK 提供的一组默认规则。如果使用错误的文件,您的应用可能无法得到优化。请务必使用 proguard-android-optimize.txt。此文件提供了标准 Android 组件的默认保留规则,并 启用了 R8 的代码优化功能。过时的 proguard-android.txt 仅提供保留规则,但不启用 R8 的优化。

progaurd.png

由于这是一个严重的性能问题,因此从 Android Studio Narwhal 3 功能更新开始,我们会开始警告开发者不要使用错误的文件。从 Android Gradle 插件 9.0 版开始,我们将不再支持过时的 proguard-android.txt 文件。因此,请务必升级到优化版本。

如何编写保留规则

keep 规则由三个主要部分组成:

  1. -keep 或 -keepclassmembers 等选项 
  2. 可选修饰符,例如 allowshrinking
  3. 类规范,用于定义要匹配的代码

如需查看完整语法和示例,请参阅添加保留规则指南。

保留规则反模式

了解最佳实践固然重要,但了解反模式也同样重要。这些反模式通常源于误解或问题排查捷径,可能会对正式版 build 的性能造成灾难性影响。

全局选项

这些标志是全局切换开关,绝不应在发布 build 中使用。它们仅用于临时调试,以隔离问题。

使用 -dontotptimize 会有效停用 R8 的性能优化,从而导致应用运行速度变慢。

使用 -dontobfuscate 时,您会停用所有重命名操作;使用 -dontshrink 时,您会关闭死代码移除功能。这两条全局规则都会增加应用大小。

请尽可能避免在生产环境中使用这些全局标志,以提供更高效的应用用户体验。

过于宽泛的保留规则

要让 R8 的优势化为乌有,最简单的方法就是编写过于宽泛的保留规则。如下所示的保留规则会指示 R8 优化器不要缩减、混淆和优化此软件包或其任何子软件包中的任何类。这会完全消除 R8 对整个软件包的优势。请尝试改为编写范围较窄且具体的保留规则。

-keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS

反转运算符 (!)

反转运算符 (!) 似乎是一种从规则中排除软件包的有效方法。但事情没那么简单。请看以下示例

-keep class !com.example.my_package.** { *; } // USE WITH CAUTION

您可能会认为此规则的意思是“不要保留com.example.package 中的类”。但实际上,它的意思是“保留整个应用中不在 com.example.package 中的每个类、方法和属性”。如果您对此感到惊讶,最好检查 R8 配置中是否存在任何否定。

Android 组件的冗余规则

另一个常见错误是手动为应用的 Activities、Services 或 BroadcastReceivers 添加保留规则。这是不必要的。默认的 proguard-android-optimize.txt 文件已包含这些标准 Android 组件的相关规则,以便它们开箱即用。

此外,许多库都有自己的保留规则。因此,您无需自行编写这些规则。如果您使用的库中的 Keep 规则存在问题,最好与库作者联系,了解问题所在。

Google 保险柜规则最佳实践

了解了不该做什么之后,我们来谈谈最佳实践。

编写范围较窄的保留规则

好的保留规则应尽可能具体和明确。它们应仅保留必要的内容,以便 R8 优化其他所有内容。

规则质量

 

-keep class com.example.** { ; }

 

低:保留整个软件包及其子软件包

 

-keep class com.example.MyClass { ; }

 

低:保留整个类,这可能仍然过于宽泛
-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}
高:仅保留特定类中的相关方法和属性

使用共同祖先实体

不要为多个不同的数据模型编写单独的 Keep 规则,而是编写一条针对通用基类或接口的规则。以下规则指示 R8 保留实现此接口的类的所有成员,并且具有高度可伸缩性。

# Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}

使用注解定位多个类

创建一个自定义注解(例如 @Serialize),并使用它来“标记”需要保留其字段的类。这是另一种简洁、声明式且高度可扩缩的模式。您还可以为您正在使用的框架中的现有注解创建 Keep 规则。

# Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

选择合适的保留选项

保留选项是规则中最关键的部分。如果选择错误,可能会不必要地停用优化功能。

保留选项作用
-keep防止移除或重命名声明中提及的类和成员 。
-keepclassmembers防止指定成员被移除或重命名,但允许移除类本身,不过仅限于未以其他方式移除的类。
-keepclasseswithmembers组合:仅当所有指定成员都存在时,才保留类及其成员。

如需详细了解保留选项,请参阅我们的保留选项文档。

允许使用修饰符进行优化

allowshrinking 和 allowobfuscation 等修饰符会放宽宽泛的 -keep 规则,从而将优化权限交还给 R8。例如,如果某个旧版库强制您对整个类使用 -keep,您或许可以通过允许缩减和混淆来恢复一些优化:

# Keep this class, but allow R8 to remove it if it's unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass

添加全局选项以进行额外优化

除了保留规则之外,您还可以在 R8 配置文件中添加全局标志,以进一步优化。

-repackageclasses 是一个强大的选项,可指示 R8 将所有混淆类移至单个软件包中。这样可以移除冗余的软件包名称字符串,从而节省 DEX 文件中的大量空间。

-allowaccessmodification 允许 R8 扩大访问权限(例如,从 private 扩大到 public),以实现更积极的内联。现在,使用 proguard-android-optimize.txt 时,此功能默认处于启用状态。

警告:库作者绝不能将这些全局优化标志添加到其使用方规则中,因为它们会被强制应用于整个应用。

为了进一步明确这一点,在 Android Gradle 插件 9.0 版中,我们将开始完全忽略库中的全局优化标志。

库的最佳实践

每个 Android 应用都会以某种方式依赖于库。下面我们来讨论一下库的最佳实践。

面向库开发者

如果您的库使用反射或 JNI,您有责任向其使用者提供必要的保留规则。这些规则会放置在 consumer-rules.pro 文件中,然后自动捆绑到库的 AAR 文件中。

android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

对于库使用者

滤除有问题的保留规则

如果您必须使用包含有问题的保留规则的库,则可以从 AGP 9.0 开始在 build.gradle.kts 文件中过滤掉这些规则。这会指示 R8 忽略来自特定依赖项的规则。

release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom("com.somelibrary:somelibrary")

    }

}

最好的保留规则就是没有保留规则

终极 R8 配置策略是完全消除编写 Keep 规则的需求。对于许多应用,可以通过选择更倾向于代码生成而非反射的现代库来实现。通过代码生成,优化器可以更轻松地确定在运行时实际使用了哪些代码,以及可以移除哪些代码。此外,不使用任何动态反射意味着没有“隐藏”入口点,因此不需要保留规则。 选择新库时,请始终优先选择使用代码生成而非反射的解决方案。

如需详细了解如何选择库,请参阅明智地选择库。

调试和排查 R8 配置问题

当 R8 移除了本应保留的代码,或者 APK 大于预期时,请使用这些工具来诊断问题。

查找重复和全局 Keep 规则

由于 R8 会合并来自数十个来源的规则,因此很难知道“最终”规则集是什么。将此标志添加到 proguard-rules.pro 文件中会生成一份完整的报告:

# Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt

您可以搜索此文件,找到冗余规则或将有问题的规则(例如 -dontoptimize)追溯到包含该规则的特定库。

询问 R8:为什么要保留此内容?

如果您希望移除的类仍在应用中,R8 可以告诉您原因。只需添加以下规则:

# Asks R8 to explain why it's keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 

在构建过程中,R8 会输出导致其保留该类的确切引用链,以便您跟踪引用并调整规则。

如需查看完整指南,请参阅排查 R8 问题部分。

后续步骤

R8 是一款强大的工具,可用于提升 Android 应用性能。其有效性取决于对它作为静态分析引擎的运行方式的正确理解。

通过编写具体的成员级规则、利用祖先和注解,以及谨慎选择正确的保留选项,您可以精确保留必要的内容。最先进的做法是选择基于代码生成的现代库,而不是基于反射的旧库,从而完全消除对规则的需求。

在关注 Performance Spotlight Week 的同时,请务必观看 YouTube 上今天的 Spotlight Week 视频,并继续参与我们的 R8 挑战。如果您在启用或排查 R8 问题时遇到任何疑问,请使用 #optimizationEnabled。我们随时为您提供帮助。

是时候亲自体验这些福利了。

我们建议您立即为应用启用 R8 完整模式。

  1. 请参阅我们的开发者指南,了解如何开始使用:启用应用优化。
  2. 检查您是否仍在使用 proguard-android.txt,并将其替换为 proguard-android-optimize.txt。
  3. 然后,衡量所产生的影响。不要只是感觉到差异,还要验证差异。通过调整  GitHub 上的 Macrobenchmark 示例应用中的代码,衡量优化前后的启动时间,从而衡量性能提升幅度。

我们相信,您会看到应用的性能有显著提升。

在此期间,您可以使用社交标签 #AskAndroid 提出问题。我们的专家会全周关注并回答您的问题。

明天,我们将讨论如何使用基准配置文件和启动配置文件进行配置文件引导的优化,分享 Compose 渲染性能在过去几个版本中的提升情况,并分享后台工作的性能注意事项。

作者:
继续阅读