r8包含了D8 的功能, 实现了对 java 字节码优化,混淆并转换成 dex 文件的功能。 可以很好的替代了 ProGuard 的在 Android 编译工具链上的应用。 同时生成的 dex 文件更为轻小。
r8 主要分为 5 个阶段: Read Input,Configuration,Shrink ,Optimize,Write Dex
代码入口 com.android.tools.r8.R8
相对于 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 之所以存在多个版本。
相关字节码列表可以查看链接 dalvik-bytecode#instructions 。
工具的运行少不了配置的使用。为了能平滑的替换 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 初探
移除未被使用的类、字段、方法和属性。这里和 ProGuard 一样不会对方法签名或指令进行裁剪。
在处理方法的时候, 需要注意以下几个
Kotlin 的反射
Kotlin 的反射是基于解析注解实现的。Kotlin 经过 kotlinc 生成的 class 文件包含一个注解 @kotlin.Metadata。 由编译器生成, 记录 Kotlin 源文件的基本信息。Kotlin 运行时使用 ReadKotlinClassHeaderAnnotationVisitor 对 @kotlin.Metadata 注解进行解析。 所以 Kotlin 反射非常慢。在处理 Kotlin 在混淆和裁剪的时候。 需同步修改 @kotlin.Metadata 里面的定义。r8 在这里使用 Kotlin 的官方库 kotlinx-metadata-jvm 操作 @kotlin.Metadata 元素。
Lambda 表达式
Lambda 是在 java 8 上引入的。如果单纯要实现 Lambda 的效果,技术方法其实有很多种。 最终使用 invokedynamic 主要有两点,一是更稳定的文件格式。 二是更灵活的转换策略,Lambda 的转换策略由运行期决定的。
Lambda 分为编译期和运行期。
编译期:
a. javac 对 Lambda 生成一个 invokedynamic 指令,该指令指向一个 BootstrapMethods 方法,
b. 将 Lambda 方法内代码转移到该类的一个私有方法内。
c. BootstrapMethods 方法指向生成的的私有方法。
运行期:
执行 invokedynamic 。 会执行 invokedynamic 指向的 BootstrapMethods 定义的方法返回 CallSite 。 Lambda 返回 CallSite 的方法是 LambdaMetafactory.metafactory 或 LambdaMetafactory.altMetafactory 。默认情况下使用 metafactory ,当你的 Lambda 实现了多个接口时,将使用 altMetafactory 返回。 最终返回一个实现了该接口的实现类。 这个实现类是由运行期 ASM 动态生成的,该类主要是做一个转发的功能, 将方法和参数转发给 c 生成的私有方法。
所以在保留 invokedynamic 字节码的时候,需要同步保留 invokedynamic 指向的的 BootstrapMethods 以及BootstrapMethods 指向的私有方法。
这里还存在一个问题。 javac 生成的是一个私有方法。 一个外部类是怎样调用另外一个类的私有方法?
关于 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 对于构造方法均只能识别无参构造方法, 对于其他的构造方法在这都是无能为力。
r8 可以删除可见的桥接方法
当允许修改访问权限,可见的桥接方法将被删除。
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6342411
可见的桥接方法是为了解决 public class 继承了一个私有类时。 反射调用存在该类父类的 public 方法出现的 IllegalAccessException 错误。
我们只要修改父类为 public 就能安全的删除桥接方法而不会有任何的影响。
r8 的 Shrink 规则和 ProGuard 相同。 首先计算所有 keep rule 定义的根节点。 从这些根节点发散出去。
对于 Externalizable 和 Serializable 需要额外的处理。 Externalizable 需要保留无参构造方法。 Externalizable 存在两个方法 readExternal 和 writeExternal 用来自定义序列化中的操作。 r8 默认会把这两个方法干掉。 ProGuard 则会将它保留。 原因是 r8 认为 readExternal 和 writeExternal 没有被调用过。而 ProGuard 认为你继承了 Externalizable 那么你就有义务保留它的重写方法。
r8 使用 SSA (静态单一赋值)对代码进行优化。
如果有需要在这个阶段将进行 java8 脱糖。
Lambda
由上面的流程介绍可知, javac 生成的方法是私有。需要修改方法为 public 。 同时需要将 ASM 生成的实现类落地。 将对应调用点转成对应的方法。
接口的默认方法
为有默认方法的接口生成一个新的类,类名在原有的基础上加入后缀 -CC。迁移默认方法。同时方法名加入前缀 $default$。同时将调用点转换成调用新的静态方法。
…
优化项
这一块的代码是基于老版本的分析。后续会有更为详细的分析。
— 待续 —
混淆跟 ProGuard 类似。 支持字典的自定义。 不同是 r8 在开启保留签名(Signature)会保留内部类的类名的时候同时会保留外部类的类名,使两个类类名保持内外类的命名关系。
r8 在这原来的基础上支持对行号进行优化。尽可能把所有方法的开始行号映射为1 。只所以是尽可能。 只所以是尽可能,而不是全部,因为涉及到堆栈反解的问题。
mapping 信息变为
2:2:android.arch.core.internal.SafeIterableMap$Entry get(java.lang.Object):45:45 -> a
前面为映射后行号, 后面为源码中行号。
优化行号的好处在于可以合并相同的 debug_info_item。这个方案有点类似于之前的支付宝瘦身。 但合并效率当然会有不如。
ProGuard 在 Android 工具链上的应用不仅仅用在代码优化混淆上。同时也用在 mainDexList 的计算。 r8 同样支持对 mainDexList 计算。甚至 mainDexList 文件可以不落地。 但是 r8 计算的 mainDexList 列表会比 ProGuard 计算出来的还多。 因为它不仅保留了所有代码入口发散出去的类,以及他们的直接引用。r8 还保留了所有的带枚举的注解。以及被这该注解标记的类。
至此 r8 已经能接管所有 ProGuard 的功能。
java 编译到 dex 的过程中。还有一个 javac 这一个非 Google 的工具链。 或许后续可能会升级 javac 用以对 dex api 的支持。
r8 已经足够的出色了。但是过于苛刻的保留规则导致之前规则并不能无条件的适应。较为可惜的是当前输出只支持 Dex 。导致该工具不能应用在其他的 java 项目上。r8 的发展还是很迅速的。反射的支持和 ProGuard 相关参数的支持都相当迅速。 现阶段已经来到 2.1 的版本。是时候从 ProGuard 切换到 r8 上。