最近項目apk方法數(shù)即將達到65536上限,雖然通過瘦身減少了一些方法數(shù),但是隨著更多sdk的接入,終究還是避免不了方法數(shù)突破限制,所以開始著手dex分包的工作。
之所以存在方法數(shù)不能超過65536的限制主要有兩個原因
1.dex文件格式的限制:dex文件中的方法個數(shù)使用short類型來存儲的,即2個字節(jié),最大值為65536,即單個dex文件的方法數(shù)上限為65536
2.系統(tǒng)對dex文件進行優(yōu)化操作時分配的緩沖區(qū)大小的限制:在android2.x的系統(tǒng)上緩沖區(qū)只有5MB,android4.x為8MB或者16MB,如果方法數(shù)量超過緩沖區(qū)的大小時,會造成dexopt崩潰
所以我們一般apk的方法數(shù)要控制在65536以內,如果超出,就要考慮dex分包。
簡單案例
首先新建一個簡單的工程

MainActivity中有一個button,點擊后跳轉到OtherActivity,MainActivity中調用了HelperOne的方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button)findViewById(R.id.btn);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
Intent intent = new Intent(MainActivity.this, OtherActivity.class);
startActivity(intent);
}
});
HelperOne helperOne = new HelperOne();
helperOne.test();
}
OtherActivity也非常簡單,初始化了HelperTwo對象并調用其方法,而HelperTwo中又調用了HelperThree類的方法
下面先采用ant的方式來對該工程進行多dex打包
dex分包——ant方式
ant腳本執(zhí)行順序
init-> clean-> dirs-> resource-src-> aidl-> compile -> dex-> package-res -> package-> copy_dex -> add-subdex-to-apk -> jarsigner ->zipalign -> debug
整個流程簡單概括如下:
- init、clean、dirs 清理并創(chuàng)建輸出目錄
- resource-src 根據(jù)資源文件、manifest文件生成R.java
- aidl 對aidl文件進行處理生成對應的class文件
- compile 編譯java源文件(包括R.java)生成class文件
- dex 將編譯后的class文件和引入的jar包打包成dex文件,通過配置參數(shù)此處可以生成多個dex文件
- package-res 使用aapt工具處理資源文件,生成.ap_文件,其中包括編譯后的資源文件、資源索引表resources.arsc、manifest文件等
- package 使用apk-builder將上述步驟生成的dex文件(主dex文件)、.ap_文件等打包生成apk文件,需要注意的是這里只能將主dex打包到apk中,不支持同時將多個dex文件打包到apk,所以還需要以下的步驟
- copy_dex 將第5步生成的多個dex文件拷貝至指定目錄
- add-subdex-to-apk 通過aapt工具將上述除主dex外的其他dex文件添加到apk文件中
- jarsigner 對apk文件進行簽名
- zipalign 對apk包進行字節(jié)對齊等優(yōu)化操作
與普通的apk文件流程基本上一致,需要注意的是5、8、9這幾個步驟,下面來詳細看一下這幾個target
5 target dex
<target
name="dex"
depends="compile" >
<echo>Converting compiled files and external libraries into ${outdir}/${dex-file}...</echo>
<apply
executable="${dx}"
failonerror="true"
parallel="true" >
<arg value="--dex" />
<!-- 多dex命令-->
<arg value="--multi-dex" />
<!-- 每個包最大的方法數(shù)為40000 -->
<arg value="--set-max-idx-number=40000" />
<arg value="--main-dex-list" />
<!-- 主dex包含的class文件列表 -->
<arg value="${main-dex-rule}" />
<arg value="--minimal-main-dex" />
<arg value="--output=${outdir}" />
<arg value="${outdir}/classes" />
<fileset
dir="${external-libs}"
includes="*.jar" />
</apply>
</target>
其中 --multi-dex 表明打包成多個dex文件
--set-max-idx-number=40000 表示每個dex文件中最多含有的方法數(shù),可配置
--main-dex-list 指定需要打進主dex文件中的類,參數(shù)為主dex中類的列表文件
--minimal-main-dex 只有在main-dex-list列表中的類才能打進主dex
關于main-dex-list中的類的規(guī)則后面會講到。這里暫且先將multidexTest目錄下的文件放入主dex中,main-dex-rule內容如下
com/example/multidextest/MainActivity.class
com/example/multidextest/HelperOne.class
com/example/multidextest/ApplicationLoader.class
8 copy_dex
<!-- copy dex to dir -->
<target
name="copy_dex"
depends="package" >
<echo message="copy dex..." />
<copy todir="${basedir}" >
<fileset dir="bin" >
<include name="classes*.dex" />
</fileset>
</copy>
</target>
執(zhí)行上述步驟5之后,會在bin目錄下生成多個dex文件,copy_dex就是將該目錄下的dex文件復制到basedir中,方便打入apk
9 add-subdex-to-apk
<target
name="add-subdex-to-apk"
depends="copy_dex" >
<echo message="Add Subdex to Apk ..." />
<foreach
param="dir.name"
target="aapt-add-dex" >
<path>
<fileset
dir="bin"
includes="classes*.dex" />
</path>
</foreach>
</target>
遍歷bin目錄下的dex文件,并將其名稱作為參數(shù)傳遞給target aapt-add-dex,注意ant中使用for循環(huán)需要引入ant-contrib擴展包
<!-- 使用aapt命令添加dex文件 -->
<target name="aapt-add-dex" >
<echo message="${dir.name}" />
<!-- 使用正則表達式獲取classes的文件名 -->
<propertyregex
casesensitive="false"
input="${dir.name}"
property="dexfile"
regexp="classes(.*).dex"
select="\0" />
<!-- 這里不需要添加classes.dex文件 -->
<if>
<equals
arg1="${dexfile}"
arg2="classes.dex" />
<then>
<echo>${dexfile} is not handle</echo>
</then>
<else>
<echo>${dexfile} is handle</echo>
<exec
executable="${aapt}"
failonerror="true" >
<arg value="add" />
<arg value="${out-debug-package-ospath}" />
<arg value="${dexfile}" />
</exec>
</else>
</if>
<delete file="${basedir}\${dexfile}" />
</target>
使用正則表達式獲取dex文件名稱,如果是主dex classes.dex 則不處理,因為主dex在target package中已經(jīng)由apk-builder打到apk中,
如果是其他從dex文件,則調用aapt工具將其添加到apk文件中
生成apk后解壓縮,可以看到apk中已經(jīng)包含了兩個dex: classes.dex和classes2.dex

