Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android關於Dex拆分(MultiDex)技術的解析

Android關於Dex拆分(MultiDex)技術的解析

編輯:關於Android編程

一、前言

關於Android中的分包技術,已經不是什麼新的技術了,網上也有很多解析了,但是他們都是給了理論上的知道和原理解析,並沒有詳細的案例說明,所以這裡我們就來詳細講解一下Android中dex拆分技術的解析。在講解之前,我們還是先來看一下為什麼有這個技術的出現?google為什麼提供這樣的技術。

 

二、背景

在開發應用時,隨著業務規模發展到一定程度,不斷地加入新功能、添加新的類庫,代碼在急劇的膨脹,相應的apk包的大小也急劇增加, 那麼終有一天,你會不幸遇到這個錯誤:
生成的apk在android 2.3或之前的機器上無法安裝,提示:INSTALL_FAILED_DEXOPT
方法數量過多,編譯時出錯,提示:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

無法安裝(Android 2.3 INSTALL_FAILED_DEXOPT)問題,是由dexopt的LinearAlloc限制引起的,在Android版本不同分別經歷了4M/5M/8M/16M限制,目前主流4.2.x系統上可能都已到16M, 在Gingerbread或者以下系統LinearAllocHdr分配空間只有5M大小的, 高於Gingerbread的系統提升到了8M。Dalvik linearAlloc是一個固定大小的緩沖區。在應用的安裝過程中,系統會運行一個名為dexopt的程序為該應用在當前機型中運行做准備。dexopt使用LinearAlloc來存儲應用的方法信息。Android 2.2和2.3的緩沖區只有5MB,Android 4.x提高到了8MB或16MB。當方法數量過多導致超出緩沖區大小時,會造成dexopt崩潰。
超過最大方法數限制的問題,是由於DEX文件格式限制,一個DEX文件中method個數采用使用原生類型short來索引文件中的方法,也就是4個字節共計最多表達65536個method,field/class的個數也均有此限制。對於DEX文件,則是將工程所需全部class文件合並且壓縮到一個DEX文件期間,也就是Android打包的DEX過程中, 單個DEX文件可被引用的方法總數(自己開發的代碼以及所引用的Android框架、類庫的代碼)被限制為65536。

我們知道原因了,但是我們可以看到,google提供了一個方案:Multidex技術來解決這樣的問題,為了兼容老版本SDK,但是我們想一下,是不是所有的項目都會用到這個技術呢?答案肯定不是的,這種問題不是所有的項目都會遇到的,只有當你的項目足夠龐大,導致方法個數超出限制了,才會使用到這種技術。那麼既然要用到,這裡就還是要介紹一下,在介紹這篇文章之前,我們先要准備哪些知識點呢?

1、了解如何使用Ant腳本編譯出一個apk包

2、了解編譯一個apk包出來的整個流程和步驟

3、了解Android中的dx命令和aapt命令的用法

4、了解Android中動態加載機制

關於這些資料,我在之前的文章中有說道,不了解的同學可以轉戰:

Ant腳本編譯一個apk包以及apk包打包的整個流程和步驟可以查看這篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/50740026

關於Android中的動態加載機制不了解的同學可以查看這篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/48104581

只有了解了這四個知識點,下面介紹的內容才能看的明白。所以這兩篇文章希望能夠仔細看一下。

 

三、技術原理

既然准備知識已經做完了,下面我們就來詳細介紹一下拆包的技術原理,其實就是google提供個方案:

第一步:使用dx進行拆包

這裡還有兩個兩類:一個是自動拆包,一個手動拆包

1、自動拆包

使用google在5.0+的SDK開始,dx命令就已經支持:--multi-dex 參數來直接自動分包

\

關於具體命令如何進行拆包,後面講到案例的時候在詳細講解

2、手動拆包

手動拆包其實就是為了解決低版本的SDK,不支持--mulit-dex參數這種情況,所以我們得想個辦法,我們知道,android中有一個步驟就是使用dx命令將class文件變成dex,那麼這裡我們就可以這麼做了,在dx命令執行前,先將javac編譯之後的所有class文件進行分類,然後在對具體的分類使用dx來轉化成dex文件,這樣結果也是有多個dex了。這時候我們可能需要寫一個額外的腳本去進行class分類了,具體分多少種dex,那就自己決定了。

上面就說完了拆包的兩種方式,其實這兩種方式各有優勢和缺點,當然我們提倡的還是使用第一種方式,因為現在SDK已經是5.0+了,而且這種方式也是google所倡導的,而且也方便。

