产品资讯

配置和排查 R8 Keep 规则

阅读用时:7 分钟
Ajesh Pai & Ben Weiss

在现代 Android 开发中,交付小巧、快速且安全的应用程序是用户的基本期望。Android 构建系统实现此目的的主要工具是 R8  优化器,该编译器负责处理无用代码和资源移除以进行缩减、代码重命名或缩小,以及应用优化。

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

阅读本文后,不妨前往 YouTube 观看“性能聚光灯周”视频,了解如何启用、调试和排查 R8 优化器方面的问题。

 

 

为什么需要保留规则

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

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

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

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

在哪里编写保留规则

应用的自定义 Keep 规则以文本文件形式编写。按照惯例,此文件名为 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 仅提供 Keep 规则,但启用 R8 的优化。

progaurd.png

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

如何编写保留规则

保留规则包含三个主要部分:

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

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

Keep 规则反模式

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

全局选项

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

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

使用 -dontobfuscate 会停用所有重命名,而使用 -dontshrink 会关闭死代码移除功能。这两种全局规则都会增加应用的大小。

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

过于宽泛的保留规则

抵消 R8 优势的最简单方法是编写过于宽泛的 Keep 规则。以下保留规则指示 R8 优化器不要压缩、混淆或优化相应软件包中的任何类或其任何子软件包。这会完全移除 R8 对整个软件包的优势。请尝试撰写范围较窄且具体的 Keep 规则。

-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 组件的冗余规则

另一个常见错误是为应用的 ActivitiesServices 或 BroadcastReceivers 手动添加 Keep 规则。这是不必要的。默认 proguard-android-optimize.txt 文件已包含相关规则,可让这些标准 Android 组件开箱即用。

此外,许多库都自带 Keep 规则。因此,您无需自行为这些内容编写规则。如果您使用的库中的 Keep 规则存在问题,最好与库作者联系,了解具体问题。

遵循规则最佳实践

既然您已经了解了不应采取的做法,接下来我们来谈谈最佳实践。

编写范围较窄的保留规则

良好的保留规则应尽可能具体且范围尽可能小。它们应仅保留必要的内容,以便 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 all fields of any class annotated with @Serialize

-keepclassmembers class * {

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

}

选择合适的保留选项

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

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

如需详细了解 keep 选项,请参阅我们的 Keep 选项文档

允许使用修饰符进行优化

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

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

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

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

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

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

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

库的最佳实践

每个 Android 应用都会以某种方式依赖于库。因此,我们来谈谈库的最佳实践。

对于库开发者

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

android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

对于库使用者

过滤掉有问题的 Keep 规则

如果您必须使用包含有问题 Keep 规则的库,可以从 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 

在 build 期间,R8 会输出导致其保留相应类的确切引用链,以便您跟踪引用并调整规则。

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

后续步骤

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

通过编写具体的会员级规则、利用祖先和注释,以及仔细选择合适的保留选项,您可以准确保留所需内容。最先进的做法是选择基于代码生成的现代库,而不是基于反射的前代库,从而完全消除对规则的需求。

在关注“性能聚焦周”的同时,请务必观看今天在 YouTube 上发布的聚焦周视频,并继续参与我们的 R8 挑战。如果您在启用或排查 R8 方面有任何问题,请使用 #optimizationEnabled。我们随时为您提供帮助。

现在就来了解一下这些福利吧。

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

  1. 请按照我们的开发者指南开始操作:启用应用优化
  2. 检查您是否仍在使用 proguard-android.txt,并将其替换为 proguard-android-optimize.txt
  3. 然后,衡量效果。不要只是感觉到差异,还要验证差异。通过调整 GitHub 上的 Macrobenchmark 示例应用中的代码来衡量性能提升,以测量启动时间前后。

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

同时,您还可以使用社交媒体标签 #AskAndroid 提出问题。在整个周内,我们的专家都会关注并回答您的问题。

敬请期待明天的内容,我们将讨论使用基准配置文件和启动配置文件的由配置文件引导的优化,分享 Compose 渲染性能在过去版本中的改进情况,并分享后台工作的性能注意事项。

作者:

继续阅读