其實你不知道MultiDex到底有多坑

就在前幾天,公司正在作的項目遇到了方法數越界的問題,當時真是醉了。想一想也難怪,項目中的依賴庫就有三十多個,方法數不越界纔怪。因此立刻上網尋找解決方法,因而找到了下面這篇文章,以爲文章講解的很全面,因此轉載過來,分享給更多的人。最後感謝做者的分享精神。html

原文連接:其實你不知道MultiDex到底有多坑java

遭遇MultiDex

愉快地寫着Android代碼的總悟君往工程裏引入了一個默默無聞的jar而後Run了一下, 通過漫長的等待AndroidStudio構建失敗了。因而總悟君帶着疑惑查看錯誤信息。android

UNEXPECTED TOP-LEVEL EXCEPTION: java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536 at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501) at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276) at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490) at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167) at com.android.dx.merge.DexMerger.merge(DexMerger.java:188) at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439) at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287) at com.android.dx.command.dexer.Main.run(Main.java:230) at com.android.dx.command.dexer.Main.main(Main.java:199) at com.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebug FAILED 

看起來是:在試圖將 classes和jar塞進一個Dex文件的過程當中產生了錯誤。git

早期的Dex文件保存全部classes的方法個數的範圍在0~65535之間。業務一直在增加,總悟君寫(copy)的代碼愈來愈長引入的庫愈來愈多,超過這個範圍只是時間問題。github

怎麼解??太陽底下木有新鮮事,淡定先google一發,找找已經踩過坑的小夥伴。api

StackOverflow 的網友們對該問題表示情緒穩定,談笑間拋出multiDex。微信

這是Android官網對當初的短視行爲給出的補丁方案。文檔說,Dalvik Executable (DEX)文件的總方法數限制在65536之內,其中包括Android framwork method, lib method (後來總悟君發現僅僅是Android 本身的框架的方法就已經佔用了1w多),還有你的 code method ,因此請使用MultiDex。 對於5.0如下版本,請使用multidex support library (這個是咱們的補丁包!build tools 請升級到21)。而5.0及以上版本,因爲ART模式的存在,app第一次安裝以後會進行一次預編譯(pre-compilation) ,若是這時候發現了classes(..N).dex文件的存在就會將他們最終合成爲一個.oat的文件,嗯看起來很厲害的樣子。微信開發

同時Google建議review代碼的直接或者間接依賴,儘量減小依賴庫,設置proguard參數進一步優化去除無用的代碼。嗯,這兩個實施起來卻是很簡單,可是治標不治本,躲得過初一躲不過十五。 在Google給出這個解決方案以前,他們的開發人員先給了一個簡陋簡易版本的multiDex具體參看這裏。(懷疑後來的官方解決方案就有這傢伙參與)。簡單地說就是:1.先把你的app 的class 拆分紅主次兩個dex。2.你的程序運行起來後,本身把第二個dex給load進來。看就這麼簡單!並且這就是個動態加載模塊的框架! 然而總悟君早已看穿Dalvik VM 這種動態加載dex 的能力歸根結底仍是由於java 的classloader類加載機制。沿着這條道走,Android模塊動態化加載,包括dex級別和apk級別的動態化加載,各類玩法層出不窮。app

參見這裏:框架

1.dynamic-load-apk 
2.android-pluginmgr 
3.AndroidDynamicLoader 
4.ACDD 
5.apkplug 
6.DroidPlugin

第一回合 天真的官方補丁方案

仍是先解決打包問題,回頭再研究那些高深的動態化加載技術。偷懶一下咯考慮到投入產出比,決定使用Google官方的multiDex解決。(Google的補丁方案啊,不會再有坑了吧?後面才發現仍是太天真) 該方案有兩步: 
1.修改gradle腳原本產生多dex。 
2.修改manifest 使用MulitDexApplication。 
步驟1.在gradle腳本里寫上:

android {
    compileSdkVersion 21 buildToolsVersion "21.1.0" defaultConfig { ... minSdkVersion 14 targetSdkVersion 21 ... // Enabling multidex support. multiDexEnabled true } ... } dependencies { compile 'com.android.support:multidex:1.0.0' }

步驟2. manifest聲明修改

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.multidex.myapplication"> <application ... android:name="android.support.multidex.MultiDexApplication"> ... </application> </manifest>

若是有本身的Application,繼承MulitDexApplication。若是當前代碼已經繼承自其它Application沒辦法修改那也行,就重寫 Application的attachBaseContext()這個方法。

@Override
protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); }

