MainDex 優化記
如果你對本文感興趣,也許你對我的公眾號也會有興趣,可掃下方二維碼或搜尋公眾微訊號:mxszgg

tips: 本文基於 AGP 3.0.1 原始碼分析
MainDex 打入規則分析
“maindex method 超過 65536 了,咋被打爆了呢?”
在過去很長一段時間內我們的應用 maindex 會被打爆,於是大佬們使用了 DexKnifePlugin 來解決問題,但是後來 AGP 上了 3.0.1 以及其他問題的出現,DexKnifePlugin 已經不是能夠很良好地適用於我們的 app 中了,於是巴神(公眾號:巴巴巴掌)用了另一個比較優雅的方案,通過 hook transform 來達到了我們的目的,但是終究是一個 hook 方案並且它不能夠運用於 D8 編譯器,於是需要一個更加優雅的方案,我們必須得從原始碼中瞭解到究竟什麼樣的類會打入 maindex——
開啟 MultiDexTransform/MainDexListTransform 原始碼(本文以 MultiDexTransform 為例),直接看向 transform()
原始碼 ——

直接看向 181 行,這裡的 input 變數是所有的 class 檔案集合,接下來進入 182 行 ——

214-227 行就是 maindex 的 一部分 keep 規則,第一部分 manifestKeepListProguardFile
路徑為 /app/build/intermediates/multi-dex/release/manifest_keep.txt
,從 TaskManager#createProcessResTask()
方法中可以瞭解到當編譯環境為 multidexEnabled 開啟並且當前 minSdk 版本小於21的時候才會有這個檔案,再從AAPT 原始碼中可知 AAPT 將會掃描應用的 AndroidManifest.xml 然後將其中的 application、instrumentation、本身或其父 application 處於另一個程序的四大元件 keep 住,keep 的內容將會是本身以及構造器方法,類似如下:
-keep class com.joker.maindexkeep.App { <init>(...); } 複製程式碼
第二部分是 useMainDexKeepProguard,這個是開發者在 gradle 中配置的希望能夠被 keep 在主 dex 的檔案,其配置規則與混淆配置檔案相同,這裡就不做額外擴充套件了;第三部分是寫死的配置規則,有 instrumentation、application 等,需要注意的是 226 行,所有的註解類也將會被 keep 住;接下來就是設定 Proguard 的輸入輸出檔案,最後就是 238 行執行 proguard 了,具體內部邏輯就不跟蹤了,最後輸出檔案也就是 234 行所提及的路徑為 /app/build/intermediates/multi-dex/release/componentClasses.jar
,開啟該 jar 包可以看到包中內容是完全根據上述的所有 keep 規則所生成的 ——

進行完第二步,就是第三步 computeList()
了——

該方法第一步是計算所有的 mainDexClasses;第二步是判斷 userMainDexKeepFile
檔案是否為空,該檔案是由開發者在 gradle 配置檔案中通過 multiDexKeepFile
配置的,配置規則就是直接填充 class 檔案的全路徑限定名;最後就是寫入 mainDexListFile 中,該檔案路徑為 /app/build/intermediates/multi-dex/release/maindexlist.txt
,該檔案實際上就是所有會被打入 maindex 中的 class 檔案集合。三步看下來只有第一步需要分析, callDx()
原始碼如下——

看向 280-288 行程式碼可以知道,如果開發者配置了 keepRuntimeAnnotatedClasses
的話,mainDexListOptions 將會新增一個 DISABLE_ANNOTATION_RESOLUTION_WORKAROUND
配置,接著看到290行並跟蹤下去, createMainDexList()
——

這段程式碼看起來很複雜,實際上就是就是根據當前編譯環境找到 sdk 中的 dx.jar(1199-1205行),然後呼叫 dx.jar 中 ClassReferenceListBuilder 類的 main 方法,第一個引數就是之前 callDx()
中所提及的引數(如果配置了 keepRuntimeAnnotatedClasses
的話),第二個引數是之前生成的 componentClasses.jar,第三個引數是一個 jar 包,該 jar 包是混淆 task 生成的,有且僅有應用所有的 class 檔案。最後此方法返回了一個 Set,這個 Set 就是最終會打入主 dex 的所有的 class 的全限定路徑名集合。
雖說呼叫的是 dx.jar 中的 ClassReferenceListBuilder,實際上與 AGP 中自帶的 ClassReferenceListBuilder 類無多大差異,所以不妨直接看 AGP 中的 ClassReferenceListBuilder 的 main 方法——


在這裡需要告訴各位讀者的是前面所提到的 createMainDexList()
所返回的集合實際上就是第 93 行程式碼的結果,也就是 MainDexListBuilder#getMainDexList()
的結果,所以看一下該方法返回的是一個什麼 ——


實際上返回了一個 Set,那麼全域性不妨搜下該 Set 的 add 方法所呼叫的地方,實際上共有兩處——

1. MainDexListBuilder#getClassNames()
方法的邏輯就不在此給各位讀者解答了,直接給結論—— componentClasses.jar
中所有的類及其引用類 的集合。

