版本:1.4.94
地址:r8

介绍

r8包含了D8 的功能, 实现了对 java 字节码优化,混淆并转换成 dex 文件的功能。 可以很好的替代了 ProGuard 的在 Android 编译工具链上的应用。 同时生成的 dex 文件更为轻小。

r8 主要分为 5 个阶段: Read Input,Configuration,Shrink ,Optimize,Write Dex
代码入口 com.android.tools.r8.R8

Read Inputs

相对于 ProGuard 只支持对 class 文件的解析 。
r8 支持对 dex 和 class 文件的解析。 class 文件使用 ASM 框架进行解析。dex 文件直接操作 2 进制的方案进行解析。 dex 版本支持 v35 (android 2-5) v37 (android 6-7) v38 (android 8) v39(android 9)。

dex 生成的版本取决 app 的 minSdkVersion。dex 之所以存在多个版本。

  1. dex 在新的版本中引入了新的字节码。
  2. google 会收集特定指令排序在特定版本虚拟机上运行的 bug 。 从而在生成对应版本 dex 的时候规避掉这些 bug。

相关字节码列表可以查看链接 dalvik-bytecode#instructions

Configuration

工具的运行少不了配置的使用。为了能平滑的替换 ProGuard 的功能。 r8 兼容大部分的 ProGuard rule 。同时扩展了 rule 定义。
支持的主要有 keep,if,repackageclasses,flattenpackagehierarchy,overloadaggressively,allowaccessmodification,basedirectory,obfuscationdictionary,classobfuscationdictionary,packageobfuscationdictionary,useuniqueclassmembernames,keepdirectories,renamesourcefileattribute,keepattributes,keeppackagenames,keepparameternames,printconfiguration,printmapping,applymapping,printseeds…
r8 不支持大部分的优化配置。 但是 新增了新的 rule 来扩展之前的优化功能。
forceinline,neverinline,neverclassinline,nevermerge

这里对 rule 的含义不做过多解释。 可以查看 ProGuard 官方文档 ProGuard usage 或查看之前的文章 ProGuard 初探

Shrink

移除未被使用的类、字段、方法和属性。这里和 ProGuard 一样不会对方法签名或指令进行裁剪。
在处理方法的时候, 需要注意以下几个

  1. Kotlin 的反射
    Kotlin 的反射是基于解析注解实现的。Kotlin 经过 kotlinc 生成的 class 文件包含一个注解 @kotlin.Metadata。 由编译器生成, 记录 Kotlin 源文件的基本信息。Kotlin 运行时使用 ReadKotlinClassHeaderAnnotationVisitor 对 @kotlin.Metadata 注解进行解析。 所以 Kotlin 反射非常慢。在处理 Kotlin 在混淆和裁剪的时候。 需同步修改 @kotlin.Metadata 里面的定义。r8 在这里使用 Kotlin 的官方库 kotlinx-metadata-jvm 操作 @kotlin.Metadata 元素。

  2. Lambda 表达式
    Lambda 是在 java 8 上引入的。如果单纯要实现 Lambda 的效果,技术方法其实有很多种。 最终使用 invokedynamic 主要有两点,一是更稳定的文件格式。 二是更灵活的转换策略,Lambda 的转换策略由运行期决定的。
    Lambda 分为编译期和运行期。
    编译期:
    a. javac 对 Lambda 生成一个 invokedynamic 指令,该指令指向一个 BootstrapMethods 方法,
    b. 将 Lambda 方法内代码转移到该类的一个私有方法内。
    c. BootstrapMethods 方法指向生成的的私有方法。
    BootstrapMethods
    运行期:
    执行 invokedynamic 。 会执行 invokedynamic 指向的 BootstrapMethods 定义的方法返回 CallSite 。 Lambda 返回 CallSite 的方法是 LambdaMetafactory.metafactory 或 LambdaMetafactory.altMetafactory 。默认情况下使用 metafactory ,当你的 Lambda 实现了多个接口时,将使用 altMetafactory 返回。 最终返回一个实现了该接口的实现类。 这个实现类是由运行期 ASM 动态生成的,该类主要是做一个转发的功能, 将方法和参数转发给 c 生成的私有方法。
    所以在保留 invokedynamic 字节码的时候,需要同步保留 invokedynamic 指向的的 BootstrapMethods 以及BootstrapMethods 指向的私有方法。
    这里还存在一个问题。 javac 生成的是一个私有方法。 一个外部类是怎样调用另外一个类的私有方法?

  3. 关于 java 反射
    r8 对于反射是在最近几个版本支持的, 支持以下 api
    AtomicIntegerFieldUpdater.newUpdater
    AtomicLongFieldUpdater.newUpdater
    AtomicReferenceFieldUpdater.newUpdater
    Class.forName
    SomeClass.getName
    SomeClass.getCanonicalName
    SomeClass.getSimpleName
    SomeClass.getTypeName
    SomeClass.getField
    SomeClass.getDeclaredField
    SomeClass.getMethod
    SomeClass.getDeclaredMethod
    相比于 ProGuard 使用模板匹配的方式。 r8 将代码转成 中间表现 IR 通过 SSA 的方式对代码进行分析。因为使用代码分析所以 r8 跟踪反射功能的适应性比 ProGuard 好。在反射优化中 r8 和 ProGuard 对于构造方法均只能识别无参构造方法, 对于其他的构造方法在这都是无能为力。

  4. r8 部分支持对 ServiceLoader 机制。
    ServiceLoader JSP(Service Provider Interfaces)。 ServiceLoader 的实现有两种版本。 一种是在 JDK 9 以下。 通过定义一个接口。同时将继承该接口的实现类将记录在META-INF/services 接口同名文件下。第二种是在 JDK 9 上面用于支持 java9 模块化下不同模块的通信。 实现类信息记录在 module-info.java 下。 对于 r8 只处理第一种实现。r8 会保留用到的services,并保留他们的无参构造函数。 java9 暂不在 r8 的支持范围内。
  5. r8 可以删除可见的桥接方法
    当允许修改访问权限,可见的桥接方法将被删除。
    https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6342411
    可见的桥接方法是为了解决 public class 继承了一个私有类时。 反射调用存在该类父类的 public 方法出现的 IllegalAccessException 错误。
    我们只要修改父类为 public 就能安全的删除桥接方法而不会有任何的影响。

  6. r8 的 Shrink 规则和 ProGuard 相同。 首先计算所有 keep rule 定义的根节点。 从这些根节点发散出去。
    对于 Externalizable 和 Serializable 需要额外的处理。 Externalizable 需要保留无参构造方法。 Externalizable 存在两个方法 readExternal 和 writeExternal 用来自定义序列化中的操作。 r8 默认会把这两个方法干掉。 ProGuard 则会将它保留。 原因是 r8 认为 readExternal 和 writeExternal 没有被调用过。而 ProGuard 认为你继承了 Externalizable 那么你就有义务保留它的重写方法。

