(十)Android 性能优化 Proguard / R8

小酌鸡汤

古人学问无遗力,少壮工夫老始成。

本文来源《Android 性能优化 全家桶》

ProGuard 和 R8 的关系 ?

 当使用 Android Gradle 插件 3.4.0 或更高版本构建项目时,该插件不再使用 ProGuard 来执行编译时代码优化,而是与 R8 编译器协同工作来处理编译时任务 。

R8 编译器如何优化应用的 ?

  • 代码缩减(即摇树优化):从应用及其库依赖项中检测并安全地移除未使用的类、字段、方法和属性(这使其成为了一个对于规避 64k 引用限制非常有用的工具)。例如,如果您仅使用某个库依赖项的少数几个 API,缩减功能可以识别应用“未”使用的库代码并仅从应用中移除这部分代码。
  • 资源缩减:从封装应用中移除不使用的资源,包括应用库依赖项中的不使用的资源。此功能可与代码缩减功能结合使用,这样一来,移除不使用的代码后,也可以安全地移除不再引用的所有资源。
  • 混淆处理:缩短类和成员的名称,从而减小 DEX 文件的大小。
  • 优化:检查并重写代码,以进一步减小应用的 DEX 文件的大小。例如,如果 R8 检测到从未采用过给定 if/else 语句的 else {} 分支,则会移除 else {} 分支的代码。

现在,就一起实操体验 R8 ~

(1)启用缩减、混淆处理和优化功能:

 Android Studio默认是不开启R8。因此,在构建应用的最终版本(也就是在发布应用之前测试的版本)时,最好启用这些编译时任务。要启用缩减、混淆处理和优化功能,请在您的项目级 build.gradle 文件中添加以下代码:

android {
        buildTypes {
            release {
                //启用代码缩减、混淆处理和优化
                minifyEnabled true
                //启用资源文件缩减、优化
                shrinkResources true
                //默认混淆文件(无内容需要自己添加)
                proguardFiles getDefaultProguardFile(
                        'proguard-android-optimize.txt'),
                        'proguard-rules.pro'
            }
        }
        ...
    }
(2)R8 配置文件

 R8 使用 ProGuard 规则文件来修改其默认行为并更好地了解应用的结构,如充当应用代码入口点的类。虽然您可以修改其中一些规则文件,但某些规则可能由编译时工具(如 AAPT2)自动生成,或从应用的库依赖项继承而来。下表介绍了 R8 使用的 ProGuard 规则文件的来源:

来源 位置 说明
Android Studio /proguard-rules.pro  当您使用 Android Studio 创建新模块时,IDE 会在该模块的根目录中创建 proguard-rules.pro 文件。
 默认情况下,此文件不会应用任何规则。因此,请在此处添加您自己的 ProGuard 规则,如您的自定义保留规则。
Android Gradle 插件 由 Android Gradle 插件在编译时生成  Android Gradle 插件会生成 proguard-android-optimize.txt(其中包含了对大多数 Android 项目都有用的规则),并启用 @Keep* 注释。
 默认情况下,使用 Android Studio 创建新模块时,模块级 build.gradle 文件会将此规则文件纳入到您的发布版本中。
注意:虽然 Android Gradle 插件包含额外的预定义 ProGuard 规则文件,但建议您使用 proguard-android-optimize.txt。
库依赖项 AAR 库:/proguard.txt
JAR 库:/META-INF/proguard/
 如果某个 AAR 库是使用它自己的 ProGuard 规则文件发布的,并且您将该 AAR 库作为编译时依赖项纳入到项目中,则 R8 在编译项目时会自动应用其规则。
 如果 AAR 库需要某些保留规则才能正常运行,那么使用该库随附的规则文件将非常有用。 也就是说,库开发者已经为您执行了问题排查步骤。
 不过,请注意,由于 ProGuard 规则是累加的,因此 AAR 库依赖项包含的某些规则无法移除,并且可能会影响对应用其他部分的编译。例如,如果某个库包含停用代码优化的规则,该规则会针对整个项目停用优化。
