Android 新一代編譯 toolchain Jack & Jill 簡介

原文發(fā)表于?http://taobaofed.org/blog/2016/05/05/new-compiler-for-android/

作者: 凱馮?發(fā)表于:?2016-05-05

2016 年 3 月 10 日, Google 向外界發(fā)布了 Android N 的預(yù)覽版,并宣布了 Android N 的?Roadmap,Android N 的最終版源代碼將于今年 8 或 9 月份釋出到 AOSP 項目。

在眾多的 Android N 新特性中,有一項新工具鏈的出現(xiàn)與 Android 生態(tài)圈的所有開發(fā)者息息相關(guān),即 Jack & Jill 編譯器的引入。

在依賴了 Sun/Oracle 的 Java 編譯器十年之后,Android 終于有了自己的 Java 編譯器。

本文試圖對市面上非常有限的資料進行總結(jié),向大家介紹 Jack & Jill 的緣起,工作方式和原理。

Jack 是 Java Android Compiler Kit 的縮寫,它可以將 Java 代碼直接編譯為 Dalvik 字節(jié)碼,并負(fù)責(zé) Minification, Obfuscation, Repackaging, Multidexing, Incremental compilation。它試圖取代 javac/dx/proguard/jarjar/multidex 庫等工具。

git 源代碼地址是?https://android.googlesource.com/toolchain/jack。

Jill 是 Jack Intermediate Library Linker 的縮寫,它負(fù)責(zé) “Shielding JACK from Java byte code”;實際上輔助 Jack 對.class 做預(yù)處理,生成?.jack?文件

git 源代碼地址是?https://android.googlesource.com/toolchain/jill。

緣起

雖然 Google 是在宣布 Android N 預(yù)覽版時隆重介紹了Jack & Jill。但是,早在 2014 年 Google 就對外宣布了新編譯器 Jack 的存在?meet our new experimental toolchain, 它的開發(fā)啟動時間更是遠(yuǎn)遠(yuǎn)早于 2014 年。

下面是我總結(jié)的 Jack 的緣起

一家名叫 FlexyCore 的小公司基于 GCC toolchain 開發(fā)了 Android 平臺上的 AOT 編譯器,被 Google 看中并于 2013 年被收購

FlexyCore team 基于 LLVM toolchain 開發(fā)了 ART,并成為 Android 5.0 之后的缺省 Java Runtime

FlexyCore team 基于 Eclipse ecj 編譯器開始開發(fā) Jack,基于 ASM4 開發(fā) Jill。 他們早在 2014 年 2 月就開始提交 Jill 的代碼了?Jill initial commit; 3 月份開始提交 Jack的代碼?Jack initial commit

自 Android build-tools 21.1 開始,里面已經(jīng)內(nèi)置 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 認(rèn)為 Jack 達到了一定的成熟度

預(yù)計等 Android 7.0 正式發(fā)布時,Jack 可能會成為官方推薦的編譯器

為什么要拋棄 Javac/dx,開發(fā) Jack 和 Jill

據(jù)個人推測主要有三個目的

提高編譯速度

應(yīng)對 Oracle 的法律訴訟

將編譯器掌控權(quán)拿在自己手中,不再受制于 Oracle,可以做一些 Android only 的優(yōu)化

下面比較一下舊的 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?轉(zhuǎn)化為單一的 classes.dex ; 如果 dex 方法數(shù)超過 65k, 就生成 classes.dex, classes1.dex…classesN.dex

新編譯流程

新的編譯過程只有一個階段了,它完全拋棄了 javac, ProGuard, jarjar 等工具,一個工具搞定一切

Jack (.java –> .jack –> .dex)

下面是用流程圖表示的 Jill 預(yù)處理過程

下面是用流程圖表示的 Jack 編譯過程

各種依賴庫仍然以 jar/aar 的形式存在

輔助工具 Jill 將根據(jù)依賴庫中的?.class?生成 Jayce 格式的 IL,并調(diào)用 Jack 做 pre-dex 并生成?.jack,此過程只在編譯 app 時發(fā)生一次

Jack 將 java 源代碼也編譯為?.jack,然后將多個?.jack?轉(zhuǎn)化為單一的?.dex; 如果 dex 方法數(shù)超過 65k, 就生成 classes.dex, classes1.dex…classesN.dex

pre-dex 的詳細(xì)解釋可以參閱此鏈接?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中間文件

.Jack?的具體格式如下圖所示

可見里面包含了 Jayce 格式的 IL ,pre-dex,原始 aar 中的資源文件,以及 Jack 會用到的一些 meta 信息

下圖簡單比較了 java 代碼轉(zhuǎn)化的?.class, Jayce IL 和 dex 的內(nèi)容異同

簡單比較下三種 IL 的區(qū)別:

Sun/Oracle Hotspot VM 是基于棧式的,所以?.class?文件的內(nèi)容就是不斷地壓操作數(shù)到棧頂,從棧頂讀取操作數(shù),比較或做運算,將結(jié)果再壓回棧頂

Dalvik VM 是基于寄存器的,所以?.dex?的內(nèi)容就是不斷地 move 操作數(shù)到寄存器,比較或做運算,將結(jié)果寫回寄存器或內(nèi)存地址