run一下,能夠了!可是dex過程好像變慢了。。。

文檔還寫明瞭multiDex support lib 的侷限。瞄一下是什麼: 
1.在應用安裝到手機上的時候dex文件的安裝是複雜的(complex)有可能會由於第二個dex文件太大致使ANR。請用proguard優化你的代碼。呵呵。

2.使用了mulitDex的App有可能在4.0(api level 14)之前的機器上沒法啓動,由於Dalvik linearAlloc bug(Issue 22586) 。請多多測試自祈多福。用proguard優化你的代碼將減小該bug概率。呵呵。

3.使用了mulitDex的App在runtime期間有可能由於Dalvik linearAlloc limit (Issue 78035) Crash。該內存分配限制在 4.0版本被增大,可是5.0如下的機器上的Apps依然會存在這個限制。

4.主dex被dalvik虛擬機執行時候,哪些類必須在主dex文件裏面這個問題比較複雜。build tools 能夠搞定這個問題。可是若是你代碼存在反射和native的調用也不保證100%正確。呵呵。

感受這就是個坑啊。補丁方案又引入一些問題。可是插件化方案要求對現有代碼有比較大的改動,代價太大,並且動態化加載框架意味着維護成本更高,會有更多潛在bug。因此先測試,遇到有問題的版本再解決。

第二回合 啥?dexopt failed?

呵呵,部分低端2.3機型(話說2.3版本的android機有高端機型麼)安裝失敗!INSTALL_FAILED_DEXOPT。這個就是前面說的Issue 22586問題。 
apk是一個zip壓縮包,dalvik每次加載apk都要從中解壓出class.dex文件,加載過程還涉及到dex的classes須要的雜七雜八的依賴庫的加載,真耗時間。因而Android決定優化一下這個問題,在app安裝到手機以後,系統運行dexopt程序對dex進行優化,將dex的依賴庫文件和一些輔助數據打包成odex文件。存放在cache/dalvik_cache目錄下。保存格式爲apk路徑 @ apk名 @ classes.dex。這樣以空間換時間大大縮短讀取/加載dex文件的過程。 
那剛纔那個bug是啥問題呢,原來dexopt程序的dalvik分配一塊內存來統計你的app的dex裏面的classes的信息,因爲classes太多方法太多超過這個linearAlloc 的限制 。那減少dex的大小就能夠咯。 
gradle腳本以下:

android.applicationVariants.all { variant -> dex.doFirst{ dex-> if (dex.additionalParameters == null) { dex.additionalParameters = [] } dex.additionalParameters += '--set-max-idx-number=48000' } }

–set-max-idx-number=用於控制每個dex的最大方法個數,寫小一點能夠產生好幾個dex。 踩過更多坑的FB的工程師表示這個linearAlloc的限制不只僅在安裝時候的dexopt程序裏7,還在你的app的dalvik rumtime裏。(很顯然啊dvk vm的宿主進程fork自於同一個母體啊)。爲了表示對這個坑的不滿以及對Google的產品表示遺憾,FB工程師Read The Fucking Source Code找到了一個hack方案。這個linearAlloc的size定義在c層並且是一個全局變量,他們經過對結構體的size的計算成功覆蓋了該值的內容,這裏要特別感謝C語言的指針和內存的設計。C的世界裏,You Are The King of This World。固然實際狀況是大部分用戶用這把利刃割傷了本身。。。別問總悟君誰是世界上最好的語言。。。

爲FB的工程師的機智和務實精神點贊!然而總悟君不肯意花那麼多精力實現FB的hack方法。(dvk虛擬機c層代碼在2.x 4.x 版本里有變動,找到那個內存地址太難,未必搞得定啊)咱們有偷懶的解決方案,爲了不2.3機型runtime 的linearAlloclimit ,最好保持每個dex體積<4M ,剛纔的的value<=48000 
好了 如今2.3的機器能夠安裝run起來了!

第三回合 ANR的意思就是Application Not Responding

問題又來了!此次不只僅是2.3 的機型!還有一些中檔配置的4.x系統的機型。問題現象是:第一次安裝後,點擊圖標,1s,2s,3s… 程序沒有任何反應就好像你沒點圖標同樣。

