缩减、混淆和优化您的应用

为了使您的应用尽可能小巧快速,您应该使用 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 规则文件,但建议您使用 proguard-android-optimize.txt

库依赖项

在 AAR 库中
proguard.txt

在 JAR 库中
META-INF/proguard/<ProGuard-rules-file>

除了这些位置之外,Android Gradle 插件 3.6 或更高版本还支持目标缩减规则

如果 AAR 或 JAR 库发布了其自己的规则文件,并且您将该库作为编译时依赖项包含在内,则 R8 会在编译项目时自动应用这些规则。

除了传统的 ProGuard 规则之外,Android Gradle 插件 3.6 或更高版本还支持目标缩减规则。这些规则针对特定的缩减器(R8 或 ProGuard),以及特定的缩减器版本。

使用与库打包在一起的规则文件非常有用,如果某些规则对于库正常运行是必需的,也就是说,库开发者已经为您完成了故障排除步骤。

但是,您应该知道,由于规则是累加的,因此库依赖项包含的某些规则无法删除,并且可能会影响应用其他部分的编译。例如,如果库包含一个禁用代码优化的规则,则该规则会禁用整个项目的优化。

Android 资源包工具 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.0r8-from-8.0.0-upto-8.2.0r8-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 规则,或者如果他们想要保持与 Android Gradle 插件 3.6 或其他工具的兼容性,则同时包含这两种类型。

包含其他配置

当您使用 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 在缩减应用时会删除该代码。

图 1. 在编译时,R8 基于项目的组合保留规则构建图形以确定不可到达的代码。

R8 通过项目R8 配置文件中的 -keep 规则确定入口点。也就是说,保留规则指定 R8 在缩减应用时不应丢弃的类,并且 R8 将这些类视为应用的可能入口点。Android Gradle 插件和 AAPT2 会自动为您生成大多数应用项目所需的保留规则,例如应用的 Activity、视图和服务。但是,如果您需要使用其他保留规则自定义此默认行为,请阅读有关如何自定义要保留的代码的部分。

如果您只对减小应用资源的大小感兴趣,请跳到有关如何缩减资源的部分。

请注意,如果库项目被缩减,则依赖该库的应用将包含缩减的库类。如果库 APK 中缺少类,您可能需要调整库保留规则。如果您正在构建和发布 AAR 格式的库,则库依赖的本地 JAR 文件不会在 AAR 文件中缩减。

自定义要保留的代码

在大多数情况下,默认的 ProGuard 规则文件(proguard-android-optimize.txt)足以让 R8 只移除未使用的代码。但是,某些情况 R8 难以正确分析,它可能会移除应用实际需要的代码。以下是一些 R8 可能错误移除代码的示例:

  • 当应用调用 Java 本地接口 (JNI) 中的方法时
  • 当应用在运行时查找代码时(例如使用反射)

测试应用应该可以发现由不恰当移除代码引起的任何错误,但您也可以通过生成已移除代码的报告来检查已移除的代码。

要修复错误并强制 R8 保留某些代码,请在 ProGuard 规则文件中添加-keep 行。例如:

-keep public class MyClass

或者,您可以在要保留的代码上添加@Keep 注解。在类上添加 @Keep 会按原样保留整个类。在方法或字段上添加它将保留方法/字段(及其名称)以及类名。请注意,此注解仅在使用AndroidX 注解库并且包含 Android Gradle 插件打包的 ProGuard 规则文件时可用,如有关如何启用缩减的部分所述。

使用 -keep 选项时,需要考虑许多因素;有关自定义规则文件的更多信息,请阅读ProGuard 手册故障排除部分概述了代码被剥离时可能遇到的其他常见问题。

剥离原生库

默认情况下,应用的发布版本会剥离原生代码库。此剥离包括移除应用使用的任何原生库中包含的符号表和调试信息。剥离原生代码库可以显著节省空间;但是,由于缺少信息(例如类和函数名称),因此无法在 Google Play Console 上诊断崩溃。

原生崩溃支持

Google Play Console 在Android 性能指标下报告原生崩溃。通过几个步骤,您可以为应用生成并上传原生调试符号文件。此文件可在 Android 性能指标中启用符号化的原生崩溃堆栈跟踪(包括类和函数名称),以帮助您调试生产环境中的应用。这些步骤因项目中使用的 Android Gradle 插件版本和项目的构建输出而异。

Android Gradle 插件版本 4.1 或更高版本

如果您的项目构建 Android App Bundle,则可以自动在其中包含原生调试符号文件。要在发布版本中包含此文件,请将以下内容添加到应用的 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
  1. 压缩此目录的内容。

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. 手动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 App Bundle 格式发布应用时,默认情况下,仅在安装应用时下载用户设备上配置的语言。类似地,下载内容中仅包含与设备屏幕密度匹配的资源,以及与设备 ABI 匹配的原生库。有关更多信息,请参阅 Android App Bundle 配置

对于使用 APK 发布的旧版应用(在 2021 年 8 月之前创建),您可以通过 构建多个 APK 来自定义要包含在 APK 中的屏幕密度或 ABI 资源,每个 APK 都针对不同的设备配置。

合并重复资源

默认情况下,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 将通过多达 30% 提高代码的运行时性能(包括 UI 线程上的启动和帧时间)。禁用任何这些功能都会大大限制 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 中删除该类。也就是说,如果您想保留该类及其静态初始化程序,则需要在规则文件中包含一个保留规则来执行此操作。

如果您在使用 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 App Bundle 发布时,此文件将作为应用捆绑包内容的一部分自动包含在内。然后 Google Play 将回溯来自用户报告问题的传入堆栈跟踪,以便您可以在 Play Console 中查看它们。有关更多信息,请参阅关于如何 反混淆崩溃堆栈跟踪 的帮助中心文章。

使用 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 从项目的保留规则中确定的入口点的报告,请在自定义规则文件中包含 -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是可访问的——如果您向上搜索,您会发现该资源在“The root reachable resources are:”下列出。这意味着代码中存在对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属性通知构建系统将其删除,如“自定义要保留的资源”部分所述。