Android 资产打包工具 2 (AAPT2) 使用 minifyEnabled true 构建项目后:module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt  AAPT2 会根据对应用清单中的类、布局及其他应用资源的引用来生成保留规则。例如,AAPT2 会为您在应用清单中注册为入口点的每个 Activity 添加一个保留规则。
自定义配置文件 默认情况下,当您使用 Android Studio 创建新模块时,IDE 会创建 /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
(3)添加其他配置

 当您使用 Android Studio 创建新项目或模块时,IDE 会创建一个 /proguard-rules.pro 文件,以便您添加自己的规则。此外,您还可以通过将相应文件添加到模块的 build.gradle 文件中的 proguardFiles 属性,从其他文件添加额外的规则。

android {
        buildTypes {
            release {
                minifyEnabled true
                shrinkResources true
                proguardFiles getDefaultProguardFile(
                        'proguard-android-optimize.txt'),
                        'proguard-rules.pro', 'media-proguard-rules.pro', 'video-proguard-rules.pro'
            }
        }
        ...
    } 
(4)缩减代码
  • 如果将 minifyEnabled 属性设为 true,则默认会启用 R8 代码缩减功能。
  • 代码缩减(也称为“摇树优化”)是指移除 R8 确定在运行时不需要的代码的过程。此过程可以大大减小应用的大小,例如,如果当您的应用包含许多库依赖项,但只会使用它们的一小部分功能时。
  • 要缩减应用的代码,R8 首先会根据组合的配置文件集确定应用代码的所有入口点。 这些入口点包括 Android 平台可用来打开应用的 Activity 或服务的所有类。 从每个入口点开始,R8 会检查应用的代码来构建一张图表,列出应用在运行时可能会访问的所有方法、成员变量和其他类。与该图表没有关联的代码将被视为执行不到,可能会从应用中移除。
  • R8 通过项目的 R8 配置文件中的 -keep 规则确定入口点。 也就是说,保留规则指定 R8 在缩减应用时不应舍弃的类,R8 将这些类视为应用的可能入口点。Android Gradle 插件和 AAPT2 会自动为您生成大多数应用项目(如应用的 Activity、视图和服务)所需的保留规则。
R8 会根据项目的组合保留规则构建一张图表来确定执行不到的代码
(5)自定义要保留的代码

 在大多数情况下,让要 R8 仅移除不使用的代码,使用默认的 ProGuard 规则文件 (proguard-android- optimize.txt) 就足够了。 不过,在某些情况下,R8 很难做出正确判断,因而可能会移除应用实际上需要的代码。下面列举了几个例子,说明了它在什么情况下可能会错误地移除代码:

  • 当您的应用通过 Java 原生接口 (JNI) 调用方法时
  • 当您的应用在运行时查询代码时(如使用反射)

 通过测试应用应该可以发现因错误移除代码而导致的错误,但您也可以通过生成已移除代码的报告来检查移除了哪些代码。
 要修复错误并强制 R8 保留某些代码,请在 ProGuard 规则文件中添加 -keep。例如:

-keep public class MyClass

 或者,您也可以为您要保留的代码添加 @Keep释。在类上添加 @Keep 可按原样保留整个类。在方法或字段上添加该注释,将使该方法/字段(及其名称)以及类名称保持不变。请注意,只有在使用 AndroidX 注释库且您添加 Android Gradle 插件随附的 ProGuard 规则文件时,此注释才可用。

(6)缩减资源

 资源缩减只有在与代码缩减配合使用时才能发挥作用。在代码缩减器移除所有不使用的代码后,资源缩减器便可确定应用仍要使用的资源,当您添加包含资源的代码库时尤其如此。您必须移除不使用的库代码,使库资源变为未引用资源,因而可由资源缩减器移除。

 shrinkResources true