5s過去。。。程序ANR! 
其實不只僅總悟君的App存在這個問題,其餘不少App也存在首次安裝運行後幾秒都無任何響應的現象或者最後ANR了。惟一的例外是美團App,點擊圖標立馬就出現界面。唉要不就算啦?反正就一次。。。不行,這但是產品給用戶的第一印象啊過重要了,並且美團搞得定就說明這問題有解決方案。

ANR了是否是侷限1描述的現象??不過也不重要…由於Google只是告訴你說第二個dex太大了致使的。並無進一步解釋根本緣由。怎麼辦?Google一發?搜索點擊圖標 而後ANR?怎麼可能有解決方案嘛。ANR就意味着UI線程被阻塞了,老老實實查看log吧。

adb logcat -v time > log.txt

因而發現 是 install dex + dexopt 時間太長! 
梳理一下流程: 
安裝完app點擊圖標以後,系統木有發現對應的process,因而從該apk抽取classes.dex(主dex) 加載,觸發 一次dexopt。 
App 的laucherActivity準備啓動 ,觸發Application啓動, 
Application的 onattach()方法調用,這時候MultiDex.install()調用,classes2.dex 被install,再次觸發dexopt。 
而後Applicaition onCreate()執行。 
而後 launcher Activity真的起來了。 
這些必須在5s內完成否則就ANR給你看!

有點棘手。首先主dex是不管如何都繞不過加載和dexopt的。若是主dex比較小的話能夠節省時間。主dex小就意味着後面的dex大啊,MultiDex.install()是在主線程裏作的,總時間又沒有實質性改變。install() 能不能放到線程裏作啊?貌似不行。。。若是異步化,何時install完成都不知道。這時候若是進程須要seconday.dex裏的classes信息不就悲劇?主dex越小這個錯誤概率就越大。要悲劇啊總悟君。

淡定,此次Google搜索MultiDex.install 。因而總悟君發現了美團多dex拆包方案。 讀完以後感受看到勝利曙光。美團的主要思路是:精簡主dex+異步加載secondary.dex 。對異步化執行速度的不肯定性,他們的解決方案是重寫Instrumentation execStartActivity 方法,hook跳轉Activity的總入口作判斷,若是當前secondary.dex 尚未加載完成,就彈一個loading Activity等待加載完成,若是已經加載完成那最好不過了。不錯,RTFSC果真是王道。 能夠試一試。

可是有幾個問題須要解決: 
1.分析主dex須要的classes這個腳本比較難寫。。。Google文檔說過這個問題比較複雜, 並且buildTools 不是已經幫咱們搞定了嗎?去瞄一下主dex的大小:8M 以及secondary.dex 3M 。 它是如何工做的?文檔說dx的時候,先依據manifest裏註冊的組件生成一個 main-list,而後把這list裏的classes所依賴的classes找出來,把他們打成classes.dex就是主dex。剩下的classes都放clsses2.dex(若是使用參數限制dex大小的話可能會有classe3.ex 等等) 。主dex至少含有main-list 的classes + 直接依賴classes ,使用mini-main-list參數能夠僅僅包含剛纔說的classes。 
關於寫分析腳本的思路是:直接使用mini-main-list參數獲取build目錄下的main-list文件,這樣manifest聲明的類和他們的直接依賴類搞定的了,那後者的直接依賴類怎麼解?這些在dvk runtime也是必須的classes。一個思路是解析class文件得到該class的依賴類。還一個思路是本身使用Dexclassloader 加載dex,而後hook getClass()方法,調用一次就記錄一個。都挺折騰的。

2.因爲歷史緣由,總悟君在維護的App的manifest註冊的組件的那些類,承載業務太多,依賴不少三方jar,致使直接依賴類很是多,並且短期內沒法梳理精簡,沒辦法mini化主dex。

3.Application的啓動入口太多。Appication初始化未必是由launcher Activity的啓動觸發,還有多是由於Service ,Receiver ,ContentProvider 的啓動。 靠攔截重寫Instrumentation execStartActivity 解決不了問題。要爲 Service ,Receiver ,ContentProvider 分別寫基類,而後在oncreate()裏判斷是否要異步加載secondary.dex。若是須要,彈出Loading Acitvity?用戶看到這個會感受比較怪異。

結合自身App的實際狀況來看美團的拆包方案雖然很美好然可是不能照搬啊。果真不能愉快地回家看動漫了。

第四回合 換一種思路

