为了使您的应用尽可能小巧快速,您应该使用 isMinifyEnabled = true
优化和缩小您的发布版本。
这样做可以启用 *缩减*,它可以删除未使用的代码和资源;*混淆*,它可以缩短应用类和成员的名称;以及 *优化*,它可以应用更积极的策略来进一步减小应用的大小并提高其性能。此页面介绍了 R8 如何为您的项目执行这些编译时任务,以及如何自定义它们。
当您使用 Android Gradle 插件 3.4.0 或更高版本构建项目时,插件不再使用 ProGuard 执行编译时代码优化。相反,插件与 *R8 编译器* 协同工作以处理以下编译时任务
- **代码缩减(或树形抖动):** 检测并安全地删除应用及其库依赖项中未使用的类、字段、方法和属性(使其成为解决 64k 参考限制 的宝贵工具)。例如,如果您仅使用库依赖项的几个 API,则缩减可以识别您的应用*未使用*的库代码,并仅从您的应用中删除该代码。要了解更多信息,请转到有关如何 缩减代码 的部分。
- **资源缩减:** 删除打包应用中未使用的资源,包括应用库依赖项中未使用的资源。它与代码缩减协同工作,以便在删除未使用的代码后,可以安全地删除不再引用的任何资源。要了解更多信息,请转到有关如何 缩减资源 的部分。
- **优化:** 检查并重写代码以提高运行时性能并进一步减小应用 DEX 文件的大小。这通过将代码的运行时性能提高多达 30% 来改进性能,从而极大地提高了启动和帧时间。例如,如果 R8 检测到给定 if/else 语句的
else {}
分支永远不会被执行,则 R8 会删除else {}
分支的代码。要了解更多信息,请转到有关 代码优化 的部分。 - **混淆(或标识符缩减):** 缩短类和成员的名称,从而减小 DEX 文件的大小。要了解更多信息,请转到有关如何 混淆代码 的部分。
在构建应用的发布版本时,可以将 R8 配置为为您执行上面描述的编译时任务。您还可以禁用某些任务或通过 ProGuard 规则文件自定义 R8 的行为。实际上,**R8 可以与您所有现有的 ProGuard 规则文件一起使用**,因此更新 Android Gradle 插件以使用 R8 不需要您更改现有规则。
启用缩减、混淆和优化
当您使用 Android Studio 3.4 或 Android Gradle 插件 3.4.0 及更高版本时,R8 是将项目的 Java 字节码转换为在 Android 平台上运行的 DEX 格式的默认编译器。但是,当您使用 Android Studio 创建新项目时,缩减、混淆和代码优化默认情况下不会启用。这是因为这些编译时优化会增加项目的构建时间,并且如果您没有充分 自定义要保留的代码,则可能会引入错误。
因此,最好在构建最终版本的应用时启用这些编译时任务,并在发布前对其进行测试。要启用缩减、混淆和优化,请在您的项目级构建脚本中包含以下内容。
Kotlin
android { buildTypes { getByName("release") { // Enables code shrinking, obfuscation, and optimization for only // your project's release build type. Make sure to use a build // variant with `isDebuggable=false`. isMinifyEnabled = true // Enables resource shrinking, which is performed by the // Android Gradle plugin. isShrinkResources = true proguardFiles( // Includes the default ProGuard rules files that are packaged with // the Android Gradle plugin. To learn more, go to the section about // R8 configuration files. getDefaultProguardFile("proguard-android-optimize.txt"), // Includes a local, custom Proguard rules file "proguard-rules.pro" ) } } ... }
Groovy
android { buildTypes { release { // Enables code shrinking, obfuscation, and optimization for only // your project's release build type. Make sure to use a build // variant with `debuggable false`. minifyEnabled true // Enables resource shrinking, which is performed by the // Android Gradle plugin. shrinkResources true // Includes the default ProGuard rules files that are packaged with // the Android Gradle plugin. To learn more, go to the section about // R8 configuration files. proguardFiles getDefaultProguardFile( 'proguard-android-optimize.txt'), 'proguard-rules.pro' } } ... }
R8 配置文件
R8 使用 ProGuard 规则文件来修改其默认行为并更好地了解应用的结构,例如用作应用代码入口点的类。虽然您可以修改其中一些规则文件,但某些规则可能是由编译时工具(如 AAPT2)自动生成的,或者继承自应用的库依赖项。下表描述了 R8 使用的 ProGuard 规则文件的来源。
来源 | 位置 | 描述 |
Android Studio | <module-dir>/proguard-rules.pro
|
当您使用 Android Studio 创建新的模块时,IDE 会在该模块的根目录中创建一个名为 proguard-rules.pro 的文件。默认情况下,此文件不应用任何规则。因此,请在此处包含您自己的 ProGuard 规则,例如您的 自定义保留规则。 |
Android Gradle 插件 | 在编译时由 Android Gradle 插件生成。 | Android Gradle 插件会生成 proguard-android-optimize.txt ,其中包含对大多数 Android 项目有用的规则,并启用 @Keep* 注释。默认情况下,当您使用 Android Studio 创建新模块时,模块级构建脚本会为您在发布版本中包含此规则文件。
注意:Android Gradle 插件包含其他预定义的 ProGuard 规则文件,但建议您使用 |
库依赖项 |
在 AAR 库中 在 JAR 库中 除了这些位置之外,Android Gradle 插件 3.6 或更高版本还支持 目标收缩规则。 |
如果 AAR 或 JAR 库发布了自己的规则文件,并且您将该库作为编译时依赖项包含在内,则 R8 会在编译项目时自动应用这些规则。 除了传统的 ProGuard 规则之外,Android Gradle 插件 3.6 或更高版本还支持 目标收缩规则。这些规则针对特定的收缩器(R8 或 ProGuard)以及特定的收缩器版本。 使用与库打包在一起的规则文件非常有用,如果库正常运行需要某些规则,也就是说,库开发者已经为您执行了故障排除步骤。 但是,您应该知道,由于规则是累加的,因此库依赖项包含的某些规则无法删除,并且可能会影响应用程序其他部分的编译。例如,如果库包含一个禁用代码优化的规则,则该规则会禁用整个项目的优化。 |
Android Asset Package 工具 2 (AAPT2) | 使用 minifyEnabled true 构建项目后:<module-dir>/build/intermediates/aapt_proguard_file/.../aapt_rules.txt |
AAPT2 根据对应用程序清单、布局和其他应用程序资源中类的引用生成保留规则。例如,AAPT2 为您在应用程序清单中注册为入口点的每个 Activity 包含一个保留规则。 |
自定义配置文件 | 默认情况下,当您使用 Android Studio 创建新的模块时,IDE 会为您创建 <module-dir>/proguard-rules.pro 以便您添加自己的规则。 |
您可以 包含其他配置,R8 会在编译时应用它们。 |
当您将 minifyEnabled
属性设置为 true
时,R8 会将来自上面列出的所有可用源的规则组合在一起。在您 使用 R8 进行故障排除 时,这一点很重要,因为其他编译时依赖项(例如库依赖项)可能会引入您不知道的 R8 行为更改。
要输出 R8 在构建项目时应用的所有规则的完整报告,请在模块的 proguard-rules.pro
文件中包含以下内容
// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt
目标收缩规则
Android Gradle 插件 3.6 或更高版本支持库的规则,这些规则针对特定的收缩器(R8 或 ProGuard)以及特定的收缩器版本。这允许库开发者调整他们的规则,以便在使用新收缩器版本的项目中最佳地工作,同时允许在使用旧收缩器版本的项目中继续使用现有规则。
要指定目标收缩规则,库开发者需要将它们包含在 AAR 或 JAR 库内的特定位置,如下所述。
In an AAR library:
proguard.txt (legacy location)
classes.jar
└── META-INF
└── com.android.tools (targeted shrink rules location)
├── r8-from-<X>-upto-<Y>/<R8-rules-file>
└── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>
In a JAR library:
META-INF
├── proguard/<ProGuard-rules-file> (legacy location)
└── com.android.tools (targeted shrink rules location)
├── r8-from-<X>-upto-<Y>/<R8-rules-file>
└── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>
这意味着目标收缩规则存储在 JAR 的 META-INF/com.android.tools
目录中,或存储在 AAR 的 classes.jar
内的 META-INF/com.android.tools
目录中。
在该目录下,可以有多个目录,其名称采用 r8-from-<X>-upto-<Y>
或 proguard-from-<X>-upto-<Y>
的形式,以指示目录内的规则是为哪个收缩器的哪个版本编写的。请注意,-from-<X>
和 -upto-<Y>
部分是可选的,<Y>
版本是不包含的,并且版本范围必须是连续的。
例如,r8-upto-8.0.0
、r8-from-8.0.0-upto-8.2.0
和 r8-from-8.2.0
构成一组有效的目标收缩规则。r8-from-8.0.0-upto-8.2.0
目录下的规则将被版本 8.0.0 到(但不包括)8.2.0 的 R8 使用。
鉴于这些信息,Android Gradle 插件 3.6 或更高版本将从匹配的 R8 目录中选择规则。如果库未指定目标收缩规则,则 Android Gradle 插件将从旧位置(AAR 的 proguard.txt
或 JAR 的 META-INF/proguard/<ProGuard-rules-file>
)选择规则。
库开发者可以选择在他们的库中包含目标收缩规则或旧版 ProGuard 规则,或者如果他们想保持与低于 3.6 的 Android Gradle 插件或其他工具的兼容性,则同时包含这两种类型。
包含其他配置
当您使用 Android Studio 创建新项目或模块时,IDE 会创建一个 <module-dir>/proguard-rules.pro
文件,供您包含自己的规则。您还可以通过将其他文件的规则添加到模块构建脚本中的 proguardFiles
属性中来包含其他规则。
例如,您可以通过在相应的 productFlavor
块中添加另一个 proguardFiles
属性来添加特定于每个构建变体的规则。以下 Gradle 文件将 flavor2-rules.pro
添加到 flavor2
产品风格中。现在,flavor2
使用所有三个 ProGuard 规则,因为来自 release
块的规则也会被应用。
此外,您可以添加 testProguardFiles
属性,该属性指定仅包含在测试 APK 中的 ProGuard 文件列表。
Kotlin
android { ... buildTypes { getByName("release") { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), // List additional ProGuard rules for the given build type here. By default, // Android Studio creates and includes an empty rules file for you (located // at the root directory of each module). "proguard-rules.pro" ) testProguardFiles( // The proguard files listed here are included in the // test APK only. "test-proguard-rules.pro" ) } } flavorDimensions.add("version") productFlavors { create("flavor1") { ... } create("flavor2") { proguardFile("flavor2-rules.pro") } } }
Groovy
android { ... buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), // List additional ProGuard rules for the given build type here. By default, // Android Studio creates and includes an empty rules file for you (located // at the root directory of each module). 'proguard-rules.pro' testProguardFiles // The proguard files listed here are included in the // test APK only. 'test-proguard-rules.pro' } } flavorDimensions "version" productFlavors { flavor1 { ... } flavor2 { proguardFile 'flavor2-rules.pro' } } }
缩减代码
当您将 minifyEnabled
属性设置为 true
时,默认情况下会启用使用 R8 进行代码缩减。
代码缩减(也称为树形抖动)是删除 R8 确定在运行时不需要的代码的过程。如果例如您的应用程序包含许多库依赖项,但仅利用了其功能的一小部分,则此过程可以大大减小应用程序的大小。
为了缩减应用程序的代码,R8 首先根据 配置文件的组合集 确定应用程序代码中的所有入口点。这些入口点包括 Android 平台可能用来打开应用程序的 Activity 或服务的全部类。从每个入口点开始,R8 检查应用程序的代码以构建应用程序可能在运行时访问的所有方法、成员变量和其他类的图形。与该图形未连接的代码被认为是不可到达的,并且可以从应用程序中删除。
图 1 显示了一个带有运行时库依赖项的应用程序。在检查应用程序代码时,R8 确定方法 foo()
、faz()
和 bar()
可以从 MainActivity.class
入口点访问。但是,类 OkayApi.class
或其方法 baz()
从未在应用程序的运行时使用过,R8 在缩减应用程序时会删除该代码。
R8 通过项目 R8 配置文件 中的 -keep
规则确定入口点。也就是说,保留规则指定 R8 在缩减应用程序时不应丢弃的类,R8 将这些类视为应用程序的可能入口点。Android Gradle 插件和 AAPT2 会自动生成大多数应用程序项目所需的保留规则,例如应用程序的活动、视图和服务。但是,如果您需要使用其他保留规则自定义此默认行为,请阅读有关如何 自定义要保留的代码 的部分。
如果您只对减小应用程序资源的大小感兴趣,请跳到有关如何 缩减资源 的部分。
请注意,如果库项目被缩减,则依赖该库的应用程序将包含缩减的库类。如果库 APK 中缺少类,您可能需要调整库保留规则。如果您正在以 AAR 格式构建和发布库,则库依赖的本地 JAR 文件不会在 AAR 文件中被缩减。
自定义要保留的代码
对于大多数情况,默认的 ProGuard 规则文件(proguard-android-optimize.txt
)足以使 R8 只删除未使用的代码。但是,某些情况对于 R8 来说难以正确分析,它可能会删除应用程序实际需要的代码。它可能错误地删除代码的一些示例包括
- 当您的应用程序从 Java 本地接口 (JNI) 调用方法时
- 当您的应用程序在运行时查找代码时(例如使用反射)
测试您的应用程序应该会显示由不恰当地删除的代码引起的任何错误,但您也可以通过 生成已删除代码的报告 来检查已删除的代码。
要修复错误并强制 R8 保留某些代码,请在 ProGuard 规则文件中添加 -keep
行。例如
-keep public class MyClass
或者,您可以将 @Keep
注释添加到要保留的代码中。在类上添加 @Keep
会按原样保留整个类。在方法或字段上添加它将保留方法/字段(及其名称)以及类名。请注意,此注释仅在使用 AndroidX Annotations 库 以及包含 Android Gradle 插件打包的 ProGuard 规则文件时可用,如有关如何 启用缩减 的部分所述。
使用 -keep
选项时,您应该考虑许多因素;有关自定义规则文件的更多信息,请阅读 ProGuard 手册。 故障排除 部分概述了代码被剥离时可能遇到的其他常见问题。
剥离原生库
默认情况下,应用的发布版本会剥离原生代码库。此剥离过程包括删除应用使用的任何原生库中包含的符号表和调试信息。剥离原生代码库可以显著节省空间;但是,由于缺少信息(例如类和函数名称),因此无法在 Google Play Console 中诊断崩溃。
原生崩溃支持
Google Play Console 在Android 性能指标下报告原生崩溃。只需几个步骤,您就可以生成并上传应用的原生调试符号文件。此文件支持 Android 性能指标中符号化的原生崩溃堆栈跟踪(包括类和函数名称),以帮助您调试生产环境中的应用。这些步骤因项目中使用的 Android Gradle 插件版本和项目的构建输出而异。
Android Gradle 插件 4.1 版或更高版本
如果您的项目构建 Android 应用包,则可以自动在其中包含原生调试符号文件。要在发布版本中包含此文件,请将以下内容添加到应用的 build.gradle.kts
文件中
android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }
从以下选项中选择调试符号级别
- 使用
SYMBOL_TABLE
可在 Play Console 的符号化堆栈跟踪中获取函数名称。此级别支持tombstones。 - 使用
FULL
可在 Play Console 的符号化堆栈跟踪中获取函数名称、文件和行号。
如果您的项目构建 APK,请使用前面显示的 build.gradle.kts
构建设置单独生成原生调试符号文件。手动将原生调试符号文件上传到 Google Play Console。作为构建过程的一部分,Android Gradle 插件会在以下项目位置输出此文件
app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip
Android Gradle 插件 4.0 版或更低版本(以及其他构建系统)
作为构建过程的一部分,Android Gradle 插件会在项目目录中保留未剥离库的副本。此目录结构类似于以下结构
app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
├── arm64-v8a/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
├── x86/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
└── x86_64/
├── libgameengine.so
├── libothercode.so
└── libvideocodec.so
压缩此目录的内容
cd app/build/intermediates/cmake/universal/release/obj
zip -r symbols.zip .
手动将
symbols.zip
文件上传到 Google Play Console。
缩减资源
资源缩减仅与代码缩减结合使用。代码缩减器删除所有未使用的代码后,资源缩减器可以识别应用仍在使用的资源。当您添加包含资源的代码库时,这一点尤其重要,您必须删除未使用的库代码,以便库资源成为未引用的资源,从而可以由资源缩减器删除。
要启用资源缩减,请在构建脚本中将 shrinkResources
属性设置为 true
(与用于代码缩减的 minifyEnabled
同时使用)。例如
Kotlin
android { ... buildTypes { getByName("release") { isShrinkResources = true isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" ) } } }
Groovy
android { ... buildTypes { release { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } }
如果您尚未使用 minifyEnabled
进行代码缩减来构建应用,请先尝试执行此操作,然后再启用 shrinkResources
,因为您可能需要编辑 proguard-rules.pro
文件以保留在开始删除资源之前动态创建或调用的类或方法。
自定义要保留的资源
如果您希望保留或丢弃特定资源,请在项目中创建一个包含 <resources>
标记的 XML 文件,并在 tools:keep
属性中指定要保留的每个资源,并在 tools:discard
属性中指定要丢弃的每个资源。这两个属性都接受以逗号分隔的资源名称列表。您可以使用星号字符作为通配符。
例如
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*" tools:discard="@layout/unused2" />
将此文件保存在您的项目资源中,例如,在 res/raw/my.package.keep.xml
中。构建不会将此文件打包到您的应用中。
注意:请确保为 keep
文件使用唯一的名称。否则,当不同的库链接在一起时,它们的保留规则会发生冲突,从而导致忽略规则或保留不必要的资源等潜在问题。
当您可以直接删除资源时,指定要丢弃哪些资源可能看起来很愚蠢,但这在使用构建变体时很有用。例如,您可以将所有资源放入公共项目目录中,然后为每个构建变体创建一个不同的 my.package.build.variant.keep.xml
文件,当您知道某个资源似乎在代码中使用(因此不会被缩减器删除)但您知道它实际上不会在给定的构建变体中使用时。构建工具也可能错误地将资源识别为必需的资源,这是可能的,因为编译器会内联添加资源 ID,然后资源分析器可能无法区分真正引用的资源和碰巧具有相同值的代码中的整数值。
启用严格引用检查
通常,资源缩减器可以准确确定是否使用了某个资源。但是,如果您的代码调用了 Resources.getIdentifier()
(或者您的任何库都这样做,AppCompat 库就是这样做的),这意味着您的代码正在根据动态生成的字符串查找资源名称。当您执行此操作时,资源缩减器默认情况下会采取防御措施,并将所有具有匹配名称格式的资源标记为可能已使用且无法删除。
例如,以下代码会导致所有以 img_
为前缀的资源被标记为已使用。
Kotlin
val name = String.format("img_%1d", angle + 1) val res = resources.getIdentifier(name, "drawable", packageName)
Java
String name = String.format("img_%1d", angle + 1); res = getResources().getIdentifier(name, "drawable", getPackageName());
资源缩减器还会检查代码中的所有字符串常量以及各种 res/raw/
资源,以查找类似于 file:///android_res/drawable//ic_plus_anim_016.png
格式的资源 URL。如果找到这样的字符串或其他看起来可能用于构建此类 URL 的字符串,则不会将其删除。
这些是默认情况下启用的安全缩减模式的示例。但是,您可以关闭此“安全第一”处理,并指定资源缩减器仅保留它确定已使用的资源。为此,请在 keep.xml
文件中将 shrinkMode
设置为 strict
,如下所示
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="strict" />
如果您确实启用了严格缩减模式,并且您的代码也引用了具有动态生成字符串的资源(如上所示),则必须使用 tools:keep
属性手动保留这些资源。
删除未使用的备用资源
Gradle 资源缩减器仅删除应用代码未引用的资源,这意味着它不会删除针对不同设备配置的备用资源。如有必要,您可以使用 Android Gradle 插件的 resConfigs
属性删除应用不需要的备用资源文件。
例如,如果您使用包含语言资源的库(例如 AppCompat 或 Google Play 服务),则您的应用会包含这些库中消息的所有翻译后的语言字符串,而不管应用的其余部分是否翻译成相同的语言。如果您只想保留应用正式支持的语言,则可以使用 resConfig
属性指定这些语言。任何未指定语言的资源都会被删除。
以下代码段显示了如何将语言资源限制为仅英语和法语
Kotlin
android { defaultConfig { ... resourceConfigurations.addAll(listOf("en", "fr")) } }
Groovy
android { defaultConfig { ... resConfigs "en", "fr" } }
使用 Android 应用包格式发布应用时,默认情况下,仅在安装应用时下载用户设备上配置的语言。同样,下载中仅包含与设备屏幕密度匹配的资源以及与设备 ABI 匹配的原生库。有关更多信息,请参阅Android 应用包配置。
对于使用 APK 发布的旧版应用(在 2021 年 8 月之前创建),您可以通过构建多个 APK(每个 APK 针对不同的设备配置)来自定义要包含在 APK 中的屏幕密度或 ABI 资源。
合并重复资源
默认情况下,Gradle 还会合并同名资源,例如可能位于不同资源文件夹中的同名可绘制对象。此行为不受 shrinkResources
属性控制,也无法禁用,因为必须避免多个资源与代码查找的名称匹配时发生错误。
仅当两个或多个文件共享相同的资源名称、类型和限定符时,才会发生资源合并。Gradle 会选择它认为在重复项中最合适的哪个文件(基于下面描述的优先级顺序),并且仅将该资源传递给 AAPT 以在最终工件中分发。
Gradle 在以下位置查找重复资源
- 与主源集关联的主资源,通常位于
src/main/res/
中。 - 来自构建类型和构建变体的变体覆盖。
- 库项目依赖项。
Gradle 按以下级联优先级顺序合并重复资源
依赖项 → 主 → 构建变体 → 构建类型
例如,如果重复资源同时出现在主资源和构建变体中,Gradle 会选择构建变体中的资源。
如果相同的资源出现在同一个源集中,Gradle 无法合并它们,并会发出资源合并错误。如果在 build.gradle.kts
文件的 sourceSet
属性中定义了多个源集,则可能会发生这种情况,例如,如果 src/main/res/
和 src/main/res2/
都包含相同的资源。
混淆代码
混淆的目的是通过缩短应用的类、方法和字段的名称来减小应用的大小。以下是使用 R8 进行混淆的示例
androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
android.content.Context mContext -> a
int mListItemLayout -> O
int mViewSpacingRight -> l
android.widget.Button mButtonNeutral -> w
int mMultiChoiceItemLayout -> M
boolean mShowTitle -> P
int mViewSpacingLeft -> j
int mButtonPanelSideLayout -> K
虽然混淆不会从应用中删除代码,但在具有索引许多类、方法和字段的 DEX 文件的应用中可以看到明显的尺寸节省。但是,由于混淆会重命名代码的不同部分,因此某些任务(例如检查堆栈跟踪)需要其他工具。要了解混淆后的堆栈跟踪,请阅读有关如何解码混淆的堆栈跟踪的部分。
此外,如果您的代码依赖于应用方法和类的可预测命名(例如,使用反射时),则应将这些签名视为入口点并为其指定保留规则,如有关如何自定义要保留的代码的部分中所述。这些保留规则告诉 R8 不仅在应用的最终 DEX 中保留该代码,而且还要保留其原始命名。
解码混淆的堆栈跟踪
在 R8 混淆您的代码后,理解堆栈跟踪会变得困难(如果不是不可能的话),因为类和方法的名称可能已被更改。要获取原始堆栈跟踪,您应该回溯堆栈跟踪。
代码优化
为了进一步优化您的应用,R8 会更深入地检查您的代码,以删除更多未使用的代码,或者在可能的情况下重写您的代码使其更简洁。以下是此类优化的几个示例
- 如果您的代码从未执行给定 if/else 语句的
else {}
分支,R8 可能会删除else {}
分支的代码。 - 如果您的代码仅在少数几个地方调用某个方法,R8 可能会删除该方法并在少数调用站点内联该方法。
- 如果 R8 确定某个类只有一个唯一的子类,并且该类本身未被实例化(例如,仅由一个具体实现类使用的抽象基类),那么 R8 可以合并这两个类并从应用中删除一个类。
- 要了解更多信息,请阅读 Jake Wharton 编写的R8 优化博客文章。
R8 不允许您禁用或启用离散优化,或修改优化的行为。事实上,R8 会忽略任何试图修改默认优化的 ProGuard 规则,例如-optimizations
和-optimizationpasses
。此限制非常重要,因为随着 R8 不断改进,维护优化的标准行为有助于 Android Studio 团队轻松地排查和解决您可能遇到的任何问题。
请注意,启用优化会更改应用程序的堆栈跟踪。例如,内联会删除堆栈帧。请参阅有关回溯的部分,了解如何获取原始堆栈跟踪。
对运行时性能的影响
如果缩减、混淆和优化都已启用,R8 将通过提高代码的运行时性能(包括启动时间和 UI 线程上的帧时间)来提高最多 30%。禁用任何一项都会极大地限制 R8 使用的优化集。
如果启用了 R8,您还应该创建启动配置文件以获得更好的启动性能。
启用更激进的优化
R8 包括一组额外的优化(称为“完整模式”),这使得它的行为与 ProGuard 不同。这些优化从Android Gradle 插件版本 8.0.0开始默认启用。
您可以通过在项目的gradle.properties
文件中包含以下内容来禁用这些额外的优化
android.enableR8.fullMode=false
由于额外的优化使 R8 的行为与 ProGuard 不同,因此如果您使用为 ProGuard 设计的规则,则可能需要包含其他 ProGuard 规则以避免运行时问题。例如,假设您的代码通过 Java 反射 API 引用了一个类。在不使用“完整模式”时,R8 假设您打算在运行时检查和操作该类的对象——即使您的代码实际上没有这样做——并且它会自动保留该类及其静态初始化器。
但是,在使用“完整模式”时,R8 不会做出此假设,并且如果 R8 断言您的代码在运行时从未使用过该类,它会从应用程序的最终 DEX 中删除该类。也就是说,如果您想保留该类及其静态初始化器,则需要在规则文件中包含一个 keep 规则来执行此操作。
如果您在使用 R8 的“完整模式”时遇到任何问题,请参阅R8 常见问题解答页面以获取可能的解决方案。如果您无法解决问题,请报告错误。
回溯堆栈跟踪
由 R8 处理的代码以各种方式更改,这可能使堆栈跟踪难以理解,因为堆栈跟踪不会完全对应于源代码。在调试信息未保留时,这可能是行号更改的情况。这可能是由于内联和概述等优化造成的。最大的贡献者是混淆,即使类和方法也会更改名称。
为了恢复原始堆栈跟踪,R8 提供了retrace命令行工具,该工具与命令行工具包捆绑在一起。
为了支持应用程序堆栈跟踪的回溯,您应该确保构建保留足够的信息以进行回溯,方法是在模块的proguard-rules.pro
文件中添加以下规则
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
LineNumberTable
属性保留方法中的位置信息,以便在堆栈跟踪中打印这些位置。SourceFile
属性确保所有潜在的运行时实际上都打印位置信息。-renamesourcefileattribute
指令将堆栈跟踪中的源文件名设置为SourceFile
。回溯时不需要实际的原始源文件名,因为映射文件包含原始源文件。
R8 在每次运行时都会创建一个mapping.txt
文件,其中包含将堆栈跟踪映射回原始堆栈跟踪所需的信息。Android Studio 将该文件保存在<module-name>/build/outputs/mapping/<build-type>/
目录中。
在 Google Play 上发布您的应用时,您可以上传每个应用版本的mapping.txt
文件。使用 Android 应用包发布时,此文件会自动包含在应用包内容中。然后,Google Play 将回溯来自用户报告问题的传入堆栈跟踪,以便您可以在 Play 管理中心中查看它们。有关更多信息,请参阅关于如何反混淆崩溃堆栈跟踪的帮助中心文章。
使用 R8 进行故障排除
本节介绍了一些在使用 R8 启用缩减、混淆和优化时进行故障排除的策略。如果您在下面没有找到解决您问题的方法,还可以阅读R8 常见问题解答页面和ProGuard 的故障排除指南。
生成已删除(或保留)代码的报告
为了帮助您解决某些 R8 问题,查看 R8 从您的应用中删除的所有代码的报告可能很有用。对于要为其生成此报告的每个模块,请将-printusage <output-dir>/usage.txt
添加到您的自定义规则文件中。当您启用 R8并构建您的应用时,R8 会输出一个报告,其中包含您指定的路径和文件名。已删除代码的报告类似于以下内容
androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
public boolean hasWindowFeature(int)
public void setHandleNativeActionModesEnabled(boolean)
android.view.ViewGroup getSubDecor()
public void setLocalNightMode(int)
final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
private static final boolean DEBUG
private static final java.lang.String KEY_LOCAL_NIGHT_MODE
static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...
如果您想查看 R8 从项目的 keep 规则中确定的入口点的报告,请在自定义规则文件中包含-printseeds <output-dir>/seeds.txt
。当您启用 R8并构建您的应用时,R8 会输出一个报告,其中包含您指定的路径和文件名。已保留入口点的报告类似于以下内容
com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...
对资源缩减进行故障排除
缩减资源时,构建 窗口会显示从应用中删除的资源的摘要。(您需要先单击窗口左侧的切换视图 以显示来自 Gradle 的详细文本输出。)例如
:android:shrinkDebugResources
Removed unused resources: Resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning
Gradle 还会在<module-name>/build/outputs/mapping/release/
(与 ProGuard 的输出文件相同的文件夹)中创建一个名为resources.txt
的诊断文件。此文件包含详细信息,例如哪些资源引用其他资源以及哪些资源被使用或删除。
例如,要找出为什么@drawable/ic_plus_anim_016
仍在您的应用中,请打开resources.txt
文件并搜索该文件名。您可能会发现它被另一个资源引用,如下所示
16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out] @drawable/ic_plus_anim_016
现在您需要知道为什么@drawable/add_schedule_fab_icon_anim
是可访问的——如果您向上搜索,您会发现该资源在“根可访问资源为:”下列出。这意味着存在对add_schedule_fab_icon_anim
的代码引用(即,在可访问的代码中找到了其 R.drawable ID)。
如果您未使用严格检查,如果存在看起来可能用于构建动态加载资源的资源名称的字符串常量,则资源 ID 可以标记为可访问。在这种情况下,如果您在构建输出中搜索资源名称,您可能会看到类似以下的消息
10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
used because it format-string matches string pool constant ic_plus_anim_%1$d.
如果您看到其中一个字符串并且您确定该字符串未用于动态加载给定资源,则可以使用tools:discard
属性通知构建系统将其删除,如有关如何自定义要保留的资源的部分所述。