關於上面的兩種分包技術,有一個共同需要注意的地方

就是Android中在分包之後會有多個dex,但是系統默認會先找到classes.dex文件然後自動加載運行,所以這裡就有一個問題,我們需要將一些初始化的重要類放到classes.dex中,不然運行就會報錯或者閃退。

那麼這裡就可以看到這兩種分包技術的區別了:

如果使用第一種分包技術的話,我們可以使用:--main-dex-list 參數來規定哪些class文件歸到主dex中,也就是classes.dex中,剩余的dx會自動更具方法數的限制來進行分類成從dex,比如classes2.dex...classes3.dex...這裡沒有classes1.dex。需要注意這點。同時,subdex是不支持class的歸類的,完全依靠dx自動分類,這個也是為什麼叫做自動拆包的原因吧。

\

但是如果我們要是使用第二種拆包技術的話,就是很隨意的操作了,因為本身分類就是我們自己操作的,所以想怎麼分就怎麼分,而且這裡支持分多少個dex,哪些class歸到哪個dex,都是可以做到的。靈活性比較好,但是這種方式有一個不好就是需要自己寫腳本去歸類,這時候就要非常小心,因為可能會遺漏一個class沒有歸入到具體的dex的話,運行就會報錯,找不到這個類。

 

第二步:Dex的加載

在第一步中我們講解完了拆包技術,如何將class拆分成多個dex。那麼問題也就來了,上面我們也說到了,Android中在運行的時候默認只會加載classes.dex這個dex文件,我們也叫作主dex.那麼拆分之後還有其他的dex怎麼加載到系統運行呢?這時候google就提供了一個方案,使用DexClassLoader來進行動態加載,關於動態加載dex的話,這裡不想講解的太多了,因為我之前的很多文章都介紹了,具體可以參考這篇文章:http://blog.csdn.net/jiangwei0910410003/article/details/48104581

所以我們只要獲取到所有的dex,除了classes.dex之外。我們然後一一的將各個dex加載到系統中即可。那麼這裡有兩個問題需要解決的:

1、如何獲取所有的subdex呢?

這裡我們可以這麼做,在Application的attachContext方法中(因為這個方法的時機最早),我們可以獲取到程序的apk路徑,然後使用ZipFile來解壓apk文件,取到所有的subdex文件即可,具體的操作看後面的案例詳解。

2、我們使用DexClassLoader來加載subdex之後,然後讓系統知道?

這裡我們使用反射的機制,來合並系統的PathClassLoader和DexClassLoader中的DexList變量值。這個技術也是google提供的。具體技術,也是到後面的案例中我們詳細講解。

 

四、案例分析

到這裡我們就介紹完了,我們拆包的兩個步驟:拆分dex和加載dex;下面來使用具體的案例來實踐一下吧,這個也是網上現在很多介紹了dex拆分技術,但是就是沒有具體案例的不好的地方,理論知識誰都知道,但是沒有案例講解的話,就不知道在實際的過程中會遇到什麼問題,以及詳細的操作步驟,所以這裡必須用一個案例來詳細介紹一下。

我們的案例采用ant腳本來編譯,其實現在google推薦的是gradle來編譯,因為這個已經集成到AndroidStudio中了,說句實話,我是因為gradle的語法不太熟,所以就沒用gradle來講解了,當然gradle語法和ant語法很想,如果有同學會gradle的話,可以使用gradle來進行操作一次,這裡我不會太多的介紹ant腳本語法,而是介紹詳細的命令使用。其實所有的編譯腳本理論上就是對編譯命令的優化已經加上一些功能罷了。好了,不多說,看案例:

\

這裡的 main-dex-rule.txt 是我們後面需要用到的主dex包含的class文件清單,my.keystore是簽名apk文件

下面我們先來看一下編譯腳本build.xml

這裡再次說明一下,這裡不會全部解讀,這個腳本是在我之前的一篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/50740026

中的用到的腳本基礎上修改的,所以一定要先看這篇文章呀~~

首先來看一下如何使用dx命令來自動拆分dex的:

 


	
		Generate multi-dex...
	
	
			
			
			
			
			
			
			
			
	
這裡的參數很簡單:

 

