Android 新一代编译 toolchain Jack & Jill 简介

GCHRol 8年前
   <p> </p>    <p><img src="https://simg.open-open.com/show/089633f4072b33ec319eee4d26c84ae0.jpg"></p>    <p>2016 年 3 月 10 日, Google 向外界发布了 Android N 的预览版,并宣布了 Android N 的 <a href="/misc/goto?guid=4959644214801919579" rel="nofollow,noindex">Roadmap</a> ,Android N 的最终版源代码将于今年 8 或 9 月份释出到 AOSP 项目。</p>    <p>在众多的 Android N 新特性中,有一项新工具链的出现与 Android 生态圈的所有开发者息息相关,即 Jack & Jill 编译器的引入。</p>    <p>在依赖了 Sun/Oracle 的 Java 编译器十年之后,Android 终于有了自己的 Java 编译器。</p>    <p>本文试图对市面上非常有限的资料进行总结,向大家介绍 Jack & Jill 的缘起,工作方式和原理。</p>    <p>Jack 是 Java Android Compiler Kit 的缩写,它可以将 Java 代码直接编译为 Dalvik 字节码,并负责 Minification, Obfuscation, Repackaging, Multidexing, Incremental compilation。它试图取代 javac/dx/proguard/jarjar/multidex 库等工具。</p>    <ul>     <li>git 源代码地址是 <a href="/misc/goto?guid=4959672431879192265" rel="nofollow,noindex">https://android.googlesource.com/toolchain/jack</a> 。</li>    </ul>    <p>Jill 是 Jack Intermediate Library Linker 的缩写,它负责 “Shielding JACK from Java byte code”;实际上辅助 Jack 对.class 做预处理,生成 .jack 文件</p>    <ul>     <li>git 源代码地址是 <a href="/misc/goto?guid=4959672431970889293" rel="nofollow,noindex">https://android.googlesource.com/toolchain/jill</a> 。</li>    </ul>    <h2>缘起</h2>    <p>虽然 Google 是在宣布 Android N 预览版时隆重介绍了Jack & Jill。但是,早在 2014 年 Google 就对外宣布了新编译器 Jack 的存在 <a href="http://android-developers.blogspot.jp/2014/12/hello-world-meet-our-new-experimental.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+blogspot/hsDu+\(Android+Developers+Blog" rel="nofollow,noindex">meet our new experimental toolchain</a> , 它的开发启动时间更是远远早于 2014 年。</p>    <p>下面是我总结的 Jack 的缘起</p>    <ul>     <li>一家名叫 FlexyCore 的小公司基于 GCC toolchain 开发了 Android 平台上的 AOT 编译器,被 Google 看中并于 2013 年被收购</li>     <li>FlexyCore team 基于 LLVM toolchain 开发了 ART,并成为 Android 5.0 之后的缺省 Java Runtime</li>     <li>FlexyCore team 基于 Eclipse ecj 编译器开始开发 Jack,基于 ASM4 开发 Jill。 他们早在 2014 年 2 月就开始提交 Jill 的代码了 <a href="/misc/goto?guid=4959672432146842595" rel="nofollow,noindex">Jill initial commit</a> ; 3 月份开始提交 Jack的代码 <a href="/misc/goto?guid=4959672432228538367" rel="nofollow,noindex">Jack initial commit</a></li>     <li>自 Android build-tools 21.1 开始,里面已经内置 jack.jar 和 jill.jar</li>     <li>Android Gradle plugin 自 0.14 开始支持 Jack & Jill <a href="/misc/goto?guid=4959672432318189649" rel="nofollow,noindex">initial commit</a></li>     <li>自 Android 6.0 开始,Jack & Jill 成为 AOSP 的官方编译器, 也就是说所有的 Android 6.0 ROM 都是 Jack 编译出来的 <a href="/misc/goto?guid=4959670039058454734" rel="nofollow,noindex">link</a> ,也代表 Google 认为 Jack 达到了一定的成熟度</li>     <li>预计等 Android 7.0 正式发布时,Jack 可能会成为官方推荐的编译器</li>    </ul>    <h3>为什么要抛弃 Javac/dx,开发 Jack 和 Jill</h3>    <p>据个人推测主要有三个目的</p>    <ul>     <li>提高编译速度</li>     <li>应对 Oracle 的法律诉讼</li>     <li>将编译器掌控权拿在自己手中,不再受制于 Oracle,可以做一些 Android only 的优化</li>    </ul>    <p>下面比较一下旧的 javac/dx/ProGuard/jarjar toolchain 和新的 Jack 编译器的工作流程</p>    <h2>旧编译流程</h2>    <p>简单的说,将 Java 代码和依赖库编译为 dex 有两个大的阶段</p>    <p>javac (.java –> .class) –> dx (.class –> .dex)</p>    <p>下面是用流程图表示的旧编译过程</p>    <p><img src="https://simg.open-open.com/show/ceac0f29d4f2aa770eb4adce28db2db1.png"></p>    <ol>     <li>javac 将 java 代码编译为 java bytecode, 以 .class 的形式存在; 以 jar 和 aar 形式存在的依赖库,代码在里面以一堆.class 的形式存在</li>     <li>Proguard 工具读取 Proguard 配置,对 .class 做 shrinking, obfuscation,输出 Proguard mapping</li>     <li>dx 将多个 .class 转化为单一的 classes.dex ; 如果 dex 方法数超过 65k, 就生成 classes.dex, classes1.dex…classesN.dex</li>    </ol>    <h2>新编译流程</h2>    <p>新的编译过程只有一个阶段了,它完全抛弃了 javac, ProGuard, jarjar 等工具,一个工具搞定一切</p>    <p>Jack (.java –> .jack –> .dex)</p>    <p>下面是用流程图表示的 Jill 预处理过程</p>    <p><img src="https://simg.open-open.com/show/f50761e7d2a05a6e7e1e7f3d09610cea.png"></p>    <p>下面是用流程图表示的 Jack 编译过程</p>    <p><img src="https://simg.open-open.com/show/d7c52007bd0cf594b1fcdef38d9d436b.png"></p>    <ol>     <li>各种依赖库仍然以 jar/aar 的形式存在</li>     <li>辅助工具 Jill 将根据依赖库中的 .class 生成 Jayce 格式的 IL,并调用 Jack 做 pre-dex 并生成 .jack ,此过程只在编译 app 时发生一次</li>     <li>Jack 将 java 源代码也编译为 .jack ,然后将多个 .jack 转化为单一的 .dex ; 如果 dex 方法数超过 65k, 就生成 classes.dex, classes1.dex…classesN.dex</li>    </ol>    <p>pre-dex 的详细解释可以参阅此链接 <a href="/misc/goto?guid=4959672432466382232" rel="nofollow,noindex">new-build-system</a></p>    <pre>  <code class="language-java">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.  </code></pre>    <h3>.Jack中间文件</h3>    <p>.Jack 的具体格式如下图所示</p>    <p><img src="https://simg.open-open.com/show/e12163047bf336130405286cffb2621f.png"></p>    <p>可见里面包含了 Jayce 格式的 IL ,pre-dex,原始 aar 中的资源文件,以及 Jack 会用到的一些 meta 信息</p>    <p>下图简单比较了 java 代码转化的 .class , Jayce IL 和 dex 的内容异同</p>    <p><img src="https://simg.open-open.com/show/338dbbb58c0e8540b9f11c6864602514.png"></p>    <p>简单比较下三种 IL 的区别:</p>    <p>Sun/Oracle Hotspot VM 是基于栈式的,所以 .class 文件的内容就是不断地压操作数到栈顶,从栈顶读取操作数,比较或做运算,将结果再压回栈顶</p>    <p>Dalvik VM 是基于寄存器的,所以 .dex 的内容就是不断地 move 操作数到寄存器,比较或做运算,将结果写回寄存器或内存地址</p>    <p>Jayce 则是 Jack&Jill 专有的 IL, 目前没有查阅到更多的官方资料。只能参阅 Jill 源代码中 com.android.jill.backend.jayce 包的代码了,比如其中的 Token 类就定义了 Jayce 的 Token 定义。</p>    <p>个人推测 Jayce 存在的意义是:</p>    <ul>     <li>为了在整合多个 jack 文件,生成单一的 dex 时,方便 Jack 做一些全局性的后端编译优化。</li>     <li>从 Android 生态圈中完全去除 Oracle 的 Java Bytecode 格式</li>    </ul>    <h3>使用Jack编译器的优势</h3>    <ul>     <li>对依赖库做 pre dex,且成果会被保存到 build/intermediates/jill/debug 目录。</li>    </ul>    <p>之后的编译过程中,只要依赖库的数目和版本不变,之前的 pre dex 成果会被复用;Jack 只需要编译变化的源代码,然后对多个 dex 进行 merge 即可,能够加速整个编译过程。</p>    <ul>     <li>编译时会启动一个 Jack compilation server,并开启并行编译</li>    </ul>    <p>Jack 文档是这么介绍的</p>    <pre>  <code class="language-java">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.  </code></pre>    <ul>     <li>支持 Java 8 的一部分特性</li>     <li>Jack 由 Google 完全掌控,未来可能成为 Android sdk 的默认编译器</li>     <li>向后兼容到 Android 2.3</li>    </ul>    <h3>采用 Jack 对打包流程的影响</h3>    <ol>     <li>不再需要独立的 ProGuard。Jack 支持读取旧的 ProGuard 配置,完成 shrinking, obfuscation 的工作</li>     <li>不再需要独立的 jarjar。Jack 支持读取旧的 jarjar 配置,完成 repackaging 的工作</li>     <li>没有 .class 文件了,直接操纵或读取 Java 字节码的各种工具如 JaCoCo/Lint/Mokito/Retrolambda 没有了用武之地。但是仍然可以在 Android Library 上使用这些工具,编译为 aar/jar 后作为 Jill 的输入</li>     <li>annotation processors 如 Dagger, ButterKife 仍可以使用</li>     <li>Scala/Kotlin 等第三方 JVM 语言编写的内容必须先被 Jill 处理,再作为 Jack 的输入</li>    </ol>    <h3>Jack 当前的局限(截止到2016/03/15)</h3>    <ol>     <li>暂时还不支持 Android Studio 2.0 的 Instant Run 特性</li>     <li>暂时还不支持 data binding</li>    </ol>    <h2>65k 方法数目问题</h2>    <h3>为什么会有 65k 问题?</h3>    <p>当你的 app 足够复杂之后,在打包时常常会遇到这种错误提示</p>    <pre>  <code class="language-java">Unable to execute dex: method ID not in [0, 0xffff]: 65536  </code></pre>    <p>为什么方法数目不能超过 65k 呢?有人说是 dexopt 的问题,有人说是 dex 格式的限制,下面我们看看这个 log 到底是哪里吐出来的,然后分析下具体原因。</p>    <ul>     <li>dex 格式的限制?</li>    </ul>    <p>首先我们看一下 dex 的结构定义</p>    <pre>  <code class="language-java">//Direct-mapped "header_item" struct.  struct DexHeader {   ...    u4  methodIdsSize;   ...  };    //These match the definitions in the VM specification.  typedef uint32_t            u4;  </code></pre>    <p>可见 dex 文件结构是用 32 位来存储 method id 的,最大支持 2 的 32 次方,因此 65k 的原因不在于此。</p>    <ul>     <li>dexopt 的原因?</li>    </ul>    <p>dexopt 是 app 已经打包成功,安装到手机之后才会发生的过程。但是 65k 问题是在打包时发生的,所以问题原因也不在此</p>    <p>一般提到的 dexopt 错误,其实是 Android 2.3 及其以下在 dexopt 执行时只分配 5M 内存,导致方法数目过多(数量不一定到 65k)时在 odex 过程中崩溃,官方称之为 Dalvik linearAlloc bug(Issue 22586) 。</p>    <p>另:这个 linearAlloc 的限制不仅存在于 dexopt 里,还在 dalvik rumtime 中存在……</p>    <p>以下链接详细解释了此问题: <a href="/misc/goto?guid=4959646162125122303" rel="nofollow,noindex">https://github.com/simpleton/dalvik_patch</a></p>    <ul>     <li>错误 log 是哪里吐出来的?</li>    </ul>    <pre>  <code class="language-java">//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;  </code></pre>    <p>通过查阅 <a href="/misc/goto?guid=4958836823776484934" rel="nofollow,noindex">dalvik-bytecode</a> 可知,@CCCC 的范围必须在 0~65535 之间。</p>    <p>所以归根结底,65k 问题是因为 dalvik bytecode 中的指令格式使用了 16 位来放 @CCCC 导致的;所以,不仅 Method 数目不能超过 65k, Field 和 Class 数目也不能超过 65k。</p>    <h3>为什么 jack 没有 65k 问题</h3>    <p>前文已经很清楚地解释了 65k 问题的由来,可见只要 dalvik bytecode 指令格式不升级,65k 问题是逃不掉的。</p>    <p>Jack 官网对 65k 问题是这么说的:</p>    <pre>  <code class="language-java">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.  </code></pre>    <p>所以,Jack 和旧工具链对 multidex 的支持方式是相同的</p>    <p>被 Jack 编译出来的 app 执行时也和以前一样</p>    <ol>     <li>若是 dalvik 虚拟机,它只支持读取一个 classes.dex。而 multidex 解决方案会读取多个 .dex ,帮我们做 dex 数组合并</li>     <li>若是 art 虚拟机,它会扫描 classes.dex, classes1.dex…classesN.dex,调用 dex2oat 转化为单一的 oat</li>    </ol>    <h2>Jack 是怎么支持 Java 8 的?</h2>    <p>以 lambda 表达式为例</p>    <pre>  <code class="language-java">Interface lambda = i -> i + 1;  </code></pre>    <p>会被转化为 anonymous classes</p>    <pre>  <code class="language-java">Interface lambda = new Interface() {    public int m(int i) {      return i + 1;    }  };  </code></pre>    <p>Jack当前支持的 Java 8 特性可参见 <a href="/misc/goto?guid=4959672432617811370" rel="nofollow,noindex">j8-jack</a> 。</p>    <h2>如何在 Gradle 脚本中使用 Jack 编译器编译 app</h2>    <p>想使用 Jack 和 Jill 需要指定你的 Build Tools version 是 21.1.0+, Gradle plugin version 是1.0.0+。</p>    <p>以下的配置是我个人测试通过的配置</p>    <ul>     <li>使用 Android Gradle 插件 2.1.0-alpha2 <pre>  <code class="language-java">dependencies {    classpath 'com.android.tools.build:gradle:2.1.0-alpha2'  }  </code></pre> </li>    </ul>    <ul>     <li> <p>使用以下版本的 sdk 和 build-tool</p> <pre>  <code class="language-java">compileSdkVersion 'android-N'  buildToolsVersion '24.0.0 rc1'  </code></pre> </li>     <li> <p>在 defaultConfig 中指定用 Jack</p> <pre>  <code class="language-java">defaultConfig {    jackOptions {      enabled true    }  }  </code></pre> </li>     <li> <p>使用 gradle 2.10 以上</p> <pre>  <code class="language-java">distributionUrl=http\://mirrors.taobao.net/mirror/gradle/gradle-2.10-bin.zip  </code></pre> </li>     <li> <p>使用 Android Studio 2.1 (preview) 或者命令行编译</p> </li>     <li> <p>可能需要提升 javaMaxHeapSize</p> <pre>  <code class="language-java">dexOptions{    javaMaxHeapSize "2g"  }  </code></pre> </li>    </ul>    <h2>性能比较</h2>    <p>经过测试,当前版本(2016/03/15)的 Jack 编译器比起 Javac+dx 在编译时间,编译出的 apk 体积,编译出的 apk 的性能上暂时并没有优势。</p>    <p>但是,可以期待 Google 将在 Jack 编译器上做大量的智力投资,Jack 的未来是光明的。</p>    <p>下图是 guardsquare 公司对 Javac+dx 和 Jack 做的对比测试</p>    <p><img src="https://simg.open-open.com/show/abe59fc31469afee5d1eab8f45b1e9db.png"></p>    <p>对于不 proguard 的 clean build,javac/dx 耗时 56s, jack 耗时 1 m 48 s;之所以 jack 这么慢是因为它要做大量的 pre-dex。</p>    <p><img src="https://simg.open-open.com/show/b068d6911eb5428cb1ddbb30dab58a07.png"></p>    <p>对于不 proguard 的 clean build,javac/dx 和 jack 编译出来的 app 性能相差无几。</p>    <p><img src="https://simg.open-open.com/show/2388b8c7bfa47cfd0d423f6068bb2887.png"></p>    <p>对于共用 proguard 配置文件情况,javac/dx 和jack 编译出来的 app 体积也差不多。</p>    <p>我个人测试的编译速度 / apk 体积等对比也大致如此,在此不再赘述.</p>    <h2>结语</h2>    <p>虽然 Jack 编译器的现状并不出彩,但是它终究有一天会成为 Android app 的官方推荐编译器。</p>    <p>期待 Google Android team 加倍努力,让这一天早日到来。</p>    <p>来自: http://taobaofed.org/blog/2016/05/05/new-compiler-for-android/</p>