版本:1.4.94
地址:r8
介紹
r8包含了D8 的功能, 實(shí)現(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ī)上運(yùn)行的 bug 陵究。 從而在生成對應(yīng)版本 dex 的時候規(guī)避掉這些 bug。
相關(guān)字節(jié)碼列表可以查看鏈接 dalvik-bytecode#instructions元潘。
Configuration
工具的運(yùn)行少不了配置的使用畔乙。為了能平滑的替換 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 的反射是基于解析注解實(shí)現(xiàn)的。Kotlin 經(jīng)過 kotlinc 生成的 class 文件包含一個注解 @kotlin.Metadata刊头。 由編譯器生成黍瞧, 記錄 Kotlin 源文件的基本信息。Kotlin 運(yùn)行時使用 ReadKotlinClassHeaderAnnotationVisitor 對 @kotlin.Metadata 注解進(jìn)行解析原杂。 所以 Kotlin 反射非常慢印颤。在處理 Kotlin 在混淆和裁剪的時候。 需同步修改 @kotlin.Metadata 里面的定義穿肄。r8 在這里使用 Kotlin 的官方庫 kotlinx-metadata-jvm 操作 @kotlin.Metadata 元素年局。-
Lambda 表達(dá)式
Lambda 是在 java 8 上引入的。如果單純要實(shí)現(xiàn) Lambda 的效果咸产,技術(shù)方法其實(shí)有很多種矢否。 最終使用 invokedynamic 主要有兩點(diǎn),一是更穩(wěn)定的文件格式脑溢。 二是更靈活的轉(zhuǎn)換策略僵朗,Lambda 的轉(zhuǎn)換策略由運(yùn)行期決定的。
Lambda 分為編譯期和運(yùn)行期屑彻。
編譯期:
a. javac 對 Lambda 生成一個 invokedynamic 指令验庙,該指令指向一個 BootstrapMethods 方法,
b. 將 Lambda 方法內(nèi)代碼轉(zhuǎn)移到該類的一個私有方法內(nèi)社牲。
c. BootstrapMethods 方法指向生成的的私有方法粪薛。
運(yùn)行期:
執(zhí)行 invokedynamic 。 會執(zhí)行 invokedynamic 指向的 BootstrapMethods 定義的方法返回 CallSite 膳沽。 Lambda 返回 CallSite 的方法是 LambdaMetafactory.metafactory 或 LambdaMetafactory.altMetafactory 汗菜。默認(rèn)情況下使用 metafactory ,當(dāng)你的 Lambda 實(shí)現(xiàn)了多個接口時挑社,將使用 altMetafactory 返回陨界。 最終返回一個實(shí)現(xiàn)了該接口的實(shí)現(xiàn)類。 這個實(shí)現(xiàn)類是由運(yù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)行分析嫉戚。因?yàn)槭褂么a分析所以 r8 跟蹤反射功能的適應(yīng)性比 ProGuard 好刨裆。在反射優(yōu)化中 r8 和 ProGuard 對于構(gòu)造方法均只能識別無參構(gòu)造方法, 對于其他的構(gòu)造方法在這都是無能為力彬檀。r8 部分支持對 ServiceLoader 機(jī)制帆啃。
ServiceLoader JSP(Service Provider Interfaces)。 ServiceLoader 的實(shí)現(xiàn)有兩種版本窍帝。 一種是在 JDK 9 以下努潘。 通過定義一個接口。同時將繼承該接口的實(shí)現(xiàn)類將記錄在META-INF/services 接口同名文件下坤学。第二種是在 JDK 9 上面用于支持 java9 模塊化下不同模塊的通信疯坤。 實(shí)現(xiàn)類信息記錄在 module-info.java 下。 對于 r8 只處理第一種實(shí)現(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 相同突梦。 首先計(jì)算所有 keep rule 定義的根節(jié)點(diǎn)。 從這些根節(jié)點(diǎn)發(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 生成的實(shí)現(xiàn)類落地堡掏。 將對應(yīng)調(diào)用點(diǎn)轉(zhuǎn)成對應(yīng)的方法应结。接口的默認(rèn)方法
為有默認(rèn)方法的接口生成一個新的類,類名在原有的基礎(chǔ)上加入后綴 -CC泉唁。遷移默認(rèn)方法鹅龄。同時方法名加入前綴 。同時將調(diào)用點(diǎn)轉(zhuǎn)換成調(diào)用新的靜態(tài)方法亭畜。
...
優(yōu)化項(xiàng)
- 優(yōu)化 Stringbuilder
- 優(yōu)化 String 指令
- 簡化 if 指令扮休。
- 清理橋接方法。
- 刪除沒有影響的方法的調(diào)用
- 合并 class
- 刪除未被使用方法參數(shù)
- 優(yōu)化 枚舉 switch
- 刪除未可達(dá)的代碼
- 刪除強(qiáng)轉(zhuǎn)指令
- 刪除 assert 指令生成的方法拴鸵。
- 折疊 常量數(shù)字的 算數(shù)運(yùn)算或 邏輯運(yùn)算
- 兼容高版本 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口叙。這個方案有點(diǎn)類似于之前的支付寶瘦身。 但是合并效率當(dāng)然會有不如嗅战。
r8 其他使用庐扫。
ProGuard 在 Android 工具鏈上的應(yīng)用不僅僅用在代碼優(yōu)化混淆上。同時也用在 mainDexList 的計(jì)算仗哨。 r8 同樣支持對 mainDexList 計(jì)算形庭。甚至 mainDexList 文件可以不落地。 但是 r8 計(jì)算的 mainDexList 列表會比 ProGuard 計(jì)算出來的還多厌漂。 因?yàn)樗粌H保留了所有代碼入口發(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 項(xiàng)目上。較為可惜好港。