參數說明:
--multi-dex:多 dex 打包的開關
--main-dex-list=:參數是一個類列表的文件,在該文件中的類會被打包在第一個 dex 中
--minimal-main-dex:只有在--main-dex-list 文件中指定的類被打包在第一個 dex,其余的都在第二個 dex 文件中

因為後兩個參數是 optional 參數,所以理論上只需給 dx 加上“--multi-dex”參數即可生成出 classes.dex、classes2.dex、classes3.dex等。

這裡我們指定了--main-dex-list 參數,將指定的class合並到classes.dex中,看看main-dex-rule.txt的內容:

\

這裡我們將程序的MyApplication類和入口Activity以及需要加載dex的這三個類合並到主dex中,因為這三個是初始化時機最早的,而且也是加載後面subdex類的重要類,所以必須放到主dex中。不然程序都運行不起來的。當然也要注意內部類的情況,也要進行添加的。

好了,我們運行build之後,發現有一個問題,就是我們只看到了一個dex,那就是classes.dex。為什麼會這樣呢?

再看 dx 的參數,main-dex-list 和 minimal-main-dex 只會影響到主 dex 中包含的文件,不會影響到從 dex 是否生成,所以應該是其他原因造成的。
查不到資料,分析源代碼就是解決問題的不二法門。於是我把 dx.jar 反編譯了一下,通過分析,找到了下面的幾行關鍵代碼:

\
顯然,dx 進行多 dex 打包時,默認每個 dex 中方法數最大為65536。而查看當前應用 dex 的方法數,一共只有51392(方法數沒超標,主要是 LinearAlloc 超標),沒有達到65536,所以打包的時候只有一個 dex。
再繼續分析代碼,發現下面一段關鍵代碼:

\
這說明 dx 有一個隱藏的參數:--set-max-idx-number,這個參數可以用於設置 dx 打包時每個 dex 最大的方法數,但是此參數並未在 dx 的 Usage 中列出(坑爹啊!)。

\
我們在 ant 腳本中把這個參數設置上,暫時設置每個 dex 的方法數最大為:2000,然後在編譯:

\

好吧,這下出來了兩個dex了,這裡我可以發現,沒有classes1.dex的,下標是從2開始的,這個需要注意的。

這裡我們可以用IDA查看dex文件:

classes.dex:

\

classes2.dex:

\

看來是成功了,那麼下面我們在繼續看,現在有了兩個dex文件,下面如何編譯成apk文件呢?

我們之前知道這時候可以在使用apkbuilder命令將我們編譯完的:resource.arsc,asset,dex文件打包成apk文件。

但是這裡遇到一個蛋疼問題來了:

apkbuilder命令支持的參數中,只能包含一個dex文件,說白了就是只能打入一個dex文件。

\

這下蛋疼了,該怎麼辦呢?這裡當時急需驗證結果,先簡單弄了一下,也算是一個小技巧,直接使用WinRar軟件直接把subdex導進去:

\

然後在單獨簽名apk,運行,可以了,心情也是很好的,但是感覺這個不能自動化呀。太黑了,所以得想個辦法,想到了ZipFile這個類,我們可以解壓apk,然後將subdex添加進去,然後把這部分功能打包成一個可執行的jar,然後在build.xml中運行即可。這個也是個方法,但是還是感覺不好,有點繁瑣。思前想後,最後找到了一個好辦法,也是一個很重要的知識點,那就是使用aapt命令來進行操作,我們在之前可能知道aapt命令用來編譯資源文件的,其實他還有一個重要的用途就是可以編輯apk文件:

\

可以看到,我們可以刪除apk中的一個文件,或者是添加一個文件,好的,方法終於找到了,下面就直接操作吧,首先我們單獨用命令來操作一下看看效果:

\

成功了,接下來我們在來看看apk文件:

\

尼瑪,發現還不對,這裡有一個坑,就是我們沒看到classes2.dex,而看到了一個目錄結構,哦,想想應該明白了,aapt命令在添加或者刪除的時候,文件的路徑必須明確,不能是絕對路徑,而是相對路徑,這個相對就要相對apk的跟目錄,我們現在想把classes2.dex放到apk的根目錄下面,那麼就應該直接是classes2.dex,我們可以將dex和apk放到一個目錄下面來進行操作:

\

這時候我們在來看一下apk文件:

\

好吧,成功了,所以這裡我們需要注意的是subdex的路徑。

