Android R8

版本: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 只处理第一种实现。 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 节省 字节指令。
  16. ...

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

Obfuscate

混淆跟 ProGuard 类似。 支持字典的自定义。 不同是 r8 在开启保留签名(Signature)会保留内部类的类名的时候同时会保留外部类的类名,使两个类类名保持内外类的命名关系。
r8 在这原来的基础上支持对行号进行优化。尽可能把所有方法的开始行号映射为1 。
mapping 文件变为

    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 的支持。

小结

r8 已经足够的出色了。但是过于苛刻的保留规则导致之前规则并不能无条件的适应。当前输出只支持 Dex 。 导致该工具不能应用在其他的 java 项目上。较为可惜。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容