[TOC]
錯誤表現(xiàn)
- app 無法打包锋叨,日志為
com.android.dex.DexException:Too many classes in --main-dex-list, main dex capacity exceeded
錯誤原因
生成的第一個classes.dex中方法數(shù)操過65535
也就是 Short.MAX_VALUE
在Android生成APK工具鏈的 dx 源碼中有
dalvik/dx/src/com/android/dx/command/dexer/Main.java
if (args.mainDexListFile != null) {
// with --main-dex-list
// ...
// forced in main dex
for (int i = 0; i < fileNames.length; i++) {
// call processClass
processOne(fileNames[i], mainPassFilter);
}
if (dexOutputArrays.size() > 0) {
throw new DexException("Too many classes in " + Arguments.MAIN_DEX_LIST_OPTION
+ ", main dex capacity exceeded");
}
}
processClass
private static boolean processClass(String name, byte[] bytes) {
int numMethodIds = outputDex.getMethodIds().items().size();
int numFieldIds = outputDex.getFieldIds().items().size();
int constantPoolSize = cf.getConstantPool().size();
int maxMethodIdsInDex = numMethodIds + constantPoolSize + cf.getMethods().size() +
MAX_METHOD_ADDED_DURING_DEX_CREATION;
int maxFieldIdsInDex = numFieldIds + constantPoolSize + cf.getFields().size() +
MAX_FIELD_ADDED_DURING_DEX_CREATION;
if (args.multiDex
&& (outputDex.getClassDefs().items().size() > 0)
&& ((maxMethodIdsInDex > args.maxNumberOfIdxPerDex) ||
(maxFieldIdsInDex > args.maxNumberOfIdxPerDex))) {
DexFile completeDex = outputDex;
createDexFile();
}
createDexFile創(chuàng)建一個新的Byte[]對象放入dexOutputArrays
processAllFiles遇到dexOutputArrays.size > 0就會拋DexException
分包過程解析
分包的原因
Android系統(tǒng)安裝運行應(yīng)用的時候触菜,有一步是對 dex 進行運行優(yōu)化仆救,增加運行效率
優(yōu)化過程中,有過處理匯編文件加載的優(yōu)化叫dexOpt
dexOpt的執(zhí)行過程
在第一次加載Dex文件的時候執(zhí)行的挑胸,這個過程會生成一個 odex
文件,即Optimised dex
odex的用途是分離程序資源和可執(zhí)行文件、以及做預(yù)編譯處理
執(zhí)行 odex處理過的
的效率會比直接執(zhí)行 dex純粹jar包
文件的效率要高很多
dexOpt有一個設(shè)計玩荠,會把每一個類的方法id檢索起來,存在一個鏈表結(jié)構(gòu)里面
這個鏈表的長度是用一個short類型來保存的贼邓,導(dǎo)致了方法id的數(shù)目不能夠超過65536(Short.MAX_VALUE)
- Dalvik 模式下阶冈,dexOpt 肯定開啟
- ART 模式下,雖然不是JIT塑径,仍然復(fù)用了這個優(yōu)化策略
解決方法數(shù)超限的問題,需要將該dex文件拆成兩個或多個
google官方文檔 https://developer.android.com/tools/building/multidex.html#about
分包過程
- 找出必須優(yōu)先加載的類在主包
- 其余類按引用順序堂飞,隨機到其余包
- 打包構(gòu)建出APP發(fā)布
- 運行分包的APP在 Applicaion 實例化之后绑咱,會檢查系統(tǒng)版本是否支持 multidex
- 運行加載主包 Dalvik 模式將子包拷貝到應(yīng)用的沙盒目錄 ART 則是運行主包绰筛,混合子包生成 oat 文件
- Dalvik 運行時,先運行編譯運行主包,然后通過反射將子dex注入到當(dāng)前的 classloader 中
- ART 模式加載 包含多個dex的 oat 文件描融,然后解釋運行主包,一樣通過反射將子dex注入到當(dāng)前的 classloader 中
Android運行時ART加載OAT文件的過程分析
http://blog.csdn.net/luoshengyang/article/details/39307813
5.0 系統(tǒng)前后分包支持
- Android5.0之前骏庸,使用
Dalvik
方式運行毛甲,先加載主分包,然后反射加載其余的包 - Android5.0之后具被,使用
ART
方式運行玻募,ART預(yù)編譯時七咧,掃描主分包和子包叮叹,生成.oat
文件用于用戶運行
分包方案的隱患
-
API14 之前的不能支持分包
Dalvik linearalloc bug - 復(fù)雜的依賴的工程,分包后蛉顽,不同依賴項目間的dex文件函數(shù)相互調(diào)用,
報錯找不到方法
- 帶有混淆的工程悼粮,非常容易出現(xiàn)依賴沾粘(不同依賴項目間的dex文件同一個類定義樹)噪叙,安裝時報告
類定義安全檢查異常
- 開發(fā)過程有分包,導(dǎo)致
每一次的模塊構(gòu)建過程都是相當(dāng)耗時
苞笨,開發(fā)效率低下 - 分包文件過大子眶,安裝分割dex文件在某些設(shè)備上表現(xiàn)很差,
非常容易導(dǎo)致ANR
(此問題 5.0 以后的設(shè)備表現(xiàn)為安裝不成功) -
分包數(shù)量過多引起安裝失敗
粤咪,分到第6個以后容易出現(xiàn)渴杆,原理同上面一點 - 應(yīng)用程序使用 multiedex 就大量使用反射磁奖,會造成使用比較大的內(nèi)存囊拜,
OOM 將會非常容易出現(xiàn)
- 工程過大冠跷,且依賴管理混亂時,主分包因為必須加載蜜托,方法數(shù)還是超過了65536橄务,導(dǎo)致
主分包無法生成
解決multiedex隱患思路
- 刪除無意義代碼,無意義資源
- 刪除重復(fù)代碼輪子
- 拆分重型模塊柑司,依賴越多锅劝,越成樹狀越易維護
- 降低工程依賴圈復(fù)雜度(不要循環(huán)依賴蟆湖,用反射解耦)
- 分離開發(fā)和發(fā)布構(gòu)建,盡量讓模塊功能最小化诬垂,減少模塊本身分包可能性
- 活用混淆伦仍,降低dex文件大小(混淆實際上起不到安全作用隧枫,Dalvik 匯編很容易理解掌握破解混淆)
- 使用二進制優(yōu)化工具谓苟,減小dex的大小
- 業(yè)務(wù)過于多,做多個 APP 通訊的方式卑笨,或者拆分子 app 動態(tài)加載
- 優(yōu)化主分包依賴
主分包詳解
主分包在Android編譯仑撞,發(fā)布隧哮,運行時地位很高,而主分包的生成是靠分析出的 maindexlist.txt
來生成的
maindexlist.txt 創(chuàng)建分析
android gradle plugin
有一個類專門負(fù)責(zé)創(chuàng)建maindexlist.txt艺普,叫做CreateMainDexList
源碼
tools/base/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/multidex/CreateMainDexList.groovy
@TaskAction
void output() {
if (getAllClassesJarFile() == null) {
throw new NullPointerException("No input file")
}
// manifest components plus immediate dependencies must be in the main dex.
File _allClassesJarFile = getAllClassesJarFile()
Set<String> mainDexClasses = callDx(_allClassesJarFile, getComponentsJarFile())
...
}
callDx
最終調(diào)用AndroidBuilder.createMainDexList
實際是通過開啟后臺進程執(zhí)行ClassReferenceListBuilder.main
分析類的依賴關(guān)系歧譬,生成一個maindexlist.txt
public void addRoots(ZipFile jarOfRoots) throws IOException {
...
for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();
entries.hasMoreElements();) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.endsWith(CLASS_EXTENSION)) {
DirectClassFile classFile;
...
classFile = path.getClass(name);
...
addDependencies(classFile.getConstantPool());
}
}
}
ClassReferenceListBuilder.addRoots
通過讀文件遍歷componentClasses.jar
的每個entry
再調(diào)用addDependencies
分析這個類的依賴關(guān)系
private void addDependencies(ConstantPool pool) {
for (Constant constant : pool.getEntries()) {
if (constant instanceof CstType) {
Type type = ((CstType) constant).getClassType();
String descriptor = type.getDescriptor();
if (descriptor.endsWith(";")) {
int lastBrace = descriptor.lastIndexOf('[');
if (lastBrace < 0) {
addClassWithHierachy(descriptor.substring(1, descriptor.length()-1));
} else {
assert descriptor.length() > lastBrace + 3
&& descriptor.charAt(lastBrace + 1) == 'L';
addClassWithHierachy(descriptor.substring(lastBrace + 2,
descriptor.length() - 1));
}
}
}
}
}
addDependencies
從ConstantPool
得到import類
瑰步,調(diào)用addClassWithHierachy繼續(xù)分析繼承關(guān)系
也可以通過
javap -verbose
先反匯編,再分析匹配”= class”的字符串來獲取來調(diào)試
依賴關(guān)系分析結(jié)束后读虏,輸出maindexlist.txt
所以一句話
componentClasses.jar
最終決定了maindexlist.txt
的大小
componentClasses.jar 生成分析
這個中間生成的componentClasses.jar
最后會在打包成功后刪除
當(dāng)然出現(xiàn)方法超過的時候袁滥,這個包在 moduel/build/intermediates/multi-dex/
對應(yīng)渠道里面
gradle plugin 2.3.0 以后位置有變動,不過一樣可以找到
componentClasses.jar
生成任務(wù) proguardComponentsTask
根據(jù)manifest_keep.txt
從allclasses.jar
中抽取生成的揩徊,manifest_keep.txt
內(nèi)容一般是
-keep class com.xxx.app.XXXXApplication {
<init>();
void attachBaseContext(android.content.Context);
}
-keep class com.xxx.splash.XXXXActivity { <init>(); }
-keep class com.xxx.app.MainActivity { <init>(); }
-keep class com.xxx.login.xxxx.LoginActivity { <init>(); }
-keep class com.xxx.sidebar.account.XXAccountActivity { <init>(); }
...
不難看出manifest_keep.txt
是通過CreateManifestKeepList
解析AndroidManifest.xml文件
得到
./tools/base/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/multidex/CreateManifestKeepList.groovy
@TaskAction
void generateKeepListFromManifest() {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser()
Writer out = new BufferedWriter(new FileWriter(getOutputFile()))
try {
parser.parse(getManifest(), new ManifestHandler(out))
// add a couple of rules that cannot be easily parsed from the manifest.
out.write(
"""-keep public class * extends android.app.backup.BackupAgent {
<init>();
}
-keep public class * extends java.lang.annotation.Annotation {
*;
}
""")
if (proguardFile != null) {
out.write(Files.toString(proguardFile, Charsets.UTF_8))
}
} finally {
out.close()
}
}
...
CreateManifestKeepList
私有內(nèi)部類ManifestHandler
用CreateManifestKeepList.KEEP_SPECS[qName]
決定哪些類需要放入manifest_keep.txt
private class ManifestHandler extends DefaultHandler {
...
@Override
void startElement(String uri, String localName, String qName, Attributes attr) {
String keepSpec = CreateManifestKeepList.KEEP_SPECS[qName]
if (keepSpec) {
boolean keepIt = true
if (CreateManifestKeepList.this.filter) {
Map<String, String> attrMap = [:]
for (int i = 0; i < attr.getLength(); i++) {
attrMap[attr.getQName(i)] = attr.getValue(i)
}
keepIt = CreateManifestKeepList.this.filter(qName, attrMap)
}
if (keepIt) {
String nameValue = attr.getValue('android:name')
if (nameValue != null) {
out.write((String) "-keep class ${nameValue} $keepSpec\n")
}
過濾的KEEP_SPECS
private static String DEFAULT_KEEP_SPEC = "{ <init>(); }"
private static Map<String, String> KEEP_SPECS = [
'application' : """{
<init>();
void attachBaseContext(android.content.Context);
}""",
'activity' : DEFAULT_KEEP_SPEC,
'service' : DEFAULT_KEEP_SPEC,
'receiver' : DEFAULT_KEEP_SPEC,
'provider' : DEFAULT_KEEP_SPEC,
'instrumentation' : DEFAULT_KEEP_SPEC,
]
那么至少AndroidManifest.xml中
- application
- activity
- service
- receiver
- provider
- instrumentation
這6種標(biāo)簽的類
以及繼承
- java.lang.annotation.Annotation
- android.app.backup.BackupAgent
的類都會會用來產(chǎn)生maindexlist.txt
總結(jié),必須在主分包中的類有
- 基于apk運行的加載機制齿税,Application 中的引用肯定在在主包內(nèi)
- 開啟 MultiDex 分包 那么
android.support.multidex
包肯定必須在主包內(nèi) - 繼承
java.lang.annotation.Annotation
android.app.backup.BackupAgent
在主包 - 在
AndroidManifest.xml
注冊的四大組件炊豪,必須在第一個包 - 使用
instrumentation
測試技術(shù)實現(xiàn)的必須在第一個包內(nèi)