下面我們繼續來看build.xml腳本內容,看看在拆包成功之後,如何打包成apk中:

 



	
	
		
			
		
	



 
	
	
	  
	
	  
		  
		  
		${dexfile} is not handle  
		  
	  
		${dexfile} is handle
	
		
		
		
	
	  
	 
	 
  



	
	 
		 
			 
		 
	 
 
這裡的大體流程是這樣的:

 

1、遍歷bin目錄下所有的dex文件

2、將dex文件先拷貝到項目的根目錄下面,因為我們的腳本文件是在根目錄下面,運行腳本的時候也是在根目錄下面,所以需要把dex文件拷貝到這裡,不然會發生上面說到的問題,不能使用絕對路徑,而是相對路徑來添加dex文件。

3、因為apk默認的話是包含了classes.dex文件,所以這裡需要過濾classes.dex文件,對其他的dex文件進行aapt命令進行添加。
 

但是這裡當時還遇到了一個問題,就是在ant腳本中如何使用循環,正則表達式,條件判斷等標簽:

具體標簽不說了,主要說明一下如何使用這些標簽?因為默認情況下,這些標簽是不能用的,所以需要導入jar包?

 

網上光說導入這個定義,但是還是報錯的:

 

\

這時候我們還需要把:ant-contrib-1.0b3.jar 放到ant的lib目錄下面,默認是沒有這個jar的。關於這個jar,網上很多,自行下載即可\

這時候我們就可以運行build.xml腳本了。

 

上面就介紹完了使用腳本拆包,以及如何將多個dex打包成apk.

 

下面來看一下代碼如何實現:

代碼中我們定義了四個Activity:

\

MainActivity是我們的入口Activity,SecondaryDexEx是動態加載的核心類

 

首先如何從apk中獲取dex文件:

 

ZipFile apkFile = null;
try {
	apkFile = new ZipFile(appContext.getApplicationInfo().sourceDir);
} catch (Exception e) {
	Log.i("multidex", "create zipfile error:"+Log.getStackTraceString(e));
	return;
}

Log.i("multidex", "zipfile:"+apkFile.getName());

File filesDir = appContext.getDir("odex", Context.MODE_PRIVATE);
Log.i("multidex", "filedir:"+filesDir.getAbsolutePath());
for(int i = 0 ; i < SUB_DEX_NUM; i ++){
	String possibleDexName = buildDexFullName(i);
	Log.i("multidex", "proname:"+possibleDexName);
	ZipEntry zipEntry = apkFile.getEntry(possibleDexName);
	Log.i("multidex", "entry:"+zipEntry);
	if(zipEntry == null) {
		break;
	}
	msLoadedDexList.add(new LoadedDex(filesDir,possibleDexName,zipEntry));
}
先得到應用apk的路徑,使用ZipFile來解壓apk,獲取所有的dex文件,這裡我們不需要處理classes.dex文件了,默認就會加載,所以這裡進行過濾一下,還有就是subdex的下標都是從2開始的,沒有classes1.dex。需要注意的。同時這裡獲取應用的apk文件是不需要任何權限限制的,類似於QQ中可以分享本地app給好友的功能一樣。

 

接下來我們獲取到了所有的subdex之後,然後就可以動態加載了,需要把我們的dex加載到系統中,這裡主要就是使用反射技術合並dexList即可。

 

/**
 * 這裡需要注入DexClassLoader
 * 因為GDT內部點擊廣告是去下載App,是啟動一個DownloadService去下載App
 * 所以這裡需要這麼做,不然會報異常
 * @param loader
 */
private static void inject(DexClassLoader loader, Context ctx){
	PathClassLoader pathLoader = (PathClassLoader) ctx.getClassLoader();
	try {
		Object dexElements = combineArray(
				getDexElements(getPathList(pathLoader)),
				getDexElements(getPathList(loader)));
		Object pathList = getPathList(pathLoader);
		setField(pathList, pathList.getClass(), "dexElements", dexElements);
	} catch (Exception e) {
		Log.i("multidex", "inject dexclassloader error:" + Log.getStackTraceString(e));
	}
}

private static Object getPathList(Object baseDexClassLoader)
		throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
	return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}

private static Object getField(Object obj, Class cl, String field)
		throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
	Field localField = cl.getDeclaredField(field);
	localField.setAccessible(true);
	return localField.get(obj);
}

private static Object getDexElements(Object paramObject)
		throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
	return getField(paramObject, paramObject.getClass(), "dexElements");
}
private static void setField(Object obj, Class cl, String field,
		Object value) throws NoSuchFieldException,
		IllegalArgumentException, IllegalAccessException {

	Field localField = cl.getDeclaredField(field);
	localField.setAccessible(true);
	localField.set(obj, value);
}