我們來運行一下apk,可以發(fā)現(xiàn)報錯, 找不到OtherActivity

這是因為在dalvik虛擬機加載apk時只會主動加載主dex,并不會對其他從dex進行處理, 我們在打包時 也沒有將OtherActivity等其他類也打到主dex中,并且也沒有去主動加載從dex,所以導致程序運行時找不到從dex中的類文件。而在art虛擬機上則沒有這個問題,因為其對dex文件的處理又不一樣,下節(jié)再詳細討論。
針對dalvik虛擬機,我們需要手動加載從dex文件,一般為了不影響程序使用,都是在application中去加載從dex。
dex文件的加載
以下的copyDex()方法和loadDex()方法需要在Application類onCreate中依次執(zhí)行,以在apk啟動時加載好所有需要的類。
首先將apk中的subdex復制到私有目錄
private void copyDex() throws Exception{
Log.d(TAG, "copyDex");
//獲取已安裝的apk文件
File originalApk = new File(getApplicationInfo().sourceDir);
//創(chuàng)建臨時apk文件,存放于/data/data/<application package>/files目錄下
File newApk = new File(getFilesDir().getAbsoluteFile() + File.separator + "test.apk");
if (!newApk.exists()) {
newApk.createNewFile();
}
//拷貝apk
copyFile(new FileInputStream(originalApk), new FileOutputStream(newApk));
ZipFile apk = new ZipFile(newApk);
Enumeration<? extends ZipEntry> enumeration = apk.entries();
ZipEntry zipEntry = null;
while (enumeration.hasMoreElements()) {
zipEntry = (ZipEntry) enumeration.nextElement();
//遍歷得到除主dex文件外的其他從dex文件
if (!zipEntry.isDirectory() && zipEntry.getName().endsWith("dex")&& !"classes.dex".equals(zipEntry.getName())) {
Log.d(TAG, "zip entry name " + zipEntry.getName() + " file size "+ zipEntry.getSize());
InputStream is = apk.getInputStream(zipEntry);
FileOutputStream fos = openFileOutput(zipEntry.getName(), MODE_PRIVATE);
//從臨時apk文件中拷貝出從dex文件
copyFile(is, fos);
}
}
apk.close();
}
//拷貝文件
private void copyFile(InputStream is, FileOutputStream fos) {
try {
int hasRead = 0;
byte[] buf = new byte[1024];
while((hasRead = is.read(buf)) > 0) {
fos.write(buf, 0, hasRead);
}
fos.flush();
} catch (Exception e) {
Log.d(TAG, "copyFile error " + e);
} finally {
try {
if (fos != null) {
fos.close();
}
if(is != null) {
is.close();
}
} catch (Exception e2) {
Log.d(TAG, "copyFile close error " + e2);
}
}
}
拷貝完了之后就可以加載對應的從dex文件了
//加載dex
private void loadDex(){
Log.d(TAG, "loadDex");
File[] files = getFilesDir().listFiles();
if (null != files) {
//遍歷files目錄下的dex文件
for(File file: files){
String fileName = file.getName();
if (fileName.endsWith("dex") && !"classes.dex".equals(fileName)) {
injectDex(file.getAbsolutePath());
}
}
}
}
調用injectDex方法加載從dex文件
private String injectDex(String dexPath){
boolean hasBaseDexClassLoaded = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (Exception e) {
Log.d(TAG, "load BaseDexClassLoader fail " + e);
hasBaseDexClassLoaded = false;
}
if (hasBaseDexClassLoaded) {
//獲取PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();
//通過DexClassLoader加載指定路徑的dex文件
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, getDir("dex", 0).getAbsolutePath(), dexPath, getClassLoader());
try {
//通過pathClassLoader獲取已經(jīng)加載dex文件信息,即BaseDexClassLoader的DexPathList屬性,而DexPathList中的dexElements屬性用于保存加載的dex文件相關信息
//獲取DexClassLoader加載的從dex文件信息,并與已經(jīng)加載的dex文件信息合并到一起
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
//獲取pathList屬性
Object pathList = getPathList(pathClassLoader);
//然后通過反射方式設置為dexElements屬性值
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Exception e) {
Log.d(TAG, " " + e);
}
}
return "";
}
//通過反射獲取BaseDexClassLoader的pathList屬性
public Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
public Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
public 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);
}
//獲取DexPathList的dexElements屬性,dexElements用于存放已經(jīng)加載的dex文件
public Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
public 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;
}
關于DexClassLoader和PathClassLoader的區(qū)別以及dex文件加載的過程會在后面的文章中詳細講到,這里不再贅述。
dalvik VS art 在multidex方面的區(qū)別
dalvik和art重要的區(qū)別就是dalvik執(zhí)行的是dex字節(jié)碼,而art虛擬機執(zhí)行的是本地機器馬。dalvik采用的是JIT(Just-in-Time)解釋器在程序運行時動態(tài)的將dex字節(jié)碼翻譯成本地機器碼,并且在程序每次重新運行的時候都需要重復這一步驟,所以dalvik的執(zhí)行效率要低于art。
android 4.4以后開始引入art,art虛擬機執(zhí)行的是本地機器碼,而之前dalvik虛擬機上運行的app中包含的都是dex文件,所以就需要一個將dex文件翻譯成本地機器碼的過程。ART采用的是AOT(Ahead-of-Time),即在程序運行之前就將dex文件翻譯成本地機器碼,時機就在app安裝的時候。
app在安裝的過程中,PackageManagerService會請求守護進程installd來執(zhí)行一次dexopt操作,即對dex字節(jié)碼進行優(yōu)化或者翻譯,如果系統(tǒng)使用的是dalvik虛擬機,則會調用run_dexopt來將dex文件優(yōu)化成odex文件,此處只會對classes.dex文件即主dex文件進行優(yōu)化操作 ,如果系統(tǒng)使用的是art虛擬機,則會調用run_dex2oat來將dex文件翻譯成本地機器碼并保存在oat文件中,如果apk中含有多個dex文件,則會對多個dex文件都會進行解釋處理,并保存到oat文件中。
所以在程序運行時,如果是dalvik虛擬機,則只會加載主dex的odex文件,而從dex文件需要通過multidex的方式手動進行dexopt和加載操作,如果是art虛擬機,則直接加載oat文件即可。
main-dex-list規(guī)則
上述例子中,我們是手動指定了主dex中的類文件,但是一般工程中有太多的文件,不可能靠手動來指定哪些類來打入主dex,所以google再android api21版本之上的sdk build-tools中加入了mainDexClasses 腳本來自動遍歷指定路徑中符合規(guī)則的文件名,并輸出到指定文件中,這個輸出文件就是上面提到的main-dex-rule。
在sdk build-tools下有如下三個文件,其中mainDexClasses為linux下的腳本,mainDexClasses.bat為windows環(huán)境下的bat腳本,mainDexClasses.rules為過濾規(guī)則,只有符合規(guī)則的類才能添加到main-dex-rule中。