考慮到剛纔說的2,3緣由,先不要急着動手寫分析腳本。總悟君指望找到更好的方案。問題到如今變成了:既但願在Application的attachContext()方法裏同步加載secondary.dex,又不但願卡住UI線程。若是思路限制在線程異步化上,確實不可能實現。因而發現了微信開發團隊的這篇文章。該文章介紹了關於這一問題 FB/QQ/微信的解決方案。FB的解決思路特別贊,讓Launcher Activity在另一個進程啓動!固然這個Launcher Activity就是用來load dex 的 ,load完成就啓動MainActivity。

微信這篇文章給出了一個很是重要的觀點:安裝完成以後第一次啓動時,是secondary.dex的dexopt花費了更多的時間。認識到這點很是重要,使得問題又轉化爲:在不阻塞UI線程的前提下,完成dexopt,之後都不須要再次dexopt,因此能夠在UI線程install dex 了!文章最後給了一個對FB方案的改進版。

仔細讀完感受徹底可行。 
1.對現有代碼改動量最小。

2.該方案不關注Application被哪一個組件啓動。Activity ,Service ,Receiver ,ContentProvider 都知足。(有個問題要說明:如細心網友指出的那樣,新安裝還未啓動可是收到Receiver的場景下,會致使Load界面出現。這個場景實際出現概率比較少,且僅出現一次。能夠接受。)

3.該方案不限制 Application ,Activity ,Service ,Receiver ,ContentProvider 繼續新增業務。

因而總悟君實現了這篇文章最後介紹的改進版的方法,稍微有一點點擴充。

流程圖以下

這裏寫圖片描述

上最終解決問題版的代碼! 
在Application裏面(這裏不要再繼承自MultiApplication了,咱們要手動加載Dex):