ProGuard 和 r8 对比

  1. 保留一个 class r8 仅仅保留 静态初始化 cinit 的方法,而 ProGuard 同步保留他们的无参构造方法。
  2. 一个虚方法被保留 ProGuard 将保留整条继承数上的该方法。 r8 的仅仅保留该方法。 只在该方法调用 super 才会保留父类的虚方法。
  3. 一个类被 keep 。r8 会同步 keep 它的父类以及他们的接口。但是也只是仅仅 keep 住他们接口本身。 ProGuard 是 keep 他的父类。而接口并不会主动 keep 。 接口的 keep 是在接口方法被调用的时候。

Optimize

r8 使用 SSA (静态单一赋值)对代码进行优化。

如果有需要在这个阶段将进行 java8 脱糖。

  1. Lambda
    由上面的流程介绍可知, javac 生成的方法是私有。需要修改方法为 public 。 同时需要将 ASM 生成的实现类落地。 将对应调用点转成对应的方法。

  2. 接口的默认方法
    为有默认方法的接口生成一个新的类,类名在原有的基础上加入后缀 -CC。迁移默认方法。同时方法名加入前缀 $default$。同时将调用点转换成调用新的静态方法。

优化项

  1. 优化 StringBuilder
  2. 优化 String 指令
  3. 简化 if 指令。
  4. 清理桥接方法。
  5. 删除没有影响的方法的调用
  6. 合并 class
  7. 删除未被使用方法参数
  8. 优化 枚举 switch
  9. 删除未可达的代码
  10. 删除强转指令
  11. 删除 assert 指令生成的方法。
  12. 折叠 常量数字的 算数运算或 逻辑运算
  13. 兼容高版本 api 在低版本没有的问题。
  14. 内连方法
  15. new-array 指令转换 fill-array-data / filled-new-array 节省 字节指令。

这一块的代码是基于老版本的分析。后续会有更为详细的分析。
— 待续 —

Obfuscate

混淆跟 ProGuard 类似。 支持字典的自定义。 不同是 r8 在开启保留签名(Signature)会保留内部类的类名的时候同时会保留外部类的类名,使两个类类名保持内外类的命名关系。
r8 在这原来的基础上支持对行号进行优化。尽可能把所有方法的开始行号映射为1 。只所以是尽可能。 只所以是尽可能,而不是全部,因为涉及到堆栈反解的问题。
mapping 信息变为

1
2:2:android.arch.core.internal.SafeIterableMap$Entry get(java.lang.Object):45:45 -> a

前面为映射后行号, 后面为源码中行号。

优化行号的好处在于可以合并相同的 debug_info_item。这个方案有点类似于之前的支付宝瘦身。 但合并效率当然会有不如。

r8 其他使用。

ProGuard 在 Android 工具链上的应用不仅仅用在代码优化混淆上。同时也用在 mainDexList 的计算。 r8 同样支持对 mainDexList 计算。甚至 mainDexList 文件可以不落地。 但是 r8 计算的 mainDexList 列表会比 ProGuard 计算出来的还多。 因为它不仅保留了所有代码入口发散出去的类,以及他们的直接引用。r8 还保留了所有的带枚举的注解。以及被这该注解标记的类。

至此 r8 已经能接管所有 ProGuard 的功能。
proguard

r8

java 编译到 dex 的过程中。还有一个 javac 这一个非 Google 的工具链。 或许后续可能会升级 javac 用以对 dex api 的支持。

小结

r8 已经足够的出色了。但是过于苛刻的保留规则导致之前规则并不能无条件的适应。较为可惜的是当前输出只支持 Dex 。导致该工具不能应用在其他的 java 项目上。r8 的发展还是很迅速的。反射的支持和 ProGuard 相关参数的支持都相当迅速。 现阶段已经来到 2.1 的版本。是时候从 ProGuard 切换到 r8 上。