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 库等工具。
Jill 是 Jack Intermediate Library Linker 的缩写,它负责 “Shielding JACK from Java byte code”;实际上辅助 Jack 对.class 做预处理,生成.jack
文件
虽然 Google 是在宣布 Android N 预览版时隆重介绍了Jack & Jill。但是,早在 2014 年 Google 就对外宣布了新编译器 Jack 的存在meet our new experimental toolchain, 它的开发启动时间更是远远早于 2014 年。
下面是我总结的 Jack 的缘起
据个人推测主要有三个目的
下面比较一下旧的 javac/dx/ProGuard/jarjar toolchain 和新的 Jack 编译器的工作流程
简单的说,将 Java 代码和依赖库编译为 dex 有两个大的阶段
javac (.java –> .class) –> dx (.class –> .dex)
下面是用流程图表示的旧编译过程
.class
的形式存在; 以 jar 和 aar 形式存在的依赖库,代码在里面以一堆.class 的形式存在.class
做 shrinking, obfuscation,输出 Proguard mapping.class
转化为单一的 classes.dex ; 如果 dex 方法数超过 65k, 就生成 classes.dex, classes1.dex…classesN.dex新的编译过程只有一个阶段了,它完全抛弃了 javac, ProGuard, jarjar 等工具,一个工具搞定一切
Jack (.java –> .jack –> .dex)
下面是用流程图表示的 Jill 预处理过程
下面是用流程图表示的 Jack 编译过程
.class
生成 Jayce 格式的 IL,并调用 Jack 做 pre-dex 并生成.jack
,此过程只在编译 app 时发生一次.jack
,然后将多个.jack
转化为单一的.dex
; 如果 dex 方法数超过 65k, 就生成 classes.dex, classes1.dex…classesN.dexpre-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 存在的意义是:
之后的编译过程中,只要依赖库的数目和版本不变,之前的 pre dex 成果会被复用;Jack 只需要编译变化的源代码,然后对多个 dex 进行 merge 即可,能够加速整个编译过程。
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. |
.class
文件了,直接操纵或读取 Java 字节码的各种工具如 JaCoCo/Lint/Mokito/Retrolambda 没有了用武之地。但是仍然可以在 Android Library 上使用这些工具,编译为 aar/jar 后作为 Jill 的输入当你的 app 足够复杂之后,在打包时常常会遇到这种错误提示
Unable to execute dex: method ID not in [0, 0xffff]: 65536 |
为什么方法数目不能超过 65k 呢?有人说是 dexopt 的问题,有人说是 dex 格式的限制,下面我们看看这个 log 到底是哪里吐出来的,然后分析下具体原因。
首先我们看一下 dex 的结构定义
//Direct-mapped "header_item" struct. struct DexHeader { ... u4 methodIdsSize; ... }; //These match the definitions in the VM specification. typedef uint32_t u4; |
可见 dex 文件结构是用 32 位来存储 method id 的,最大支持 2 的 32 次方,因此 65k 的原因不在于此。
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
//MemberIdsSection.java if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) { throw new DexIndexOverflowException(getTooManyMembersMessage()); } /* Maximum addressable field or method index. The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or meth@CCCC. */ public static final int MAX_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 执行时也和以前一样
.dex
,帮我们做 dex 数组合并以 lambda 表达式为例
Interface lambda = i -> i + 1; |
会被转化为 anonymous classes
Interface lambda = new Interface() { public int m(int i) { return i + 1; } }; |
Jack当前支持的 Java 8 特性可参见j8-jack。
想使用 Jack 和 Jill 需要指定你的 Build Tools version 是 21.1.0+, Gradle plugin version 是1.0.0+。
以下的配置是我个人测试通过的配置
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 { enabled true } } |
使用 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 加倍努力,让这一天早日到来。