2.該方法的邏輯是如果當前類或類的方法或類的欄位被 執行時註解 所修飾了的話,那麼也將會被新增到 filesToKeep
變數中,但是 keepAnnotated()
的執行邏輯從上一張圖中的 128 行程式碼可以看出,只有 keepAnnotated
變數為 true 的時候才會執行,那麼什麼時候該變數為 true 呢?從 MainDexListBuilder#main()
方法中可以知道,預設情況下 keepAnnotated()
就是會為 true 的,除非當開發者手動將 keepRuntimeAnnotatedClasses
設為 false。
綜上兩點所述和前面對 MultiDexTransform#computeList()
方法所述,最終打入 maindex 中的 class 會有以下幾個部分組成:
- 預設的 keep 規則中的類(如 application、annotation);
- 開發者通過 multiDexKeepProguard 配置的類;
- 前兩者所涉及到的類的引用類;
- 所有類本身、類方法、類欄位其中任一被 執行時註解 所修飾的類;(可選項)
- 開發者通過 multiDexKeepFile 配置的類。
MainDex 瘦身
根據以上五點我們不難總結出以下幾個優化點:
1.註解類不要寫成內部類:眼尖的小夥伴發現本文第三張配圖中,實際上內部類 a 是註解類,但是外部類 a 並不是註解類,但是由於內部類 a 是外部類 a 的內部類(emm..)所以實際上外部類 a 也會被 keep 住並被打入 componentClasses.jar
中,而 componentClasses.jar
中所有類的引用類將會被打入 maindex 中。這很可怕,舉個例子,如果開發者在一個龐大的 activity 中寫了一個註解內部類,那麼該 activity 的引用類都將會被打入 maindex,那麼可想而知 maindex 多麼容易被打爆。
2.如果僅僅是想打 一個 類到 maindex 裡面,那麼請使用 multiDexKeepFile 配置檔案進行配置,因為使用 multiDexKeepProguard 配置的配置類,不僅是其本身,還有它的直接引用類、間接引用類都將會被打入 maindex。
3.註解類 RetentionPolicy 規範化:如果不是用於反射的註解,那麼沒有必要將它設為 RUNTIME
的,這樣就可以減少第四點中所提及的類。
4.筆者在前面標記了第四點為可選項是因為實際上開發者可以通過在 app/build.gradle 中配置以下閉包,這樣的話就不會進行第四項規則匹配——
android { dexOptions { keepRuntimeAnnotatedClasses false } } 複製程式碼
當設定以上閉包後,maindex 將不會再掃描類本身、類方法或類欄位被執行時註解所修飾的類,也並不會將它們打入 maindex 中,這是一個減小 maindex 體積的瘦身利器!
容易忽略的地方
前面總結了幾點瘦身的建議,但是還是有很多容易令人忽略的地方:
1.由於混淆執行在打 dex 之前,這意味著開發者試圖想要 keep 的類名可能已經被混淆過了,所以在使用 multiDexKeepProguard/multiDexKeepFile 配置的時候,開發者需要先在 proguard-rules.pro
中配置該類相關資訊。
2.前面一直談論的是 MultiDexTransform 原始碼,筆者在文章前說過除了 MultiDexTransform 還可以是 MainDexListTransform ——

注意到 290 行的註釋以及 270-277 行的程式碼可知實際上沒多大變化,只是前面提到的 keepRuntimeAnnotatedClasses
規則也同樣適用於 multiDexKeepFile 所配置的檔案,而在 MultiDexTransform 中 keepRuntimeAnnotatedClasses
是不會適用於 multiDexKeepFile 所配置的檔案,所以前面提到的第2點優化不適用於 MainDexListTransform。那麼什麼時候 gradle 編譯的時候是如何選擇 MultiDexTransform 與 MainDexListTransform 的呢?答案位於 TaskManager 類中 ——


如果開發者在 gradle.properties 檔案中顯式配置 android.useDexArchive=false
(預設為true,無需配置)則將選擇 MultiDexTransform,如果當前是 debug buildType 則選用 MainDexListTransform,最後就是取決於 android.enableD8
的值了。
其它優化
在實際專案中也許並不是由筆者說的這麼簡單,一方面是由於歷史程式碼遺留問題,不方便重構前人所寫的不規範的註解類;另一方面 java 或三方庫提供的註解我們無法修改,例如 javax 包中的註解都是 RUNTIME
的,因為服務端不會像客戶端一般對效能要求更為嚴苛,而 Dagger2 引用的就是 javax 包中的註解,例如像 butterknife 10.0.0 版本中的註解類已被改成為 RUNTIME 等等等等;還有可能一概而論的忽略所有的使用 RUNTIME 註解的類可能會有一定的麻煩與風險。也許很多場景下並不能夠簡單使用 keepRuntimeAnnotatedClasses
來解決問題,針對這種問題筆者開源了 thinAnnotation ,這個開源庫可以在混淆之後,打 dex 之前將開發者配置的註解類刪除,從而使得構造 maindex 的時候減少該註解類及使用該註解類的類的引入,更加具體的介紹歡迎各位讀者去閱讀 README 了(本文樣例也放在了 thinAnnotation 中)。