原文发表于 http://taobaofed.org/blog/2016/05/05/new-compiler-for-android/
作者: 凯冯 发表于: 2016-05-05
2016 年 3 月 10 日, Google 向外界发布了 Android N 的预览版,并宣布了 Android N 的 Roadmap,Android N 的最终版源代码将于今年 8 或 9 月份释出到 AOSP 项目。
在众多的 Android N 新特性中,有一项新工具链的出现与 Android 生态圈的所有开发者息息相关,即 Jack & Jill 编译器的引入。
在依赖了 Sun/Oracle 的 Java 编译器十年之后,Android 终于有了自己的 Java 编译器。
本文试图对市面上非常有限的资料进行总结,向大家介绍 Jack & Jill 的缘起,工作方式和原理。
Jack 是 Java Android Compiler Kit 的缩写,它可以将 Java 代码直接编译为 Dalvik 字节码,并负责 Minification, Obfuscation, Repackaging, Multidexing, Incremental compilation。它试图取代 javac/dx/proguard/jarjar/multidex 库等工具。
git 源代码地址是 https://android.googlesource.com/toolchain/jack。
Jill 是 Jack Intermediate Library Linker 的缩写,它负责 “Shielding JACK from Java byte code”;实际上辅助 Jack 对.class 做预处理,生成 .jack 文件
git 源代码地址是 https://android.googlesource.com/toolchain/jill。
虽然 Google 是在宣布 Android N 预览版时隆重介绍了Jack & Jill。但是,早在 2014 年 Google 就对外宣布了新编译器 Jack 的存在 meet our new experimental toolchain, 它的开发启动时间更是远远早于 2014 年。
下面是我总结的 Jack 的缘起
一家名叫 FlexyCore 的小公司基于 GCC toolchain 开发了 Android 平台上的 AOT 编译器,被 Google 看中并于 2013 年被收购
FlexyCore team 基于 LLVM toolchain 开发了 ART,并成为 Android 5.0 之后的缺省 Java Runtime
FlexyCore team 基于 Eclipse ecj 编译器开始开发 Jack,基于 ASM4 开发 Jill。 他们早在 2014 年 2 月就开始提交 Jill 的代码了 Jill initial commit; 3 月份开始提交 Jack的代码 Jack initial commit
自 Android build-tools 21.1 开始,里面已经内置 jack.jar 和 jill.jar
Android Gradle plugin 自 0.14 开始支持 Jack & Jill initial commit
自 Android 6.0 开始,Jack & Jill 成为 AOSP 的官方编译器, 也就是说所有的 Android 6.0 ROM 都是 Jack 编译出来的 link,也代表 Google 认为 Jack 达到了一定的成熟度
预计等 Android 7.0 正式发布时,Jack 可能会成为官方推荐的编译器
为什么要抛弃 Javac/dx,开发 Jack 和 Jill
据个人推测主要有三个目的
提高编译速度
应对 Oracle 的法律诉讼
将编译器掌控权拿在自己手中,不再受制于 Oracle,可以做一些 Android only 的优化
下面比较一下旧的 javac/dx/ProGuard/jarjar toolchain 和新的 Jack 编译器的工作流程
简单的说,将 Java 代码和依赖库编译为 dex 有两个大的阶段
javac (.java –> .class) –> dx (.class –> .dex)
下面是用流程图表示的旧编译过程
javac 将 java 代码编译为 java bytecode, 以 .class 的形式存在; 以 jar 和 aar 形式存在的依赖库,代码在里面以一堆.class 的形式存在
Proguard 工具读取 Proguard 配置,对 .class 做 shrinking, obfuscation,输出 Proguard mapping
dx 将多个 .class 转化为单一的 classes.dex ; 如果 dex 方法数超过 65k, 就生成 classes.dex, classes1.dex…classesN.dex
新的编译过程只有一个阶段了,它完全抛弃了 javac, ProGuard, jarjar 等工具,一个工具搞定一切
Jack (.java –> .jack –> .dex)
下面是用流程图表示的 Jill 预处理过程
下面是用流程图表示的 Jack 编译过程
各种依赖库仍然以 jar/aar 的形式存在
辅助工具 Jill 将根据依赖库中的 .class 生成 Jayce 格式的 IL,并调用 Jack 做 pre-dex 并生成 .jack,此过程只在编译 app 时发生一次
Jack 将 java 源代码也编译为 .jack,然后将多个 .jack 转化为单一的 .dex; 如果 dex 方法数超过 65k, 就生成 classes.dex, classes1.dex…classesN.dex
pre-dex 的详细解释可以参阅此链接 new-build-system
Improving Build Server performance.
The Gradle based build system has a strong focus on incremental builds. One way it is doing this in doing pre-dexing on the dependencies of each modules, so that each gets turned into its own dex file (ie converting its Java bytecode into Android bytecode). This allows the dex task to do less work and to only re-dex what changed and merge all the dex files.
.Jack 的具体格式如下图所示
可见里面包含了 Jayce 格式的 IL ,pre-dex,原始 aar 中的资源文件,以及 Jack 会用到的一些 meta 信息
下图简单比较了 java 代码转化的 .class, Jayce IL 和 dex 的内容异同
简单比较下三种 IL 的区别:
Sun/Oracle Hotspot VM 是基于栈式的,所以 .class 文件的内容就是不断地压操作数到栈顶,从栈顶读取操作数,比较或做运算,将结果再压回栈顶
Dalvik VM 是基于寄存器的,所以 .dex 的内容就是不断地 move 操作数到寄存器,比较或做运算,将结果写回寄存器或内存地址
Jayce 则是 Jack&Jill 专有的 IL, 目前没有查阅到更多的官方资料。只能参阅 Jill 源代码中 com.android.jill.backend.jayce 包的代码了,比如其中的 Token 类就定义了 Jayce 的 Token 定义。
个人推测 Jayce 存在的意义是:
为了在整合多个 jack 文件,生成单一的 dex 时,方便 Jack 做一些全局性的后端编译优化。
从 Android 生态圈中完全去除 Oracle 的 Java Bytecode 格式
对依赖库做 pre dex,且成果会被保存到 build/intermediates/jill/debug 目录。
之后的编译过程中,只要依赖库的数目和版本不变,之前的 pre dex 成果会被复用;Jack 只需要编译变化的源代码,然后对多个 dex 进行 merge 即可,能够加速整个编译过程。
编译时会启动一个 Jack compilation server,并开启并行编译
Jack 文档是这么介绍的
This server brings an intrinsic speedup, because it avoids launching a new host JRE JVM, loading Jack code, initializing Jack and warming up the JIT at each compilation. It also provides very good compilation times during small compilations (e.g. in incremental mode).
The server is also a short-term solution to control the number of parallel Jack compilations, and so to avoid overloading your computer (memory or disk issue), because it limits the number of parallel compilations.
支持 Java 8 的一部分特性
Jack 由 Google 完全掌控,未来可能成为 Android sdk 的默认编译器
向后兼容到 Android 2.3
不再需要独立的 ProGuard。Jack 支持读取旧的 ProGuard 配置,完成 shrinking, obfuscation 的工作
不再需要独立的 jarjar。Jack 支持读取旧的 jarjar 配置,完成 repackaging 的工作
没有 .class 文件了,直接操纵或读取 Java 字节码的各种工具如 JaCoCo/Lint/Mokito/Retrolambda 没有了用武之地。但是仍然可以在 Android Library 上使用这些工具,编译为 aar/jar 后作为 Jill 的输入
annotation processors 如 Dagger, ButterKife 仍可以使用
Scala/Kotlin 等第三方 JVM 语言编写的内容必须先被 Jill 处理,再作为 Jack 的输入
暂时还不支持 Android Studio 2.0 的 Instant Run 特性
暂时还不支持 data binding
当你的 app 足够复杂之后,在打包时常常会遇到这种错误提示
Unable to execute dex: method ID not in [0, 0xffff]: 65536
为什么方法数目不能超过 65k 呢?有人说是 dexopt 的问题,有人说是 dex 格式的限制,下面我们看看这个 log 到底是哪里吐出来的,然后分析下具体原因。
dex 格式的限制?
首先我们看一下 dex 的结构定义
//Direct-mapped "header_item" struct.
structDexHeader {
...
u4 methodIdsSize;
...
};
//These match the definitions in the VM specification.
typedefuint32_tu4;
可见 dex 文件结构是用 32 位来存储 method id 的,最大支持 2 的 32 次方,因此 65k 的原因不在于此。
dexopt 的原因?
dexopt 是 app 已经打包成功,安装到手机之后才会发生的过程。但是 65k 问题是在打包时发生的,所以问题原因也不在此
一般提到的 dexopt 错误,其实是 Android 2.3 及其以下在 dexopt 执行时只分配 5M 内存,导致方法数目过多(数量不一定到 65k)时在 odex 过程中崩溃,官方称之为 Dalvik linearAlloc bug(Issue 22586) 。
另:这个 linearAlloc 的限制不仅存在于 dexopt 里,还在 dalvik rumtime 中存在……
以下链接详细解释了此问题:https://github.com/simpleton/dalvik_patch
错误 log 是哪里吐出来的?
//MemberIdsSection.java
if(items().size() > DexFormat.MAX_MEMBER_IDX +1) {
thrownewDexIndexOverflowException(getTooManyMembersMessage());
}
/*
Maximum addressable field or method index.
The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or meth@CCCC.
*/
publicstaticfinalintMAX_MEMBER_IDX =0xFFFF;
通过查阅 dalvik-bytecode 可知,@CCCC 的范围必须在 0~65535 之间。
所以归根结底,65k 问题是因为 dalvik bytecode 中的指令格式使用了 16 位来放 @CCCC 导致的;所以,不仅 Method 数目不能超过 65k, Field 和 Class 数目也不能超过 65k。
前文已经很清楚地解释了 65k 问题的由来,可见只要 dalvik bytecode 指令格式不升级,65k 问题是逃不掉的。
Jack 官网对 65k 问题是这么说的:
Multidex support
Since dex files are limited to 65K methods, apps with over 65K methods must be split into multiple dex files. (See ‘Building Apps with Over 65K Methods’ for more information about multidex.)
Jack offers native and legacy multidex support.
所以,Jack 和旧工具链对 multidex 的支持方式是相同的
被 Jack 编译出来的 app 执行时也和以前一样
若是 dalvik 虚拟机,它只支持读取一个 classes.dex。而 multidex 解决方案会读取多个 .dex,帮我们做 dex 数组合并
若是 art 虚拟机,它会扫描 classes.dex, classes1.dex…classesN.dex,调用 dex2oat 转化为单一的 oat
以 lambda 表达式为例
Interface lambda = i -> i +1;
会被转化为 anonymous classes
Interface lambda =newInterface() {
publicintm(inti){
returni +1;
}
};
Jack当前支持的 Java 8 特性可参见 j8-jack。
如何在 Gradle 脚本中使用 Jack 编译器编译 app
想使用 Jack 和 Jill 需要指定你的 Build Tools version 是 21.1.0+, Gradle plugin version 是1.0.0+。
以下的配置是我个人测试通过的配置
使用 Android Gradle 插件 2.1.0-alpha2
dependencies {
classpath'com.android.tools.build:gradle:2.1.0-alpha2'
}
使用以下版本的 sdk 和 build-tool
compileSdkVersion'android-N'
buildToolsVersion'24.0.0 rc1'
在 defaultConfig 中指定用 Jack
defaultConfig {
jackOptions {
enabledtrue
}
}
使用 gradle 2.10 以上
distributionUrl=http\://mirrors.taobao.net/mirror/gradle/gradle-2.10-bin.zip
使用 Android Studio 2.1 (preview) 或者命令行编译
可能需要提升 javaMaxHeapSize
dexOptions{
javaMaxHeapSize"2g"
}
经过测试,当前版本(2016/03/15)的 Jack 编译器比起 Javac+dx 在编译时间,编译出的 apk 体积,编译出的 apk 的性能上暂时并没有优势。
但是,可以期待 Google 将在 Jack 编译器上做大量的智力投资,Jack 的未来是光明的。
下图是 guardsquare 公司对 Javac+dx 和 Jack 做的对比测试
对于不 proguard 的 clean build,javac/dx 耗时 56s, jack 耗时 1 m 48 s;之所以 jack 这么慢是因为它要做大量的 pre-dex。
对于不 proguard 的 clean build,javac/dx 和 jack 编译出来的 app 性能相差无几。
对于共用 proguard 配置文件情况,javac/dx 和jack 编译出来的 app 体积也差不多。
我个人测试的编译速度 / apk 体积等对比也大致如此,在此不再赘述.
虽然 Jack 编译器的现状并不出彩,但是它终究有一天会成为 Android app 的官方推荐编译器。
期待 Google Android team 加倍努力,让这一天早日到来。
https://www.guardsquare.com/blog/the_upcoming_jack_and_jill_compilers_in_android
http://source.android.com/devices/tech/dalvik/dex-format.html
http://tools.android.com/tech-docs/jackandjill
https://developer.android.com/intl/zh-cn/tools/building/multidex.html