(7)自定义要保留的资源

 如果有想要保留或舍弃的特定资源,请在项目中创建一个包含 标记的 XML 文件,并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受以逗号分隔的资源名称列表。可以将星号字符用作通配符。

 
    

 将该文件保存在项目资源中,例如,保存在 res/raw/keep.xml。构建系统不会将此文件打包到 APK 中。

(8)启用严格引用检查

 通常,资源缩减器可以准确地判断是否使用了某个资源。不过,如果您的代码会调用 Resources.getIdentifier()(或者您的任何库会进行此调用,例如 AppCompat 库便会执行此调用),则意味着您的代码将根据动态生成的字符串查询资源名称。当您启用严格引用检查时,资源缩减器在默认情况下会采取保护性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。
 例如,以下代码会将所有带 img_ 前缀的资源标记为已使用。

  val name = String.format("img_%1d", angle + 1)
  val res = resources.getIdentifier(name, "drawable", packageName)

 这是默认启用的安全缩减模式的一些示例。 不过,您可以停用这种“防患于未然”的处理方式,指定资源缩减器只保留确定要使用的资源。 为此,您可以将 keep.xml 文件中的 shrinkMode 设置为 strict,如下所示:


    

 如果您确实启用了严格缩减模式,并且您的代码也通过动态生成的字符串引用资源(如上所示),则您必须使用 tools:keep 属性来手动保留这些资源。

(9)移除未使用的备用资源

 Gradle 资源缩减器只会移除未由您的应用代码引用的资源,这意味着,它不会移除用于不同设备配置的备用资源。如有必要,您可以使用 Android Gradle 插件的 resConfigs 属性来移除应用不需要的备用资源文件。
 以下代码段展示了如何设置只保留英语和法语的语言资源:

android {
        defaultConfig {
            ...
            resConfigs "en", "fr"
        }
    }
(10)合并重复资源

 默认情况下,Gradle 还会合并同名的资源,如可能位于不同资源文件夹中的同名可绘制对象。这一行为不受 shrinkResources 属性控制,也无法停用,因为当多个资源与代码查询的名称匹配时,有必要利用这一行为来避免错误。
 只有在两个或更多个文件具有完全相同的资源名称、类型和限定符时,才会进行资源合并。Gradle 会在重复项中选择它认为最合适的文件(根据下述优先顺序),并且只将这一个资源传递给 AAPT,以便在 APK 文件中分发。
 Gradle 会在以下位置查找重复资源:

  • 与主源集关联的主资源,一般位于 src/main/res/ 中。
  • 来自版本类型和版本变种的变体叠加层。
  • 库项目依赖项。

 Gradle 会按以下级联优先顺序合并重复资源:
依赖项 → 主资源 → 版本变种 → 版本类型

 如果完全相同的资源出现在同一源集中,Gradle 无法合并它们,并且会发出资源合并错误。如果您在 build.gradle 文件的 sourceSet 属性中定义了多个源集,就可能会发生这种情况。例如,如果 src/main/res/ 和 src/main/res2/ 包含完全相同的资源,就可能会发生这种情况。

(11)对代码进行混淆处理

 混淆处理的目的是通过缩短应用的类、方法和字段的名称来减小应用的大小。

R8 对代码进行混淆处理

 混淆处理不会从应用中移除代码,但如果应用的 DEX 文件将许多类、方法和字段编入索引,那么混淆处理将可以显著缩减应用的大小。
 此外,如果您的代码依赖于应用的方法和类的可预测命名(例如,使用反射时),您应该将相应签名视为入口点并为其指定保留规则。这些保留规则会告知 R8 不仅要在应用的最终 DEX 中保留该代码,而且还要保留其原始命名。