import java.util.Map; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; public class App extends Application { public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest"; @Override protected void attachBaseContext(Context base) { super .attachBaseContext(base); LogUtils.d( "loadDex", "App attachBaseContext "); if (!quickStart() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {//>=5.0的系統默認對dex進行oat優化 if (needWait(base)){ waitForDexopt(base); } MultiDex.install (this ); } else { return; } } @Override public void onCreate() { super .onCreate(); if (quickStart()) { return; } ... } public boolean quickStart() { if (StringUtils.contains( getCurProcessName(this), ":mini")) { LogUtils.d( "loadDex", ":mini start!"); return true; } return false ; } //neead wait for dexopt ? private boolean needWait(Context context){ String flag = get2thDexSHA1(context); LogUtils.d( "loadDex", "dex2-sha1 "+flag); SharedPreferences sp = context.getSharedPreferences( PackageUtil.getPackageInfo(context). versionName, MODE_MULTI_PROCESS); String saveValue = sp.getString(KEY_DEX2_SHA1, ""); return !StringUtils.equals(flag,saveValue); } /** * Get classes.dex file signature * @param context * @return */ private String get2thDexSHA1(Context context) { ApplicationInfo ai = context.getApplicationInfo(); String source = ai.sourceDir; try { JarFile jar = new JarFile(source); Manifest mf = jar.getManifest(); Map<String, Attributes> map = mf.getEntries(); Attributes a = map.get("classes2.dex"); return a.getValue("SHA1-Digest"); } catch (Exception e) { e.printStackTrace(); } return null ; } // optDex finish public void installFinish(Context context){ SharedPreferences sp = context.getSharedPreferences( PackageUtil.getPackageInfo(context).versionName, MODE_MULTI_PROCESS); sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit(); } public static String getCurProcessName(Context context) { try { int pid = android.os.Process.myPid(); ActivityManager mActivityManager = (ActivityManager) context .getSystemService(Context. ACTIVITY_SERVICE); for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager .getRunningAppProcesses()) { if (appProcess.pid == pid) { return appProcess. processName; } } } catch (Exception e) { // ignore } return null ; } public void waitForDexopt(Context base) { Intent intent = new Intent(); ComponentName componentName = new ComponentName( "com.zongwu", LoadResActivity.class.getName()); intent.setComponent(componentName); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); base.startActivity(intent); long startWait = System.currentTimeMillis (); long waitTime = 10 * 1000 ; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ) { waitTime = 20 * 1000 ;//實測發現某些場景下有些2.3版本有可能10s都不能完成optdex } while (needWait(base)) { try { long nowWait = System.currentTimeMillis() - startWait; LogUtils.d("loadDex" , "wait ms :" + nowWait); if (nowWait >= waitTime) { return; } Thread.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }

PackageUtil的方法

public static PackageInfo getPackageInfo(Context context){ PackageManager pm = context.getPackageManager(); try { return pm.getPackageInfo(context.getPackageName(), 0); } catch (PackageManager.NameNotFoundException e) { LogUtils.e(e.getLocalizedMessage()); } return new PackageInfo(); }

這裏使用了classes(N).dex的方式保存了後面的dex而不是像微信目前的作法放到assest文件夾。前面有說到ART模式會將多個dex優化合併成oat文件。若是放置在asset裏面就沒有這個好處了。

Launcher Activity 依然是原來的代碼裏的WelcomeActivity。 
在Application啓動的時候會檢測dexopt是否已經完成過,(檢測方式是查看sp文件是否有dex文件的SHA1-Digest記錄,這裏要兩個進程讀取該sp,讀取模式是MODE_MULTI_PROCESS)。若是沒有就啓動LoadDexActivity(屬於:mini進程) 。不然就直接install dex !對,直接install。經過日誌發現,已經dexopt的dex文件再次install的時候 只耗費幾十毫秒。

LoadDexActivity 的邏輯比較簡單,啓動AsyncTask 來install dex 這時候會觸發dexopt 。

public class LoadResActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); super .onCreate(savedInstanceState); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN , WindowManager.LayoutParams.FLAG_FULLSCREEN ); overridePendingTransition(R.anim.null_anim, R.anim.null_anim); setContentView(R.layout.layout_load); new LoadDexTask().execute(); } class LoadDexTask extends AsyncTask { @Override protected Object doInBackground(Object[] params) { try { MultiDex.install(getApplication()); LogUtils.d("loadDex" , "install finish" ); ((App) getApplication()).installFinish(getApplication()); } catch (Exception e) { LogUtils.e("loadDex" , e.getLocalizedMessage()); } return null; } @Override protected void onPostExecute(Object o) { LogUtils.d( "loadDex", "get install finish"); finish(); System.exit( 0); } } @Override public void onBackPressed() { //cannot backpress }

Manifest.xml 裏面

<activity
    android:name= "com.zongwu.LoadResActivity" android:launchMode= "singleTask" android:process= ":mini" android:alwaysRetainTaskState= "false" android:excludeFromRecents= "true" android:screenOrientation= "portrait" /> <activity android:name= "com.zongwu.WelcomeActivity" android:launchMode= "singleTop" android:screenOrientation= "portrait"> <intent-filter > <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter > </activity>

替換Activity默認的出現動畫 R.anim.null_anim 文件的定義:

<set xmlns:android="http://schemas.android.com/apk/res/android"> <alpha android:fromAlpha="1.0" android:toAlpha="1.0" android:duration="550"/> </set>

如微信開發團隊的這篇文章所說,application啓動了LoadDexActivity以後,自身再也不是前臺進程因此怎麼hold 線程都不會ANR。

系統什麼時候會對apk進行dexopt總悟君其實並無十分明白。經過查看安裝運行的日誌發現,安裝的時候packageManagerService會對classes.dex 進行dexopt 。在調用MultiDex.install()加載 secondary.dex的時候,也會進行一次dexopt 。 這背後的流程究竟是怎樣的?dexopt是如何在另一個進程執行的?若是是另一個進程執行爲什麼會阻塞主app的UI進程? 官方文檔並無詳細介紹這個,那就RTFSC一探究竟吧.

源代碼跟蹤比較長,移步到這裏看吧。

最終章碎碎念

1.MultiDex的問題難點在:要持續解決好幾個bug才能最終解決問題。進一步的,想要仔細分辨且解決這些bug,就必須持續探索一些關聯性的概念和原理

2.耗費了這麼多時間來解決了Android系統的缺陷是否是有點略傷心。這不該該是Google給出一個比較完全的解決方案嗎?

3.FB的工程師們腦洞好大。思考問題的方式很值得借鑑。

4.微信團隊的文章提到逆向了很多App。哈!總悟君感受增加知識拓寬視野的新技能增強。

5.RTFSC是王道。

6.在查看log的過程當中發現一個比較有趣的現象。在App的secondary.dex加載以前竟然先加載了某數字公司的dex!(手機沒有root可是安裝了xx手機助手)再加上以前看到的錯誤堆棧裏Android framework的調用堆棧之間也赫然有他們的代碼。總悟君惡意猜想該app利用了某種手段進行了提權,hook了系統框架代碼,將本身的代碼注入到了每個應用app的進程裏。嗯。。。有趣。。。

相關文章
相關標籤/搜索