Jayce 則是 Jack&Jill 專有的 IL, 目前沒有查閱到更多的官方資料。只能參閱 Jill 源代碼中 com.android.jill.backend.jayce 包的代碼了,比如其中的 Token 類就定義了 Jayce 的 Token 定義。

個人推測 Jayce 存在的意義是:

為了在整合多個 jack 文件,生成單一的 dex 時,方便 Jack 做一些全局性的后端編譯優(yōu)化。

從 Android 生態(tài)圈中完全去除 Oracle 的 Java Bytecode 格式

使用Jack編譯器的優(yōu)勢

對依賴庫做 pre dex,且成果會被保存到 build/intermediates/jill/debug 目錄。

之后的編譯過程中,只要依賴庫的數(shù)目和版本不變,之前的 pre dex 成果會被復(fù)用;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 的默認(rèn)編譯器

向后兼容到 Android 2.3

采用 Jack 對打包流程的影響

不再需要獨立的 ProGuard。Jack 支持讀取舊的 ProGuard 配置,完成 shrinking, obfuscation 的工作

不再需要獨立的 jarjar。Jack 支持讀取舊的 jarjar 配置,完成 repackaging 的工作

沒有?.class?文件了,直接操縱或讀取 Java 字節(jié)碼的各種工具如 JaCoCo/Lint/Mokito/Retrolambda 沒有了用武之地。但是仍然可以在 Android Library 上使用這些工具,編譯為 aar/jar 后作為 Jill 的輸入

annotation processors 如 Dagger, ButterKife 仍可以使用

Scala/Kotlin 等第三方 JVM 語言編寫的內(nèi)容必須先被 Jill 處理,再作為 Jack 的輸入

Jack 當(dāng)前的局限(截止到2016/03/15)

暫時還不支持 Android Studio 2.0 的 Instant Run 特性

暫時還不支持 data binding

65k 方法數(shù)目問題

為什么會有 65k 問題?

當(dāng)你的 app 足夠復(fù)雜之后,在打包時常常會遇到這種錯誤提示

Unable to execute dex: method ID not in [0, 0xffff]: 65536

為什么方法數(shù)目不能超過 65k 呢?有人說是 dexopt 的問題,有人說是 dex 格式的限制,下面我們看看這個 log 到底是哪里吐出來的,然后分析下具體原因。

dex 格式的限制?

首先我們看一下 dex 的結(jié)構(gòu)定義

//Direct-mapped "header_item" struct.

structDexHeader {

...

? u4? methodIdsSize;

...

};

//These match the definitions in the VM specification.

typedefuint32_tu4;

可見 dex 文件結(jié)構(gòu)是用 32 位來存儲 method id 的,最大支持 2 的 32 次方,因此 65k 的原因不在于此。

dexopt 的原因?

dexopt 是 app 已經(jīng)打包成功,安裝到手機之后才會發(fā)生的過程。但是 65k 問題是在打包時發(fā)生的,所以問題原因也不在此

一般提到的 dexopt 錯誤,其實是 Android 2.3 及其以下在 dexopt 執(zhí)行時只分配 5M 內(nèi)存,導(dǎo)致方法數(shù)目過多(數(shù)量不一定到 65k)時在 odex 過程中崩潰,官方稱之為 Dalvik linearAlloc bug(Issue 22586) 。

另:這個 linearAlloc 的限制不僅存在于 dexopt 里,還在 dalvik rumtime 中存在……

以下鏈接詳細(xì)解釋了此問題: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 之間。

所以歸根結(jié)底,65k 問題是因為 dalvik bytecode 中的指令格式使用了 16 位來放 @CCCC 導(dǎo)致的;所以,不僅 Method 數(shù)目不能超過 65k, Field 和 Class 數(shù)目也不能超過 65k。

為什么 jack 沒有 65k 問題

前文已經(jīng)很清楚地解釋了 65k 問題的由來,可見只要 dalvik bytecode 指令格式不升級,65k 問題是逃不掉的。

Jack 官網(wǎng)對 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 執(zhí)行時也和以前一樣

若是 dalvik 虛擬機,它只支持讀取一個 classes.dex。而 multidex 解決方案會讀取多個?.dex,幫我們做 dex 數(shù)組合并

若是 art 虛擬機,它會掃描 classes.dex, classes1.dex…classesN.dex,調(diào)用 dex2oat 轉(zhuǎn)化為單一的 oat

Jack 是怎么支持 Java 8 的?

以 lambda 表達式為例

Interface lambda = i -> i +1;

會被轉(zhuǎn)化為 anonymous classes

Interface lambda =newInterface() {

publicintm(inti){

returni +1;

? }

};

Jack當(dāng)前支持的 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"

}

性能比較

經(jīng)過測試,當(dāng)前版本(2016/03/15)的 Jack 編譯器比起 Javac+dx 在編譯時間,編譯出的 apk 體積,編譯出的 apk 的性能上暫時并沒有優(yōu)勢。

但是,可以期待 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 體積等對比也大致如此,在此不再贅述.

結(jié)語

雖然 Jack 編譯器的現(xiàn)狀并不出彩,但是它終究有一天會成為 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

https://www.guardsquare.com/blog/DroidconLondon2015

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容