(12)解码经过混淆处理的堆栈轨迹

 R8 对您的代码进行混淆处理后,理解堆栈轨迹的难度将会极大增加,因为类和方法的名称可能已发生变化。 除了重命名之外,R8 还可以更改堆栈轨迹中的行号,以在写入 DEX 文件时进一步缩减大小。 幸运的是,R8 在每次运行时都会创建一个 mapping.txt 文件,其中列出了经过混淆处理的类、方法和字段名称与原始名称的映射关系。此映射文件还包含用于将行号映射回原始源文件行号的信息。R8 会将此文件保存在 /build/outputs/mapping// 目录中。

注意 :您每次构建项目时都会覆盖 R8 生成的 mapping.txt 文件,因此您每次发布新版本时都要注意保存一个该文件的副本。 通过为每个发布版本保留一个 mapping.txt 文件的副本,您可以在用户提交来自旧版应用的经过混淆处理的堆栈轨迹时,调试相关问题。

(13)代码优化

 为了进一步缩减应用,R8 会在更深的层次上检查代码,以移除更多不使用的代码,或者在可能的情况下重写代码,以使其更简洁。下面是此类优化的几个示例:

  • 如果您的代码从未采用过给定 if/else 语句的 else {} 分支,R8 可能会移除 else {} 分支的代码。
  • 如果您的代码只在一个位置调用某个方法,R8 可能会移除该方法并将其内嵌在这一个调用点。
  • 如果 R8 确定某个类只有一个唯一的子类且该类本身未实例化(例如,一个仅由一个具体实现类使用的抽象基类),它就可以将这两个类合并在一起并从应用中移除一个类。
  • 要了解详情,请阅读 Jake Wharton 撰写的关于 R8 优化的博文。

 R8 不允许您停用或启用离散优化,也不允许您修改优化的行为。事实上,R8 会忽略试图修改默认优化行为的所有 ProGuard 规则,例如 -optimizations 和 - optimizationpasses。 此限制很重要,因为随着 R8 的不断改进,维护标准的优化行为有助于 Android Studio 团队轻松排查并解决您可能遇到的任何问题。

(14)启用更积极的优化

 R8 包含一组额外的优化功能,这些功能在默认情况下处于停用状态。 您可以通过在项目的 gradle.properties 文件中添加以下代码来启用这些额外的优化功能:

android.enableR8.fullMode=true

 这些额外的优化功能会使 R8 的行为与 ProGuard 不同,因此可能会需要您添加额外的 ProGuard 规则来避免运行时问题。例如,假设您的代码通过 Java Reflection API 引用一个类。默认情况下,R8 会假设您打算在运行时检查和操纵该类的对象(即使您的代码实际上并不这样做),因此它会自动保留该类及其静态初始化程序。
 不过,在使用“完整模式”时,R8 不会做出这种假设,如果 R8 断定您的代码从不在运行时使用该类,它会将该类从应用的最终 DEX 中移除。也就是说,如果您要保留该类及其静态初始化程序,则需要在规则文件中添加相应的保留规则。

(15)生成移除的(或保留的)代码的报告

 为了便于排查特定的 R8 问题,建议您查看 R8 从您的应用中移除的所有代码的报告。请针对要为其生成此报告的每个模块,将 -printusage /usage.txt 添加到您的自定义规则文件中。当您在 启用 R8 的情况下构建应用时,R8 会以您指定的路径和文件名输出报告。也可以在下面路径中查看:

R8 生成代码报告
(16)排查资源缩减问题

 Gradle 会在 /build/outputs/mapping/release/(ProGuard 输出文件所在的文件夹)中创建一个名为 resources.txt 的诊断文件。此文件包含一些详细信息,比如,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除。

一起来查看自己的 R8 吧~

小编的扩展链接

  • SamplePop代码下载
  • 《Android 性能优化 全家桶》

参考链接

  • 谷歌官网 -> 这是应该读的第一篇文章
  • 谷歌Play -> 对崩溃的堆叠追踪进行反混淆处理
  • Jake Wharton -> R8 优化的博文
回眸一笑百媚生,六宫粉黛无颜色

举手之劳,赞有余香! ❤ 比心 ❤

你可能感兴趣的:((十)Android 性能优化 Proguard / R8)