版本:1.4.94
地址:r8
介紹
r8包含了D8 的功能, 實現(xiàn)了對 java 字節(jié)碼優(yōu)化,混淆并轉(zhuǎn)換成 dex 文件的功能。 可以很好的替代了 ProGuard 的在 Android 編譯工具鏈上的應(yīng)用。 同時生成的 dex 文件更為輕小。
r8 主要分為 5 個階段: Read Input,Configuration,Shrink ,Optimize,Write Dex
代碼入口 com.android.tools.r8.R8
Read Inputs
相對于 ProGuard 只支持對 class 文件的解析 。
r8 支持對 dex 和 class 文件的解析。 class 文件使用 ASM 框架進(jìn)行解析。dex 文件直接采用操作 2 進(jìn)制的方案進(jìn)行解析。 dex 版本支持 v35 ( android 2-5 ) v37 ( android 6 - 7) v38 ( android 8 ) v39( android 9 ) 。
dex 生成的版本取決 app 的 minSdkVersion。dex 之所以存在多個版本。
- dex 在新的版本中引入了新的字節(jié)碼。
- google 會收集特定指令排序在特定版本虛擬機(jī)上運行的 bug 。 從而在生成對應(yīng)版本 dex 的時候規(guī)避掉這些 bug。
相關(guān)字節(jié)碼列表可以查看鏈接 dalvik-bytecode#instructions。
Configuration
工具的運行少不了配置的使用。為了能平滑的替換 ProGuard 的功能。 r8 兼容大部分的 ProGuard rule 。同時擴(kuò)展了 rule 定義。
支持的主要有 keep,if,repackageclasses,flattenpackagehierarchy,overloadaggressively,allowaccessmodification,basedirectory,obfuscationdictionary,classobfuscationdictionary,packageobfuscationdictionary,useuniqueclassmembernames,keepdirectories,renamesourcefileattribute,keepattributes,keeppackagenames,keepparameternames,printconfiguration,printmapping,applymapping,printseeds...
r8 不支持大部分的優(yōu)化配置。 但是 新增了新的 rule 來擴(kuò)展之前的優(yōu)化功能。
forceinline,neverinline,neverclassinline,nevermerge
這里對 rule 的含義不做過多解釋。 可以查看 ProGuard 官方文檔 ProGuard usage 或查看之前的文章 ProGuard 初探
Shrink
移除未被使用的類、字段、方法和屬性。這里和 ProGuard 一樣不會對方法簽名或指令進(jìn)行裁剪。
在處理方法的時候, 需要注意這幾個
Kotlin 的反射
Kotlin 的反射是基于解析注解實現(xiàn)的。Kotlin 經(jīng)過 kotlinc 生成的 class 文件包含一個注解 @kotlin.Metadata。 由編譯器生成, 記錄 Kotlin 源文件的基本信息。Kotlin 運行時使用 ReadKotlinClassHeaderAnnotationVisitor 對 @kotlin.Metadata 注解進(jìn)行解析。 所以 Kotlin 反射非常慢。在處理 Kotlin 在混淆和裁剪的時候。 需同步修改 @kotlin.Metadata 里面的定義。r8 在這里使用 Kotlin 的官方庫 kotlinx-metadata-jvm 操作 @kotlin.Metadata 元素。-
Lambda 表達(dá)式
Lambda 是在 java 8 上引入的。如果單純要實現(xiàn) Lambda 的效果,技術(shù)方法其實有很多種。 最終使用 invokedynamic 主要有兩點,一是更穩(wěn)定的文件格式。 二是更靈活的轉(zhuǎn)換策略,Lambda 的轉(zhuǎn)換策略由運行期決定的。
Lambda 分為編譯期和運行期。
編譯期:
a. javac 對 Lambda 生成一個 invokedynamic 指令,該指令指向一個 BootstrapMethods 方法,
b. 將 Lambda 方法內(nèi)代碼轉(zhuǎn)移到該類的一個私有方法內(nèi)。
c. BootstrapMethods 方法指向生成的的私有方法。
BootstrapMethods
運行期:
執(zhí)行 invokedynamic 。 會執(zhí)行 invokedynamic 指向的 BootstrapMethods 定義的方法返回 CallSite 。 Lambda 返回 CallSite 的方法是 LambdaMetafactory.metafactory 或 LambdaMetafactory.altMetafactory 。默認(rèn)情況下使用 metafactory ,當(dāng)你的 Lambda 實現(xiàn)了多個接口時,將使用 altMetafactory 返回。 最終返回一個實現(xiàn)了該接口的實現(xiàn)類。 這個實現(xiàn)類是由運行期 ASM 動態(tài)生成的,該類主要是做一個轉(zhuǎn)發(fā)的功能, 將方法和參數(shù)轉(zhuǎn)發(fā)給 c 生成的私有方法。
所以在保留 invokedynamic 字節(jié)碼的時候,需要同步保留 invokedynamic 指向的的 BootstrapMethods 以及BootstrapMethods 指向的私有方法。
這里還存在一個問題。 javac 生成的是一個私有方法。 一個外部類是怎樣調(diào)用另外一個類的私有方法? 關(guān)于 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 將代碼轉(zhuǎn)成 中間表現(xiàn) IR 通過 SSA 的方式對代碼進(jìn)行分析。因為使用代碼分析所以 r8 跟蹤反射功能的適應(yīng)性比 ProGuard 好。在反射優(yōu)化中 r8 和 ProGuard 對于構(gòu)造方法均只能識別無參構(gòu)造方法, 對于其他的構(gòu)造方法在這都是無能為力。r8 部分支持對 ServiceLoader 機(jī)制。
ServiceLoader JSP(Service Provider Interfaces)。 ServiceLoader 的實現(xiàn)有兩種版本。 一種是在 JDK 9 以下。 通過定義一個接口。同時將繼承該接口的實現(xiàn)類將記錄在META-INF/services 接口同名文件下。第二種是在 JDK 9 上面用于支持 java9 模塊化下不同模塊的通信。 實現(xiàn)類信息記錄在 module-info.java 下。 對于 r8 只處理第一種實現(xiàn)。 java9 暫不在 r8 的支持范圍內(nèi)。r8 可以刪除可見的橋接方法
當(dāng)允許修改訪問權(quán)限,可見的橋接方法將被刪除。
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6342411
可見的橋接方法是為了解決 public class 繼承了一個私有類的時。 反射調(diào)用存在該類父類的 public 方法出現(xiàn)的 IllegalAccessException 錯誤。
我們只要修改父類為 public 就能安全的刪除橋接方法而不會有任何的影響。r8 的 Shrink 規(guī)則和 ProGuard 相同。 首先計算所有 keep rule 定義的根節(jié)點。 從這些根節(jié)點發(fā)散出去。
對于 Externalizable 和 Serializable 需要額外的處理。 Externalizable 需要保留無參構(gòu)造方法。 Externalizable 存在兩個方法 readExternal 和 writeExternal 用來自定義序列化中的操作。 r8 默認(rèn)會把這兩個方法干掉。 ProGuard 則會將它保留。 原因是 r8 認(rèn)為 readExternal 和 writeExternal 沒有被調(diào)用過。而ProGuard 認(rèn)為你繼承了 Externalizable 那么你就有義務(wù)保留它的重寫方法。
ProGuard 和 r8 對比
- 保留一個 class r8 僅僅保留 靜態(tài)初始化 cinit 的方法,而 ProGuard 同步保留他們的無參構(gòu)造方法。
- 一個虛方法被保留 ProGuard 將保留整條繼承數(shù)上的該方法。 r8 的僅僅保留該方法。 只在該方法調(diào)用 super 才會保留父類的虛方法。
- 一個類被 keep 。r8 會同步 keep 它的父類以及他們的接口。但是也只是僅僅 keep 住他們接口本身。 ProGuard 是 keep 他的父類。而接口并不會主動 keep 。 接口的 keep 是在接口方法被調(diào)用的時候。
Optimize
r8 使用 SSA (靜態(tài)單一賦值)對代碼進(jìn)行優(yōu)化。
如果有需要在這個階段將進(jìn)行 java8 脫糖。
Lambda
由上面的流程介紹可知, javac 生成的方法是私有。需要修改方法為 public 。 同時需要將 ASM 生成的實現(xiàn)類落地。 將對應(yīng)調(diào)用點轉(zhuǎn)成對應(yīng)的方法。接口的默認(rèn)方法
為有默認(rèn)方法的接口生成一個新的類,類名在原有的基礎(chǔ)上加入后綴 -CC。遷移默認(rèn)方法。同時方法名加入前綴。同時將調(diào)用點轉(zhuǎn)換成調(diào)用新的靜態(tài)方法。
...
優(yōu)化項
- 優(yōu)化 Stringbuilder
- 優(yōu)化 String 指令
- 簡化 if 指令。
- 清理橋接方法。
- 刪除沒有影響的方法的調(diào)用
- 合并 class
- 刪除未被使用方法參數(shù)
- 優(yōu)化 枚舉 switch
- 刪除未可達(dá)的代碼
- 刪除強轉(zhuǎn)指令
- 刪除 assert 指令生成的方法。
- 折疊 常量數(shù)字的 算數(shù)運算或 邏輯運算
- 兼容高版本 api 在低版本沒有的問題。
- 內(nèi)連方法
- new-array 指令轉(zhuǎn)換 fill-array-data / filled-new-array 節(jié)省 字節(jié)指令。
- ...
這一塊的代碼是基于老版本的分析。后續(xù)會有更為詳細(xì)的分析。
--- 待續(xù) ---
Obfuscate
混淆跟 ProGuard 類似。 支持字典的自定義。 不同是 r8 在開啟保留簽名(Signature)會保留內(nèi)部類的類名的時候同時會保留外部類的類名,使兩個類類名保持內(nèi)外類的命名關(guān)系。
r8 在這原來的基礎(chǔ)上支持對行號進(jìn)行優(yōu)化。盡可能把所有方法的開始行號映射為1 。
mapping 文件變?yōu)?/p>
2:2:android.arch.core.internal.SafeIterableMap$Entry get(java.lang.Object):45:45 -> a
前面為映射后行號, 后面為源碼中行號。
優(yōu)化行號的好處在于可以合并相同的 debug_info_item。這個方案有點類似于之前的支付寶瘦身。 但是合并效率當(dāng)然會有不如。
r8 其他使用。
ProGuard 在 Android 工具鏈上的應(yīng)用不僅僅用在代碼優(yōu)化混淆上。同時也用在 mainDexList 的計算。 r8 同樣支持對 mainDexList 計算。甚至 mainDexList 文件可以不落地。 但是 r8 計算的 mainDexList 列表會比 ProGuard 計算出來的還多。 因為它不僅保留了所有代碼入口發(fā)散出去的類,以及他們的直接引用。r8 還保留了所有的帶枚舉的注解。以及被這該注解標(biāo)記的類。
至此 r8 已經(jīng)能接管所有 ProGuard 的功能。
java 編譯到 dex 的過程中。還有一個 javac 這一個非 Google 的工具鏈。 或許后續(xù)可能會升級 javac 用以對 dex 的支持。
小結(jié)
r8 已經(jīng)足夠的出色了。但是過于苛刻的保留規(guī)則導(dǎo)致之前規(guī)則并不能無條件的適應(yīng)。當(dāng)前輸出只支持 Dex 。 導(dǎo)致該工具不能應(yīng)用在其他的 java 項目上。較為可惜。