• 在之前的文章Android高手笔记-包体积优化中提到过通过编译优化包体积,涉及到了ProGuard,D8,R8,其中关于ProGuard及包体积优化方案已经进行了详细介绍,那么今天我们来说说D8和R8;

D8

  • D8是一款用于取代 DX、更快的 Dex 编译器,可以生成更小的 APK;
开启D8的好处
  1. 编译更快、时间更短
  2. 编译时占用内存更小
  3. .dex文件更小
  4. .dex 文件拥有更好的运行时性能
  5. 支持在代码中使用 Java 8 语言
开启与关闭
  • Android Studio 3.0 需要主动在gradle.properties文件中新增:android.enableD8=true
  • D8作为DX的一个替代方案,Android Studio 3.1版本开始,将D8作为默认的Dex编译器。
  • 想关闭D8 ,可以在gradle.properties里添加如下配置:
android.enableD8=false //关闭D8恢复到DX-
android.enableD8.desugaring=false //恢复到以前的行为,让脱糖发生在Java编译之后,.class字节码仍遵循Java 7格式
执行增量构建
  • 为了在开发过程中提高构建速度(例如提高持续集成 build 的速度),可以指示 d8 仅编译项目的部分 Java 字节码;
  • 例如,如果启用了按类 dexing 处理,则只需重新编译自上次构建以来修改过的类(d8 无法自动检测哪些字节码文件已被修改过,因此您需要手动指定类列表):
//执行几个类的增量构建,并启用按类 dexing 处理,并为增量构建指定输出目录
d8 MainActivity.class R.class --intermediate --file-per-class --output ~/build/intermediate/dex
  • 可以使用 --main-dex-list 指定想让 d8 编译到主 DEX 文件中的类
d8 ~/build/intermediate/dex --release --main-dex-list ~/build/classes.txt --output ~/build/release/dex
支持Java8
  • 通过一个叫做“脱糖”的编译过程,将这些实用的语言功能转换为可以在 Android 平台上运行的字节码,D8脱糖就不会在transforms目录下生成desugar目录。
  • Android Studio 和 Android Gradle 插件包含了 d8 启用脱糖所需的类路径资源。
  • 从命令行使用 d8 时,需要手动添加一些资源:
  1. –lib:标记目标 Android SDK 中的 android.jar路径
  2. –classpath:标记项目的部分已编译的 Java 字节码,目前不打算将这部分字节码编译为 DEX 字节码,但在将其他类编译为 DEX 字节码时需要用到这些字节码。例如,如果代码使用默认和静态接口方法(一种 Java 8 语言功能),则需要使用此标记来指定您项目的所有 Java 字节码的路径,即使您不打算将所有 Java 字节码都编译为 DEX 字节码也是如此。这是因为 d8 需要根据这些信息来理解您项目的代码并解析对接口方法的调用
  • 示例对一个访问默认接口方法的类执行增量构建:
d8 MainActivity.class --intermediate --file-per-class --output ~/build/intermediate/dex
--lib android_sdk/platforms/api-level/android.jar
--classpath ~/build/javac/debug
Java8新特性:接口默认方法和静态方法
  • JDK1.8以前,接口(interface)没有提供任何具体的实现;
  • JDK1.8开始,接口允许定义默认方法和静态方法

R8

  • R8之前采用D8+ProGuard的形式构建,R8则将ProGuard和D8工具进行整合,目的是加速构建时间和减少输出apk的大小;