private static Object combineArray(Object arrayLhs, Object arrayRhs) {
	Class localClass = arrayLhs.getClass().getComponentType();
	int i = Array.getLength(arrayLhs);
	int j = i + Array.getLength(arrayRhs);
	Object result = Array.newInstance(localClass, j);
	for (int k = 0; k < j; ++k) {
		if (k < i) {
			Array.set(result, k, Array.get(arrayLhs, k));
		} else {
			Array.set(result, k, Array.get(arrayRhs, k - i));
		}
	}
	return result;
}

 

在MyApplication中的attachBaseContext方法中調用loadSecondaryDex方法即可:

\

 

到這裡我們介紹完了腳本和代碼,下面就開跑一邊腳本吧:ant release

\\
跑完之後,我們看一下bin目錄下多了一個成功的apk文件:

\

我們安裝運行,結果如下:

\

看到了,我們首先啟動的是MainActivity入口,然後我們依次點擊btn進入不同的Activity。結果運行正常,同時我們也可以查看一下日志,獲取dex和加載dex是正常的。

\
 

到這裡我們的心情很激動,終於搞定了拆包,遇到一些問題,但是我們都解決了。哈哈~~

 

項目下載:http://download.csdn.net/detail/jiangwei0910410003/9452599

 

五、梳理流程

下面我們來總結一下我們做的哪些工作:

1、首先我們使用腳本進行自動拆包,得到兩個dex文件。

2、因為apkbuilder命令在打apk包的時候,只能包含一個dex文件,所以我們需要在使用aapt命令添加其他的subdex文件

3、得到應用程序的apk文件,然後得到所有的dex文件,在使用DexClassLoader進行subdex的加載。

但是關於我們之前說到的拆包的第二個技術,手動拆包這裡就沒有介紹了,其實很簡單,就是在ant腳本中進行分類copy文件就好了,得到了多個dex之後,同樣使用aapt命令添加到apk中就可以了。

 

六、知識點概要

學習到了哪些知識點:

1、更加深入的了解了ant腳本的語法

2、學習到了使用dx命令來進行自動拆包

3、復習了DexClassLoader的用法

 

七、遇到的錯誤

我們在這個過程中可能會遇到一些錯誤,最多的就是:ClassNotFound和NotDefClass這兩個錯誤,這個處理很簡單的,我們查看是哪個類,然後用IDA查看每個dex文件是否包含了這個類,如果沒有,那說明沒有將這個類歸類進去

 

八、性能分析

在冷啟動時因為需要加載多個DEX文件,如果DEX文件過大時,處理時間過長,很容易引發ANR(Application Not Responding);
采用MultiDex方案的應用可能不能在低於Android 4.0 (API level 14) 機器上啟動,這個主要是因為Dalvik linearAlloc的一個bug (Issue 22586);采用MultiDex方案的應用因為需要申請一個很大的內存,在運行時可能導致程序的崩潰,這個主要是因為Dalvik linearAlloc 的一個限制(Issue 78035). 這個限制在 Android 4.0 (API level 14)已經增加了, 應用也有可能在低於 Android 5.0 (API level 21)版本的機器上觸發這個限制。

Dex分包後,如果是啟動時同步加載,對應用的啟動速度會有一定的影響,但是主要影響的是安裝後首次啟動。這是因為安裝後首次啟動時,Android系統會對加載的從dex做Dexopt並生成ODEX,而 Dexopt 是比較耗時的操作,所以對安裝後首次啟動速度影響較大。在非安裝後首次啟動時,應用只需加載 ODEX,這個過程速度很快,對啟動速度影響不大。同時,從dex 的大小也直接影響啟動速度,即從dex越小則啟動越快。

 

九、總結

關於Android中的拆包技術網上已經有很多案例和解釋了,但是大部分都是理論知識,沒有說到細節的,更沒有什麼demo。所以這篇文章就主要詳細介紹了分包的技術點以及用一個demo來進行案例分析,更加深入的了解了拆包技術,本文中雖然用到了是ant腳本進行編譯的,但是同樣可以使用gradle來進行編譯,只要理解了原理,其實都是命令在操作的,和腳本沒有任何關系的。從此我們也不會在感覺拆包技術多不覺明歷了。其實就是那麼回事。

 

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved