前言:在android5.0之前,每一個android應(yīng)用中只會含有一個dex文件届垫,但是這個dex的方法數(shù)量被限制在65535之內(nèi)释液,這就是著名的64K(64*1024)事件。為了解決這個問題装处,Google官方推出了這個類似于補丁一樣的support-library,MultiDex误债。上一篇文章我們已經(jīng)了解了Multidex的使用及原理,詳見Android使用Multidex突破64K方法數(shù)限制原理解析。本篇文章我會將在使用Multidex的過程中遇到的一些坑點進行總結(jié)妄迁。
MultiDex引發(fā)的問題
周二的晚上愉快地寫著Android
代碼,往工程里引入了一個默默無聞的jar然后Run了一下寝蹈, 經(jīng)過漫長的等待AndroidStudio
構(gòu)建失敗了。wtf ? 發(fā)生了什么登淘?
emmm......帶著疑惑查看錯誤信息:
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文件的過程中產(chǎn)生了錯誤箫老。
早期的Dex文件保存所有classes的方法個數(shù)的范圍在0~65535之間。業(yè)務(wù)一直在增長黔州,寫(copy)的代碼越來越長引入的庫越來越多耍鬓,超過這個范圍只是時間問題。這個問題怎么破流妻?牲蜀?太陽底下木有新鮮事,淡定先google
一發(fā)绅这,找找已經(jīng)踩過坑的小伙伴各薇。StackOverflow
的網(wǎng)友們對該問題表示情緒穩(wěn)定,談笑間拋出multiDex
君躺。
我們來看看Android官方文檔對此是如何解釋的:
1.Dalvik Executable (DEX)文件的總方法數(shù)限制在65536以內(nèi)峭判,其中包括Android framwork method, lib method (后來發(fā)現(xiàn)僅僅是Android 自己的框架的方法就已經(jīng)占用了1w多)棕叫,還有你的 code method 林螃,所以請使用MultiDex。
2.對于5.0以下版本俺泣,請使用multidex support library (這個是我們的補丁包疗认!build tools 請升級到21)。
3.而5.0及以上版本伏钠,由于ART模式的存在横漏,app第一次安裝之后會進行一次預(yù)編譯(pre-compilation) ,如果這時候發(fā)現(xiàn)了classes(..N).dex文件的存在就會將他們最終合成為一個.oat的文件(嗯看起來很厲害的樣子)熟掂。
同時Google
建議review
代碼的直接或者間接依賴缎浇,盡可能減少依賴庫,設(shè)置proguard
參數(shù)進一步優(yōu)化去除無用的代碼赴肚。嗯素跺,這兩個實施起來倒是很簡單二蓝,但是治標(biāo)不治本,躲得過初一躲不過十五指厌。
在Google給出這個解決方案之前刊愚,他們的開發(fā)人員先給了一個簡易版本的multiDex。(懷疑后來的官方解決方案就有這家伙參與)踩验。簡單地說就是:1.先把你的app 的class 拆分成主次兩個dex鸥诽。2.你的程序運行起來后,自己把第二個dex給load進來箕憾⊙么看就這么簡單!
第一回合 天真的官方補丁方案
還是先解決打包問題厕九,回頭再研究那些高深的動態(tài)化加載技術(shù)蓖捶。考慮到投入產(chǎn)出比扁远,決定使用Google官方的multiDex解決俊鱼。(Google的補丁方案啊,不會再有坑了吧畅买?后面才發(fā)現(xiàn)還是太天真) 該方案有兩步:
- 1.修改gradle腳本來產(chǎn)生多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帝火。如果當(dāng)前代碼已經(jīng)繼承自其它Application沒辦法修改那也行,就重寫 Application的attachBaseContext()這個方法湃缎。
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
使用起來還是挺簡單的嘛, run一下犀填,可以了!但是等等嗓违。九巡。。dex過程好像變慢了蹂季。冕广。。這是怎么肥事偿洁?
官方文檔還寫明了multiDex support lib 的使用局限撒汉。瞄一下是什么:
1.在應(yīng)用安裝到手機上的時候dex文件的安裝是復(fù)雜的(complex)有可能會因為第二個dex文件太大導(dǎo)致ANR。請用proguard優(yōu)化你的代碼涕滋。(呵呵...)
2.使用了mulitDex的App有可能在4.0(api level 14)以前的機器上無法啟動睬辐,因為Dalvik linearAlloc bug。請多多測試自祈多福。用proguard優(yōu)化你的代碼將減少該bug幾率溉委。(呵呵...)
3.使用了mulitDex的App在runtime期間有可能因為Dalvik linearAlloc limit Crash。該內(nèi)存分配限制在 4.0版本被增大爱榕,但是5.0以下的機器上的Apps依然會存在這個限制瓣喊。
4.主dex被dalvik虛擬機執(zhí)行時候,哪些類必須在主dex文件里面這個問題比較復(fù)雜黔酥。build tools 可以搞定這個問題藻三。但是如果你代碼存在反射和native的調(diào)用也不保證100%正確。(呵呵...)
感覺這就是個坑啊跪者。補丁方案又引入一些其他問題棵帽。但是插件化方案要求對現(xiàn)有代碼有比較大的改動,代價太大渣玲,而且動態(tài)化加載框架意味著維護成本更高逗概,會有更多潛在bug。所以先測試忘衍,遇到有問題的版本再解決逾苫。
第二回合 啥?dexopt failed枚钓?
呵呵铅搓,部分低端2.3機型(話說2.3版本的android機有高端機型么)安裝失敗搀捷!INSTALL_FAILED_DEXOPT星掰。
apk是一個zip壓縮包,dalvik每次加載apk都要從中解壓出class.dex文件嫩舟,加載過程還涉及到dex的classes需要的雜七雜八的依賴庫的加載氢烘,真耗時間。于是Android決定優(yōu)化一下這個問題家厌,在app安裝到手機之后威始,系統(tǒng)運行dexopt程序?qū)ex進行優(yōu)化,將dex的依賴庫文件和一些輔助數(shù)據(jù)打包成odex文件像街。存放在cache/dalvik_cache目錄下黎棠。保存格式為apk路徑 @ apk名 @ classes.dex。這樣以空間換時間大大縮短讀取/加載dex文件的過程镰绎。
那剛才那個bug是啥問題呢脓斩,原來dexopt程序的dalvik分配一塊內(nèi)存來統(tǒng)計你的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的最大方法個數(shù),如果寫小一點可能會產(chǎn)生好幾個dex。好了 現(xiàn)在2.3的機器可以安裝run起來了燎猛!
第三回合 ANR的意思就是Application Not Responding
問題又來了恋捆!這次不僅僅是2.3 的機型!還有一些中檔配置的4.x系統(tǒng)的機型重绷。問題現(xiàn)象是:第一次安裝后沸停,點擊圖標(biāo),1s昭卓,2s愤钾,3s... 程序沒有任何反應(yīng)就好像你沒點圖標(biāo)一樣。5s過去候醒。能颁。。程序ANR!
其實不僅僅這個App存在這個問題倒淫,其他很多App也存在首次安裝運行后幾秒都無任何響應(yīng)的現(xiàn)象或者最后ANR了伙菊。唯一的例外是美團App,點擊圖標(biāo)立馬就出現(xiàn)界面敌土。唉要不就算啦占业?反正就一次。纯赎。谦疾。不行,這可是產(chǎn)品給用戶的第一印象啊太重要了犬金,而且美團搞得定就說明這問題有解決方案念恍。
ANR了是不是局限1描述的現(xiàn)象?晚顷?不過也不重要...因為Google只是告訴你說第二個dex太大了導(dǎo)致的峰伙。并沒有進一步解釋根本原因。怎么辦该默?Google一發(fā)瞳氓?搜索點擊圖標(biāo) 然后ANR?怎么可能有解決方案嘛栓袖。ANR就意味著UI線程被阻塞了匣摘,老老實實查看log吧。
adb logcat -v time > log.txt于是發(fā)現(xiàn) 是 install dex + dexopt 時間太長裹刮!
梳理一下流程:
- 安裝完app點擊圖標(biāo)之后音榜,系統(tǒng)木有發(fā)現(xiàn)對應(yīng)的process,于是從該apk抽取classes.dex(主dex) 加載捧弃,觸發(fā) 一次dexopt赠叼。
- App 的laucherActivity準(zhǔn)備啟動 擦囊,觸發(fā)Application啟動, Application的 onattach()方法調(diào)用嘴办,這時候MultiDex.install()調(diào)用瞬场,classes2.dex 被install,再次觸發(fā)dexopt涧郊。
- 然后Applicaition onCreate()執(zhí)行贯被。然后 launcher Activity真的起來了。
這些必須在5s內(nèi)完成不然就ANR給你看底燎!有點棘手刃榨。
首先主dex是無論如何都繞不過加載和dexopt的弹砚。如果主dex比較小的話可以節(jié)省時間双仍。主dex小就意味著后面的dex大啊,MultiDex.install()是在主線程里做的桌吃,總時間又沒有實質(zhì)性改變朱沃。install() 能不能放到線程里做啊茅诱?貌似不行逗物。。瑟俭。如果異步化翎卓,什么時候install完成都不知道。這時候如果進程需要seconday.dex里的classes信息不就悲劙诩摹失暴?主dex越小這個錯誤幾率就越大。要悲劇啊微饥。
對于這個問題美團的主要思路是:精簡主dex+異步加載secondary.dex 逗扒。對異步化執(zhí)行速度的不確定性,他們的解決方案是重寫Instrumentation execStartActivity 方法欠橘,hook跳轉(zhuǎn)Activity的總?cè)肟谧雠袛嗑丶纾绻?dāng)前secondary.dex 還沒有加載完成,就彈一個loading Activity等待加載完成肃续,如果已經(jīng)加載完成那最好不過了黍檩。
那我們照搬美團的解決方案不就好了咯?說是這么說, 但是在照搬方案之前,我們需要考慮以下幾個方面的問題:
1..分析主dex需要的classes這個腳本比較難寫始锚。建炫。。
Google文檔說過這個問題比較復(fù)雜疼蛾, 而且buildTools 不是已經(jīng)幫我們搞定了嗎肛跌?去瞄一下主dex的大小:8M 以及secondary.dex 3M 。 它是如何工作的衍慎?文檔說dx的時候转唉,先依據(jù)manifest里注冊的組件生成一個 main-list,然后把這list里的classes所依賴的classes找出來稳捆,把他們打成classes.dex就是主dex赠法。剩下的classes都放clsses2.dex(如果使用參數(shù)限制dex大小的話可能會有classe3.dex 等等) 。主dex至少含有main-list 的classes + 直接依賴classes 乔夯,使用mini-main-list參數(shù)可以僅僅包含剛才說的classes砖织。
關(guān)于寫分析腳本的思路是:直接使用mini-main-list參數(shù)獲取build目錄下的main-list文件,這樣manifest聲明的類和他們的直接依賴類搞定的了末荐,那后者的直接依賴類怎么解侧纯?這些在dvk runtime也是必須的classes。一個思路是解析class文件獲得該class的依賴類甲脏。還一個思路是自己使用Dexclassloader 加載dex眶熬,然后hook getClass()方法,調(diào)用一次就記錄一個块请。都挺折騰的娜氏。2..由于歷史客觀原因,公司項目在維護的App的manifest注冊的組件的那些類,承載業(yè)務(wù)太多,依賴很多三方j(luò)ar步咪,導(dǎo)致直接依賴類非常多萌抵,而且短時間內(nèi)無法梳理精簡,沒辦法mini化主dex。
3..Application的啟動入口太多。。最岗。
Appication初始化未必是由launcher Activity的啟動觸發(fā),還有可能是因為Service 朝捆,Receiver 般渡,ContentProvider 的啟動。 靠攔截重寫Instrumentation execStartActivity 解決不了問題芙盘。要為 Service 驯用,Receiver ,ContentProvider 分別寫基類儒老,然后在oncreate()里判斷是否要異步加載secondary.dex蝴乔。如果需要,彈出Loading Acitvity驮樊?用戶看到這個會感覺比較怪異薇正。
結(jié)合自身App的實際情況來看美團的拆包方案雖然很美好然但是不能照搬啊片酝。此時此刻,我的心情是拔涼拔涼的!┭┮﹏┭┮
第四回合 換一種思路解決
考慮到前面的種種困難, 還是不要寫分析腳本了吧。挖腰。畢竟投入產(chǎn)出嚴(yán)重失衡啦~~現(xiàn)在我們的問題變成了:既希望在Application的attachContext()方法里同步加載secondary.dex雕沿,又不希望卡住UI線程。如果思路限制在線程異步化上猴仑,確實不可能實現(xiàn)审轮。
對此, FB的解決思路特別贊,讓Launcher Activity在另外一個進程啟動辽俗!當(dāng)然這個Launcher Activity就是用來load dex 的 疾渣,load完成就啟動Main Activity。
app在安裝完成之后第一次啟動時崖飘,是secondary.dex的dexopt花費了更多的時間,認(rèn)識到這點非常重要榴捡,使得問題又轉(zhuǎn)化為:在不阻塞UI線程的前提下,完成dexopt坐漏,以后都不需要再次dexopt薄疚,所以可以在UI線程install dex 了碧信!
因此,以下給出對FB解決方案的改進版:
先來看一下解決問題的思路流程圖:
上最終解決問題版的代碼赊琳!
- 在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 {
// 標(biāo)記
public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest";
@Override
protected void attachBaseContext(Context base) {
super .attachBaseContext(base);
LogUtils.d( "loadDex", "App attachBaseContext ");
//版本在5.0以下并且未執(zhí)行過dexopt
if (!quickStart() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {//>=5.0的系統(tǒng)默認(rèn)對dex進行oat優(yōu)化
if (needWait(base)){ // 需要等待
waitForDexopt(base); // 等待
}
MultiDex.install (this );
} else {
return;
}
}
@Override
public void onCreate() {
super .onCreate();
if (quickStart()) {
return;
}
...
}
// 是否執(zhí)行過dexopt
public boolean quickStart() {
if (StringUtils.contains( getCurProcessName(this), ":mini")) {
LogUtils.d( "loadDex", ":mini start!");
return true;
}
return false ;
}
//是否需要等待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 操作完成
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();
}
// 獲取當(dāng)前進程名字
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 ;
}
// 等待 進入LoadDexActivity
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 ;//實測發(fā)現(xiàn)某些場景下有些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的方法getPackageInfo
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();
}
在Application啟動的時候會檢測dexopt是否已經(jīng)完成過砰碴,(檢測方式是查看sp文件是否有dex文件的SHA1-Digest記錄躏筏,這里要兩個進程讀取該sp,讀取模式是MODE_MULTI_PROCESS)。如果沒有就啟動LoadDexActivity(屬于:mini進程) 呈枉。否則就直接install dex 趁尼!對,直接install猖辫。通過日志發(fā)現(xiàn)酥泞,已經(jīng)dexopt的dex文件再次install的時候 只耗費幾十毫秒。
- LoadDexActivity 的邏輯比較簡單啃憎,啟動AsyncTask 來install dex 這時候會觸發(fā)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);
// 執(zhí)行dexopt操作
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); // 退出當(dāng)前進程
}
}
@Override
public void onBackPressed() {
//cannot backpress
}
- Manifest.xml 里面指定LoadResActivity啟動模式和運行進程
<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èn)的出現(xiàn)動畫 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辛萍。Perfect C蹑ⅰ!贩毕!
總結(jié)~~
OK. Multidex使用過程中遇到的坑,總算是完美解決了悯许。感謝cctv...
MultiDex的問題難點在:要持續(xù)解決好幾個bug才能最終解決問題。進一步的辉阶,想要仔細分辨且解決這些bug先壕,就必須持續(xù)探索一些關(guān)聯(lián)性的概念和原理瘩扼。
耗費了這么多時間來解決了Android系統(tǒng)的缺陷是不是有點略傷心。這不應(yīng)該是Google給出一個比較徹底的解決方案嗎垃僚?
時間不早了, 我該睡覺了~~ O(∩_∩)O哈哈~