對應的命令格式為
mainDexClasses [--output <output file>] <application path>
output file 即輸出的文件, application path為輸入的文件組,可以包括compile之后的classes文件或者其他jar包
通過查看mainDexClasses.bat可以發(fā)現(xiàn)整個處理過程大概分為三步
環(huán)境變量檢查、命令參數(shù)校驗等 包括proguard環(huán)境變量
-
通過使用proguard的shrink功能,根據(jù)mainDexClasses.rules中定義的規(guī)則來對傳入的文件進行裁剪,去掉無關的類,最后生成tmp.jar包
mainDexClasses.rules文件其實就是proguard的規(guī)則文件,如圖
proguard.png
默認的mainDexClasses.rules只是保留了常用的入口類以及其直接引用類,如Instrumentation,Application,Activity,Service ,Receiver ,ContentProvider,直接引用類就是這些入口類中各個方法、變量引用、依賴的類,而這些被引用類中所引用到的其他類則被車位入口類的間接引用類,這些間接引用類是不會添加到main-dex-rule中的。
3.調用MainDexListBuilder類根據(jù)tem.jar包中的類生成主dex的文件列表,即main-dex-rule
所以需要修改target dex, 在multidex打包之前先自動生成main-dex-rule文件,然后再執(zhí)行dx操作
<target
name="dex"
depends="compile" >
<echo>dex:Converting compiled files and external libraries into ${outdir}...</echo>
//生成main-dex-rule文件
<path id="inputdir">
<pathelement location="${outdir-classes}"/>
</path>
<property name="files" refid="inputdir"/>
<condition property="realfiles" value=""${files}"" else="${files}" ><os family="windows"/>
</condition>
<exec executable="${mainDexClasses}" failonerror="true" >
<arg value="--disable-annotation-resolution-workaround"/>
<arg line="--output ${main-dex-rule}"/>
<arg value="${realfiles}"/>
</exec>
//dex操作
<apply
executable="${dx}"
failonerror="true"
parallel="true" >
<arg value="--dex" />
<!-- 多dex命令-->
<arg value="--multi-dex" />
<!-- 每個包最大的方法數(shù)為10000 -->
<arg value="--set-max-idx-number=10000" />
<arg value="--main-dex-list" />
<!-- 主dex包含的class文件列表 -->
<arg value="${main-dex-rule}" />
<arg value="--minimal-main-dex" />
<arg value="--output=${outdir}" />
<arg value="${outdir}/classes" />
<fileset
dir="${external-libs}"
includes="*.jar" />
</apply>
</target>
這里有個問題需要注意一下,在執(zhí)行mainDexClasses時多添加了一個參數(shù) --disable-annotation-resolution-workaround
如果不加該參數(shù),會產(chǎn)生如下的異常,導致生成的main-dex-rule文件內容為空

通過查看MainDexListBuilder的源碼,可以發(fā)現(xiàn)main中對傳入的參數(shù)進行了驗證,如果沒有--disable-annotation-resolution-workaround,則直接報錯退出

關于--disable-annotation-resolution-workaround的官方解釋如下

加入該參數(shù)后構建正常通過,生成的main-dex-rule文件內容如下

入口類以及直接引用類已包含在內。
構建出apk文件后解壓縮,可以看到主dex中包含的類即為main-dex-rule中含有的類,

從dex中包含其他間接引用類和第三方jar包中的類

針對main-dex-list生成的規(guī)則,各大互聯(lián)網(wǎng)公司都有自己的解決方案??梢詤⒖?br> 《Android拆分與加載Dex的多種方案對比》一文
到這里,一個簡單的dex分包示例就介紹完了,雖然是簡單的例子,但是在一般較大的工程中進行分包操作也是同樣的方式和注意事項。后續(xù)也會對比gradle的分包方式。
參考文章:
《Android APK DEX分包總結》
《美團Android DEX自動拆包及動態(tài)加載簡介》
《MultiDex》
附demo下載
MultiDexTest
