若是你對本文感興趣,也許你對個人公衆號也會有興趣,可掃下方二維碼或搜索公衆微信號:mxszgg java
![]()
tips: 本文基於 AGP 3.0.1 源碼分析android
「maindex method 超過 65536 了,咋被打爆了呢?」git
在過去很長一段時間內咱們的應用 maindex 會被打爆,因而大佬們使用了DexKnifePlugin 來解決問題,可是後來 AGP 上了 3.0.1 以及其餘問題的出現,DexKnifePlugin 已經不是可以很良好地適用於咱們的 app 中了,因而巴神(公衆號:巴巴巴掌)用了另外一個比較優雅的方案,經過 hook transform 來達到了咱們的目的,可是終究是一個 hook 方案而且它不可以運用於 D8 編譯器,因而須要一個更加優雅的方案,咱們必須得從源碼中瞭解到究竟什麼樣的類會打入 maindex——github
打開 MultiDexTransform/MainDexListTransform 源碼(本文以 MultiDexTransform 爲例),直接看向 transform()
源碼 ——bash
直接看向 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 規則所生成的 ——app
進行完第二步,就是第三步 computeList()
了——ide
該方法第一步是計算全部的 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 會有如下幾個部分組成:
根據以上五點咱們不難總結出如下幾個優化點:
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 ——
首先進行 callDx()
,可是 292 行第一個參數爲 false,這將不會對應用中全部的類進行註解掃描,緊接着 266-268 行是添加 multiDexKeepFile 所配置的文件,最後 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 中)。