开启R8的好处
  1. 代码缩减(摇树优化):使用静态代码分析来查找和删除无法访问的代码和未实例化的类型,对规避 64k 引用限制非常有用;
  2. 资源缩减:移除不使用的资源,包括应用库依赖项中不使用的资源。
  3. 混淆代码:缩短类和成员的名称,从而减小 DEX 文件的大小
  4. 优化代码:检查并重写代码,选择性内联,移除未使用的参数和类合并来优化代码大小
  5. 减少调试信息 : 规范化调试信息并压缩行号信息。
  • R8 会自动执行上述编译时任务,也可以停用某些任务或通过 ProGuard 规则文件自定义 R8 的行为。
  • 使用某个第三方库时,通常只使用其中很小一部分。若不压缩,所有库代码都会保留在应用中。冗长的代码有时可以提高可读性和可维护性: 例如,使用有意义的变量名和建造者模式 来帮助其他人更容易检查和理解代码;但是这些模式会加大代码量,通常我们自己编写的代码有很大的压缩空间。
开启与关闭
  • Android Studio 3.3 需在项目的 gradle.properties 里加上:android.enableR8=true
  • Android Studio 3.4 或 Android Gradle 插件 3.4.0 及更高版本时,R8 是默认编译器(不再使用 ProGuard 执行编译时代码优化),用于将项目的 Java 字节码转换为在 Android 平台上运行的 DEX 格式。
  • 不过创建新项目时,缩减、混淆处理和代码优化功能默认处于停用状态。因为这些编译时优化功能会增加项目的构建时间,而且如果没有充分自定义要保留的代码,还可能会引入错误。
  1. 开启代码缩减,需要在应用的主 build.gradle 文件中将 minifyEnable 属性设置为 true
  2. 开启资源缩减:需要在应用的主 build.gradle 文件中将 shrinkResources 属性设置为 true
  • 资源缩减只有在与代码缩减配合使用时才能发挥作用。在代码缩减器移除所有不使用的代码后,资源缩减器便可确定应用仍要使用的资源,当添加包含资源的代码库时尤其如此。必须移除不使用的库代码,使库资源变为未引用资源,因而可由资源缩减器移除。
  1. 创建新项目或模块时,IDE 会创建一个 /proguard-rules.pro 文件,以便您添加自己的规则。
android {
    ...
    buildTypes {
        release {
            shrinkResources true //启用 R8 的资源缩减功能
            minifyEnabled true //启用 R8 的代码缩减功能
             proguardFiles
                //1. Android Gradle 插件会生成 proguard-android-optimize.txt(其中包含了对大多数 Android 项目都有用的规则),并启用 @Keep* 注解。
                getDefaultProguardFile('proguard-android-optimize.txt'),
                //2. 使用 Android Studio 创建新模块时,Android Studio 会在该模块的根目录中创建 proguard-rules.pro 文件
                'proguard-rules.pro'

                //3. AAR 库:<library-dir>/proguard.txt, JAR 库:<library-dir>/META-INF/proguard/
                //由于 ProGuard 规则是累加的,因此 AAR 库依赖项包含的某些规则无法移除,并且可能会影响对应用其他部分的编译。
                //例如,如果某个库包含停用代码优化功能的规则,该规则会针对整个项目停用优化功能。

                //4. Android 资源打包工具 2 (AAPT2):
                //使用 minifyEnabled true 构建项目后,AAPT2 会根据对应用清单中的类、布局及其他应用资源的引用,生成保留规则。
                //文件路径为:<module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt

                //5. 自定义配置文件:详见下面的添加其他配置
        }
    }
}
添加其他配置
  • 可以通过在相应的 productFlavor 代码块中再添加一个 proguardFiles 属性来添加每个构建变体专用的规则
android {
    ...
    buildTypes {
        release {
            ...
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
            ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}
  • flavor2 使用全部三个 ProGuard 规则,因为还应用了来自 release 代码块的规则。
关闭R8
  • 可以在gradle.properties里添加如下配置:
android.enableR8=false
开启R8完全模式
  • R8 普通模式是兼容 ProGuard的,若原项目里已使用了ProGuard,直接启用 R8 即可。同时,R8 也有完全模式,与ProGuard不直接兼容。
    可以在 gradle.properties 文件中另外设置以下内容:
android.enableR8.fullMode=true
  • 额外的优化功能会使 R8 的行为与 ProGuard 不同,因此可能会需要您添加额外的 ProGuard 规则,以避免运行时问题。
自定义要保留的代码
  • 在某些情况下,R8 很难做出正确判断,因而可能会移除应用实际上需要的代码:
1. 当应用通过 Java 原生接口 (JNI) 调用方法时
2. 当您的应用在运行时查询代码时(如使用反射)
- 反射 (Reflection) 会导致 R8 在跟踪代码时无法识别到代码的入口点
  • 如需修复错误并强制 R8 保留某些代码,在 ProGuard 规则文件中添加 -keep 代码行,如
-keep public class MyClass
  • 或者为要保留的代码添加 @Keep 注解
1. 在类上添加 @Keep 可按原样保留整个类
2. 在方法或字段上添加该注释,将使该方法/字段(及其名称)以及类名称保持不变。
3. 只有在使用 AndroidX 注解库且您添加 Android Gradle 插件随附的 ProGuard 规则文件时,此注解才可用。
  • 如需输出 R8 在构建项目时应用的所有规则的完整报告,请将以下代码添加到模块的 proguard-rules.pro 文件中:
// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt
自定义要保留的资源
  • 如果您有想要保留或舍弃的特定资源,请在项目中创建一个包含 标记的 XML 文件,并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受以逗号分隔的资源名称列表。您可以将星号字符用作通配符。
  • 将该文件保存在项目资源中,例如,保存在 res/raw/keep.xml 中。构建系统不会将此文件打包到应用中。
<?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" />
严格引用检查
  • 通常资源缩减器可以准确地判断是否使用了某个资源。不过如果代码中调用了 Resources.getIdentifier()(或者引用的任何库会执行此调用,例如 AppCompat 库便会执行此调用),这意味着代码将根据动态生成的字符串查询资源名称。资源缩减器在默认情况下(安全缩减模式)会采取保护行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。资源缩减器还会查看代码中的所有字符串常量以及各种 res/raw/ 资源,以查找格式类似于 file:///android_res/drawable//ic_plus_anim_016.png 的资源网址。如果它找到与此类似的字符串,或找到其他看似可用来构建与此类似的网址的字符串,则不会将它们移除。
  • 例如,以下代码会将所有带 img_ 前缀的资源标记为已使用:
val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)
  • 启用严格引用检查: 将 keep.xml 文件中的 shrinkMode 设为 strict,此时如果通过动态生成的字符串引用资源,必须使用 tools:keep 属性手动保留这些资源。
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />
移除未使用的备用资源
  • Gradle 资源缩减器只会移除未由应用代码引用的资源,这意味着,它不会移除用于不同设备配置的备用资源;
  • 例如使用的是包含语言资源的库(如 AppCompat 或 Google Play 服务),那么应用中将包含这些库中消息的所有已翻译语言的字符串,可以使用 resConfigs 属性移除应用不需要的备用资源文件,如设置只保留英语和法语的语言资源
android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}
合并重复资源
  • 默认情况下,Gradle 还会合并同名的资源(多个文件具有完全相同的资源名称、类型和限定符时)。这一行为不受 shrinkResources 属性控制,也无法停用,因为当多个资源与代码查询的名称匹配时,有必要利用这一行为避免错误。
  • Gradle 会在重复项中选择它认为最合适的文件(根据下述优先顺序),并且只将这一个资源传递给 AAPT,以便在最终工件中分发
  • Gradle 会按以下级联优先顺序合并重复资源:库项目依赖项 → 主资源 → 构建变种 → 构建类型, 如某个重复资源同时出现在主资源和构建变种中,Gradle 会选择构建变种中的资源。
  • 如果完全相同的资源出现在同一源代码集中,Gradle 无法合并它们,并且会发出资源合并错误,或者在 build.gradle 文件的 sourceSet 属性中定义了多个源代码集,src/main/res/ 和 src/main/res2/ 包含完全相同的资源也会报错。

参考

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章