當(dāng)程序越來越大之后拐揭,出現(xiàn)了一個(gè) dex 包裝不下的情況堂污,通過 MultiDex 的方法解決了這個(gè)問題盟猖,但是在低端機(jī)器上又出現(xiàn)了 INSTALL_FAILED_DEXOPT 的情況式镐,那再解決這個(gè)問題吧娘汞。等解決完這個(gè)問題之后你弦,發(fā)現(xiàn)需要填的坑越來越多了禽作,文章講的是我在分包處理中填的坑彻磁,比如 65536衷蜓、LinearAlloc磁浇、NoClassDefFoundError等等置吓。
INSTALL_FAILED_DEXOPT
INSTALL_FAILED_DEXOPT 出現(xiàn)的原因大部分都是兩種衍锚,一種是 65536 了戴质,另外一種是 LinearAlloc 太小了告匠。兩者的限制不同后专,但是原因卻是相似戚哎,那就是App太大了,導(dǎo)致沒辦法安裝到手機(jī)上崭捍。
65536
trouble writing output: Too many method references: 70048; max is 65536.
或者
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
編譯環(huán)境
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
}
}
android {
compileSdkVersion 23
buildToolsVersion "25.0.3"
//....
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
//....
}
}
為什么是65536
根據(jù) StackOverFlow – Does the Android ART runtime have the same method limit limitations as Dalvik? 上面的說法,是因?yàn)?Dalvik 的 invoke-kind 指令集中橄浓,method reference index 只留了 16 bits荸实,最多能引用 65535 個(gè)方法准给。Dalvik bytecode :
Op & Format Mnemonic | Mnemonic / Syntax | Arguments |
---|---|---|
6e..72 | invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB6e: | A: argument word count (4 bits)B: |
35c | invoke-virtual6f: invoke-super70: invoke-direct71: invoke-static72: invoke-interface | method reference index (16 bits)C..G: argument registers (4 bits each) |
- 即使 dex 里面的引用方法數(shù)超過了 65536露氮,那也只有前面的 65536 得的到調(diào)用。所以這個(gè)不是 dex 的原因恨统。其次畜埋,既然和 dex 沒有關(guān)系悠鞍,那在打包 dex 的時(shí)候?yàn)槭裁磿?huì)報(bào)錯(cuò)狞玛。我們先定位 Too many 關(guān)鍵字心肪,定位到了 MemberIdsSection :
public abstract class MemberIdsSection extends UniformItemSection {
/** {@inheritDoc} */
@Override
protected void orderItems() {
int idx = 0;
if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
throw new DexIndexOverflowException(getTooManyMembersMessage());
}
for (Object i : items()) {
((MemberIdItem) i).setIndex(idx);
idx++;
}
}
private String getTooManyMembersMessage() {
Map<String, AtomicInteger> membersByPackage = new TreeMap<String, AtomicInteger>();
for (Object member : items()) {
String packageName = ((MemberIdItem) member).getDefiningClass().getPackageName();
AtomicInteger count = membersByPackage.get(packageName);
if (count == null) {
count = new AtomicInteger();
membersByPackage.put(packageName, count);
}
count.incrementAndGet();
}
Formatter formatter = new Formatter();
try {
String memberType = this instanceof MethodIdsSection ? "method" : "field";
formatter.format("Too many %s references: %d; max is %d.%n" +
Main.getTooManyIdsErrorMessage() + "%n" +
"References by package:",
memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
for (Map.Entry<String, AtomicInteger> entry : membersByPackage.entrySet()) {
formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
}
return formatter.toString();
} finally {
formatter.close();
}
}
}
items().size() > DexFormat.MAX_MEMBER_IDX + 1 慧瘤,那 DexFormat 的值是:
public final class DexFormat {
/**
* Maximum addressable field or method index.
* The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or
* meth@CCCC.
*/
public static final int MAX_MEMBER_IDX = 0xFFFF;
}
dx 在這里做了判斷锅减,當(dāng)大于 65536 的時(shí)候就拋出異常了怔匣。所以在生成 dex 文件的過程中每瞒,當(dāng)調(diào)用方法數(shù)不能超過 65535 剿骨。那我們?cè)俑桓a浓利,發(fā)現(xiàn) MemberIdsSection 的一個(gè)子類叫 MethodidsSection :
public final class MethodIdsSection extends MemberIdsSection {}
回過頭來嫡秕,看一下 orderItems() 方法在哪里被調(diào)用了淘菩,跟到了 MemberIdsSection 的父類 UniformItemSection :
public abstract class UniformItemSection extends Section {
@Override
protected final void prepare0() {
DexFile file = getFile();
orderItems();
for (Item one : items()) {
one.addContents(file);
}
}
protected abstract void orderItems();
}
再跟一下 prepare0 在哪里被調(diào)用潮改,查到了 UniformItemSection 父類 Section :
public abstract class Section {
public final void prepare() {
throwIfPrepared();
prepare0();
prepared = true;
}
protected abstract void prepare0();
}
那現(xiàn)在再跟一下 prepare() 汇在,查到 DexFile 中有調(diào)用:
public final class DexFile {
private ByteArrayAnnotatedOutput toDex0(boolean annotate, boolean verbose) {
classDefs.prepare();
classData.prepare();
wordData.prepare();
byteData.prepare();
methodIds.prepare();
fieldIds.prepare();
protoIds.prepare();
typeLists.prepare();
typeIds.prepare();
stringIds.prepare();
stringData.prepare();
header.prepare();
//blablabla......
}
}
那再看一下 toDex0() 吧糕殉,因?yàn)槭?private 的阿蝶,直接在類中找調(diào)用的地方就可以了:
public final class DexFile {
public byte[] toDex(Writer humanOut, boolean verbose) throws IOException {
boolean annotate = (humanOut != null);
ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);
if (annotate) {
result.writeAnnotationsTo(humanOut);
}
return result.getArray();
}
public void writeTo(OutputStream out, Writer humanOut, boolean verbose) throws IOException {
boolean annotate = (humanOut != null);
ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);
if (out != null) {
out.write(result.getArray());
}
if (annotate) {
result.writeAnnotationsTo(humanOut);
}
}
}
先搜搜 toDex() 方法吧羡洁,最終發(fā)現(xiàn)在 com.android.dx.command.dexer.Main 中:
public class Main {
private static byte[] writeDex(DexFile outputDex) {
byte[] outArray = null;
//blablabla......
if (args.methodToDump != null) {
outputDex.toDex(null, false);
dumpMethod(outputDex, args.methodToDump, humanOutWriter);
} else {
outArray = outputDex.toDex(humanOutWriter, args.verboseDump);
}
//blablabla......
return outArray;
}
//調(diào)用writeDex的地方
private static int runMonoDex() throws IOException {
//blablabla......
outArray = writeDex(outputDex);
//blablabla......
}
//調(diào)用runMonoDex的地方
public static int run(Arguments arguments) throws IOException {
if (args.multiDex) {
return runMultiDex();
} else {
return runMonoDex();
}
}
}
args.multiDex 就是是否分包的參數(shù)辛蚊,那么問題找著了袋马,如果不選擇分包的情況下虑凛,引用方法數(shù)超過了 65536 的話就會(huì)拋出異常卧檐。
同樣分析第二種情況焰宣,根據(jù)錯(cuò)誤信息可以具體定位到代碼匕积,但是很奇怪的是 DexMerger 闪唆,我們沒有設(shè)置分包參數(shù)或者其他參數(shù)悄蕾,為什么會(huì)有 DexMerger 帆调,而且依賴工程最終不都是 aar 格式的嗎番刊?那我們還是來跟一跟代碼吧芹务。
public class Main {
private static byte[] mergeLibraryDexBuffers(byte[] outArray) throws IOException {
ArrayList<Dex> dexes = new ArrayList<Dex>();
if (outArray != null) {
dexes.add(new Dex(outArray));
}
for (byte[] libraryDex : libraryDexBuffers) {
dexes.add(new Dex(libraryDex));
}
if (dexes.isEmpty()) {
return null;
}
Dex merged = new DexMerger(dexes.toArray(new Dex[dexes.size()]), CollisionPolicy.FAIL).merge();
return merged.getBytes();
}
}
這里可以看到變量 libraryDexBuffers 枣抱,是一個(gè) List 集合佳晶,那么我們看一下這個(gè)集合在哪里添加數(shù)據(jù)的:
public class Main {
private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME);
//blablabla...
} else if (isClassesDex) {
synchronized (libraryDexBuffers) {
libraryDexBuffers.add(bytes);
}
return true;
} else {
//blablabla...
}
//調(diào)用processFileBytes的地方
private static class FileBytesConsumer implements ClassPathOpener.Consumer {
@Override
public boolean processFileBytes(String name, long lastModified,
byte[] bytes) {
return Main.processFileBytes(name, lastModified, bytes);
}
//blablabla...
}
//調(diào)用FileBytesConsumer的地方
private static void processOne(String pathname, FileNameFilter filter) {
ClassPathOpener opener;
opener = new ClassPathOpener(pathname, true, filter, new FileBytesConsumer());
if (opener.process()) {
updateStatus(true);
}
}
//調(diào)用processOne的地方
private static boolean processAllFiles() {
//blablabla...
// forced in main dex
for (int i = 0; i < fileNames.length; i++) {
processOne(fileNames[i], mainPassFilter);
}
//blablabla...
}
//調(diào)用processAllFiles的地方
private static int runMonoDex() throws IOException {
//blablabla...
if (!processAllFiles()) {
return 1;
}
//blablabla...
}
}
跟了一圈又跟回來了垂攘,但是注意一個(gè)變量:fileNames[i]晒他,傳進(jìn)去這個(gè)變量陨仅,是個(gè)地址灼伤,最終在 processFileBytes 中處理后添加到 libraryDexBuffers 中狐赡,那跟一下這個(gè)變量:
public class Main {
private static boolean processAllFiles() {
//blablabla...
String[] fileNames = args.fileNames;
//blablabla...
}
public void parse(String[] args) {
//blablabla...
}else if(parser.isArg(INPUT_LIST_OPTION + "=")) {
File inputListFile = new File(parser.getLastValue());
try{
inputList = new ArrayList<String>();
readPathsFromFile(inputListFile.getAbsolutePath(), inputList);
} catch(IOException e) {
System.err.println("Unable to read input list file: " + inputListFile.getName());
throw new UsageException();
}
} else {
//blablabla...
fileNames = parser.getRemaining();
if(inputList != null && !inputList.isEmpty()) {
inputList.addAll(Arrays.asList(fileNames));
fileNames = inputList.toArray(new String[inputList.size()]);
}
}
public static void main(String[] argArray) throws IOException {
Arguments arguments = new Arguments();
arguments.parse(argArray);
int result = run(arguments);
if (result != 0) {
System.exit(result);
}
}
}
跟到這里發(fā)現(xiàn)是傳進(jìn)來的參數(shù),那我們?cè)倏纯?gradle 里面?zhèn)鞯氖鞘裁磪?shù)吧享郊,查看 Dex task :
public class Dex extends BaseTask {
@InputFiles
Collection<File> libraries
}
我們把這個(gè)參數(shù)打印出來:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
println dx.libraries
}
}
打印出來發(fā)現(xiàn)是 build/intermediates/pre-dexed/ 目錄里面的 jar 文件炊琉,再把 jar 文件解壓發(fā)現(xiàn)里面就是 dex 文件了苔咪。所以 DexMerger 的工作就是合并這里的 dex 悼泌。
更改編譯環(huán)境
buildscript {
//...
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-alpha3'
}
}
將 gradle 設(shè)置為 2.1.0-alpha3 之后馆里,在項(xiàng)目的 build.gradle 中即使沒有設(shè)置 multiDexEnabled true 也能夠編譯通過鸠踪,但是生成的 apk 包依舊是兩個(gè) dex 营密,我想的是可能為了設(shè)置 instantRun 。
解決 65536
Google MultiDex 解決方案:
在 gradle 中添加 MultiDex 的依賴:
dependencies { compile 'com.android.support:MultiDex:1.0.0' }
在 gradle 中配置 MultiDexEnable :
android {
buildToolsVersion "21.1.0"
defaultConfig {
// Enabling MultiDex support.
MultiDexEnabled true
}
}
在 AndroidManifest.xml 的 application 中聲明:
<application
android:name="android.support.multidex.MultiDexApplication">
<application/>
如果有自己的 Application 了痢虹,讓其繼承于 MultiDexApplication 奖唯。
如果繼承了其他的 Application 丰捷,那么可以重寫 attachBaseContext(Context):
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
LinearAlloc
gradle:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--set-max-idx-number=48000'
}
}
--set-max-idx-number= 用于控制每一個(gè) dex 的最大方法個(gè)數(shù)病往。
這個(gè)參數(shù)在查看 dx.jar 找到:
//blablabla...
} else if (parser.isArg("--set-max-idx-number=")) { // undocumented test option
maxNumberOfIdxPerDex = Integer.parseInt(parser.getLastValue());
} else if(parser.isArg(INPUT_LIST_OPTION + "=")) {
//blablabla...
更多細(xì)節(jié)可以查看源碼:Github – platform_dalvik/Main
FB 的工程師們?cè)?jīng)還想到過直接修改 LinearAlloc 的大小停巷,比如從 5M 修改到 8M: Under the Hood: Dalvik patch for Facebook for Android 叠穆。
dexopt && dex2oat
dexopt
當(dāng) Android 系統(tǒng)安裝一個(gè)應(yīng)用的時(shí)候,有一步是對(duì) Dex 進(jìn)行優(yōu)化示损,這個(gè)過程有一個(gè)專門的工具來處理检访,叫 DexOpt脆贵。DexOpt 是在第一次加載 Dex 文件的時(shí)候執(zhí)行的卖氨,將 dex 的依賴庫文件和一些輔助數(shù)據(jù)打包成 odex 文件筒捺,即 Optimised Dex系吭,存放在 cache/dalvik_cache 目錄下。保存格式為 apk路徑 @ apk名 @ classes.dex 躯枢。執(zhí)行 ODEX 的效率會(huì)比直接執(zhí)行 Dex 文件的效率要高很多闺金。
dex2oat
Android Runtime 的 dex2oat 是將 dex 文件編譯成 oat 文件败匹。而 oat 文件是 elf 文件掀亩,是可以在本地執(zhí)行的文件槽棍,而 Android Runtime 替換掉了虛擬機(jī)讀取的字節(jié)碼轉(zhuǎn)而用本地可執(zhí)行代碼炼七,這就被叫做 AOT(ahead-of-time)豌拙。dex2oat 對(duì)所有 apk 進(jìn)行編譯并保存在 dalvik-cache 目錄里按傅。PackageManagerService 會(huì)持續(xù)掃描安裝目錄唯绍,如果有新的 App 安裝則馬上調(diào)用 dex2oat 進(jìn)行編譯枝誊。
NoClassDefFoundError
現(xiàn)在 INSTALL_FAILED_DEXOPT 問題是解決了叶撒,但是有時(shí)候編譯完運(yùn)行的時(shí)候一打開 App 就 crash 了痊乾,查看 log 發(fā)現(xiàn)是某個(gè)類找不到引用哪审。
- Build Tool 是如何分包的
為什么會(huì)這樣呢?是因?yàn)?build-tool 在分包的時(shí)候只判斷了直接引用類舌狗。什么是直接引用類呢痛侍?舉個(gè)栗子:
public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
DirectReferenceClass test = new DirectReferenceClass();
}
}
public class DirectReferenceClass {
public DirectReferenceClass() {
InDirectReferenceClass test = new InDirectReferenceClass();
}
}
public class InDirectReferenceClass {
public InDirectReferenceClass() {
}
}
上面有 MainActivity主届、DirectReferenceClass 君丁、InDirectReferenceClass 三個(gè)類绘闷,其中 DirectReferenceClass 是 MainActivity 的直接引用類印蔗,InDirectReferenceClass 是 DirectReferenceClass 的直接引用類华嘹。而 InDirectReferenceClass 是 MainActivity 的間接引用類(即直接引用類的所有直接引用類)除呵。
如果我們代碼是這樣寫的:
public class HelloMultiDexApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
DirectReferenceClass test = new DirectReferenceClass();
MultiDex.install(this);
}
}
這樣直接就 crash 了。同理還要單例模式中拿到單例之后直接調(diào)用某個(gè)方法返回的是另外一個(gè)對(duì)象秉剑,并非單例對(duì)象侦鹏。
build tool 的分包操作可以查看 sdk 中 build-tools 文件夾下的 mainDexClasses 腳本略水,同時(shí)還發(fā)現(xiàn)了 mainDexClasses.rules 文件渊涝,該文件是主 dex 的匹配規(guī)則跨释。該腳本要求輸入一個(gè)文件組(包含編譯后的目錄或jar包)鳖谈,然后分析文件組中的類并寫入到–output所指定的文件中缆娃。實(shí)現(xiàn)原理也不復(fù)雜,主要分為三步:
- 環(huán)境檢查疙驾,包括傳入?yún)?shù)合法性檢查它碎,路徑檢查以及proguard環(huán)境檢測(cè)等扳肛。
- 使用mainDexClasses.rules規(guī)則挖息,通過Proguard的shrink功能套腹,裁剪無關(guān)類电禀,生成一個(gè)tmp.jar包尖飞。
- 通過生成的tmp jar包政基,調(diào)用MainDexListBuilder類生成主dex的文件列表
Gradle 打包流程中是如何分包的
在項(xiàng)目中沮明,可以直接運(yùn)行 gradle 的 task 荐健。
collect{flavor}{buildType}MultiDexComponents Task 。這個(gè) task 是獲取 AndroidManifest.xml 中 Application 圣贸、Activity 吁峻、Service 用含、 Receiver 啄骇、 Provider 等相關(guān)類缸夹,以及 Annotation 虽惭,之后將內(nèi)容寫到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 文件中去芽唇。
packageAll{flavor}DebugClassesForMultiDex Task 匆笤。該 task 是將所有類打包成 jar 文件存在 build/intermediates/multi-dex/{flavor}/debug/allclasses.jar 炮捧。 當(dāng) BuildType 為 Release 的時(shí)候寓盗,執(zhí)行的是 proguard{flavor}Release Task,該 task 將 proguard 混淆后的類打包成 jar 文件存在 build/intermediates/classes-proguard/{flavor}/release/classes.jar
shrink{flavor}{buildType}MultiDexComponents Task 蘸吓。該 task 會(huì)根據(jù) maindexlist.txt 生成 componentClasses.jar 库继,該 jar 包里面就只有 maindexlist.txt 里面的類,該 jar 包的位置在 build/intermediates/multi-dex/{flavor}/{buildType}/componentClasses.jar
create{flavor}{buildType}MainDexClassList Task 艺谆。該 task 會(huì)根據(jù)生成的 componentClasses.jar 去找這里面的所有的 class 中直接依賴的 class 静汤,然后將內(nèi)容寫到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 中虫给。最終這個(gè)文件里面列出來的類都會(huì)被分配到第一個(gè) dex 里面抹估。
解決 NoClassDefFoundError
gradle :
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--set-max-idx-number=48000'
dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
}
}
--main-dex-list= 參數(shù)是一個(gè)類列表的文件药蜻,在該文件中的類會(huì)被打包在第一個(gè) dex 中语泽。
multidex.keep 里面列上需要打包到第一個(gè) dex 的 class 文件湿弦,注意颊埃,如果需要混淆的話需要寫混淆之后的 class 班利。
Application Not Responding
因?yàn)榈谝淮芜\(yùn)行(包括清除數(shù)據(jù)之后)的時(shí)候需要 dexopt 罗标,然而 dexopt 是一個(gè)比較耗時(shí)的操作闯割,同時(shí) MultiDex.install() 操作是在 Application.attachBaseContext() 中進(jìn)行的宙拉,占用的是UI線程谢澈。那么問題來了,當(dāng)我的第二個(gè)包牛郑、第三個(gè)包很大的時(shí)候淹朋,程序就阻塞在 MultiDex.install() 這個(gè)地方了瑞你,一旦超過規(guī)定時(shí)間者甲,那就 ANR 了虏缸。那怎么辦刽辙?放子線程宰缤?如果 Application 有一些初始化操作慨灭,到初始化操作的地方的時(shí)候都還沒有完成 install + dexopt 的話氧骤,那不是又 NoClassDefFoundError 了嗎筹陵?同時(shí) ClassLoader 放在哪個(gè)線程都讓主線程掛起镊尺。好了庐氮,那在 multidex.keep 的加上相關(guān)的所有的類吧旭愧。好像這樣成了输枯,但是第一個(gè) dex 又大起來了桃熄,而且如果用戶操作快瞳收,還沒完成 install + dexopt 但是已經(jīng)把 App 所以界面都打開了一遍螟深。界弧。垢箕。雖然這不現(xiàn)實(shí)条获。帅掘。
微信加載方案
首次加載在地球中頁中, 并用線程去加載(但是 5.0 之前加載 dex 時(shí)還是會(huì)掛起主線程一段時(shí)間(不是全程都掛起))锄开。
dex 形式
微信是將包放在 assets 目錄下的萍悴,在加載 Dex 的代碼時(shí)癣诱,實(shí)際上傳進(jìn)去的是 zip撕予,在加載前需要驗(yàn)證 MD5实抡,確保所加載的 Dex 沒有被篡改。dex 類分包規(guī)則
分包規(guī)則即將所有 Application赏淌、ContentProvider 以及所有 export 的 Activity六水、Service 掷贾、Receiver 的間接依賴集都必須放在主 dex想帅。加載 dex 的方式
加載邏輯這邊主要判斷是否已經(jīng) dexopt博脑,若已經(jīng) dexopt叉趣,即放在 attachBaseContext 加載疗杉,反之放于地球中用線程加載烟具。怎么判斷朝聋?因?yàn)樵谖⑿胖屑胶郏襞袛?revision 改變,即將 dex 以及 dexopt 目錄清空僻他。只需簡單判斷兩個(gè)目錄 dex 名稱吨拗、數(shù)量是否與配置文件的一致哨鸭。
總的來說兔跌,這種方案用戶體驗(yàn)較好,缺點(diǎn)在于太過復(fù)雜华望,每次都需重新掃描依賴集赖舟,而且使用的是比較大的間接依賴集宾抓。
Facebook 加載方案
Facebook的思路是將 MultiDex.install() 操作放在另外一個(gè)經(jīng)常進(jìn)行的幢泼。
-
dex 形式
與微信相同讲衫。
-
dex 類分包規(guī)則
Facebook 將加載 dex 的邏輯單獨(dú)放于一個(gè)單獨(dú)的 nodex 進(jìn)程中招驴。
<activity
android:exported="false"
android:process=":nodex"android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
所有的依賴集為 Application别厘、NodexSplashActivity 的間接依賴集即可触趴。
-
加載 dex 的方式
因?yàn)?NodexSplashActivity 的 intent-filter 指定為 Main 和LAUNCHER 雕蔽,所以一打開 App 首先拉起 nodex 進(jìn)程批狐,然后打開 NodexSplashActivity 進(jìn)行 MultiDex.install() 嚣艇。如果已經(jīng)進(jìn)行了 dexpot 操作的話就直接跳轉(zhuǎn)主界面食零,沒有的話就等待 dexpot 操作完成再跳轉(zhuǎn)主界面。
這種方式好處在于依賴集非常簡單娜搂,同時(shí)首次加載 dex 時(shí)也不會(huì)卡死百宇。但是它的缺點(diǎn)也很明顯携御,即每次啟動(dòng)主進(jìn)程時(shí)啄刹,都需先啟動(dòng) nodex 進(jìn)程誓军。盡管 nodex 進(jìn)程邏輯非常簡單谭企,這也需100ms以上债查。
美團(tuán)加載方案
- dex 形式
在 gradle 生成 dex 文件的這步中盹廷,自定義一個(gè) task 來干預(yù) dex 的生產(chǎn)過程俄占,從而產(chǎn)生多個(gè) dex 缸榄。
tasks.whenTaskAdded { task ->
if (task.name.startsWith('proguard') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doLast {
makeDexFileAfterProguardJar();
}
task.doFirst {
delete "${project.buildDir}/intermediates/classes-proguard";
String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
generateMainIndexKeepList(flavor.toLowerCase());
}
} else if (task.name.startsWith('zipalign') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doFirst {
ensureMultiDexInApk();
}
}
}
- dex 類分包規(guī)則
把 Service甚带、Receiver鹰贵、Provider 涉及到的代碼都放到主 dex 中碉输,而把 Activity 涉及到的代碼進(jìn)行了一定的拆分敷钾,把首頁 Activity闰非、Laucher Activity 财松、歡迎頁的 Activity 辆毡、城市列表頁 Activity 等所依賴的 class 放到了主 dex 中舶掖,把二級(jí)眨攘、三級(jí)頁面的 Activity 以及業(yè)務(wù)頻道的代碼放到了第二個(gè) dex 中鲫售,為了減少人工分析 class 的依賴所帶了的不可維護(hù)性和高風(fēng)險(xiǎn)性情竹,美團(tuán)編寫了一個(gè)能夠自動(dòng)分析 class 依賴的腳本秦效, 從而能夠保證主 dex 包含 class 以及他們所依賴的所有 class 都在其內(nèi)阱州,這樣這個(gè)腳本就會(huì)在打包之前自動(dòng)分析出啟動(dòng)到主 dex 所涉及的所有代碼苔货,保證主 dex 運(yùn)行正常阱冶。 - 加載 dex 的方式
通過分析 Activity 的啟動(dòng)過程木蹬,發(fā)現(xiàn) Activity 是由 ActivityThread 通過 Instrumentation 來啟動(dòng)的镊叁,那么是否可以在 Instrumentation 中做一定的手腳呢晦譬?通過分析代碼 ActivityThread 和 Instrumentation 發(fā)現(xiàn)敛腌,Instrumentation 有關(guān) Activity 啟動(dòng)相關(guān)的方法大概有:execStartActivity像樊、 newActivity 等等生棍,這樣就可以在這些方法中添加代碼邏輯進(jìn)行判斷這個(gè) class 是否加載了涂滴,如果加載則直接啟動(dòng)這個(gè) Activity柔纵,如果沒有加載完成則啟動(dòng)一個(gè)等待的 Activity 顯示給用戶首量,然后在這個(gè) Activity 中等待后臺(tái)第二個(gè) dex 加載完成加缘,完成后自動(dòng)跳轉(zhuǎn)到用戶實(shí)際要跳轉(zhuǎn)的 Activity拣宏;這樣在代碼充分解耦合勋乾,以及每個(gè)業(yè)務(wù)代碼能夠做到顆粒化的前提下学歧,就做到第二個(gè) dex 的按需加載了枝笨。
美團(tuán)的這種方式對(duì)主 dex 的要求非常高横浑,因?yàn)榈诙€(gè) dex 是等到需要的時(shí)候再去加載徙融。重寫Instrumentation 的 execStartActivity 方法欺冀,hook 跳轉(zhuǎn) Activity 的總?cè)肟谧雠袛嘟呕绻?dāng)前第二個(gè) dex 還沒有加載完成,就彈一個(gè) loading Activity等待加載完成蛛芥。
綜合加載方案
微信的方案需要將 dex 放于 assets 目錄下仅淑,在打包的時(shí)候太過負(fù)責(zé)涯竟;Facebook 的方案每次進(jìn)入都是開啟一個(gè) nodex 進(jìn)程庐船,而我們希望節(jié)省資源的同時(shí)快速打開 App筐钟;美團(tuán)的方案確實(shí)很 hack篓冲,但是對(duì)于項(xiàng)目已經(jīng)很龐大壹将,耦合度又比較高的情況下并不適合诽俯。所以這里嘗試結(jié)合三個(gè)方案惊畏,針對(duì)自己的項(xiàng)目來進(jìn)行優(yōu)化颜启。
dex 形式
第一缰盏,為了能夠繼續(xù)支持 Android 2.x 的機(jī)型口猜,我們將每個(gè)包的方法數(shù)控制在 48000 個(gè),這樣最后分出來 dex 包大約在 5M 左右川抡;第二崖堤,為了防止 NoClassDefFoundError 的情況密幔,我們找出來啟動(dòng)頁胯甩、引導(dǎo)頁、首頁比較在意的一些類缰雇,比如 Fragment 等(因?yàn)樵谏?maindexlist.txt 的時(shí)候只會(huì)找 Activity 的直接引用,比如首頁 Activity 直接引用 AFragemnt愚战,但是 AFragment 的引用并沒有去找)。dex 類分包規(guī)則
第一個(gè)包放 Application梗摇、Android四大組件以及啟動(dòng)頁伶授、引導(dǎo)頁糜烹、首頁的直接引用的 Fragment 的引用類疮蹦,還放了推送消息過來點(diǎn)擊 Notification 之后要展示的 Activity 中的 Fragment 的引用類愕乎。
Fragment 的引用類是寫了一個(gè)腳本感论,輸入需要找的類然后將這些引用類寫到 multidex.keep 文件中紊册,如果是 debug 的就直接在生成的 jar 里面找囊陡,如果是 release 的話就通過 mapping.txt 找,找不到的話再去 jar 里面找,所以在 gradle 打包的過程中我們?nèi)藶楦蓴_一下:
tasks.whenTaskAdded { task ->
if (task.name.startsWith("create") && task.name.endsWith("MainDexClassList")) {
task.doLast {
def flavorAndBuildType = task.name.substring("create".length(), task.name.length() - "MainDexClassList".length())
autoSplitDex.configure {
description = flavorAndBuildType
}
autoSplitDex.execute()
}
}
}
詳細(xì)代碼可見:Github — PhotoNoter/gradle
加載 dex 的方式
在防止 ANR 方面痢畜,我們采用了 Facebook 的思路丁稀。但是稍微有一點(diǎn)區(qū)別线衫,差別在于我們并不在一開啟 App 的時(shí)候就去起進(jìn)程授账,而是一開啟 App 的時(shí)候在主進(jìn)程里面判斷是否 dexopt 過沒,沒有的話再去起另外的進(jìn)程的 Activity 專門做 dexopt 操作 敛助。一旦拉起了去做 dexopt 的進(jìn)程纳击,那么讓主進(jìn)程進(jìn)入一個(gè)死循環(huán)焕数,一直等到 dexopt 進(jìn)程結(jié)束再結(jié)束死循環(huán)往下走刨啸。那么問題來了加匈,第一雕拼,主進(jìn)程進(jìn)入死循環(huán)會(huì) ANR 嗎啥寇?第二辑甜,如何判斷是否 dexopt 過磷醋;第三邓线,為了界面友好骇陈,dexopt 的進(jìn)程該怎么做你雌;第四婿崭,主進(jìn)程怎么知道 dexopt 進(jìn)程結(jié)束了逛球,也就是怎么去做進(jìn)程間通信颤绕。一個(gè)一個(gè)問題的解決奥务,先第一個(gè):因?yàn)楫?dāng)拉起 dexopt 進(jìn)程之后挡篓,我們?cè)?dexopt 進(jìn)程的 Activity 中進(jìn)行 MultiDex.install() 操作帚称,此時(shí)主進(jìn)程不再是前臺(tái)進(jìn)程了官研,所以不會(huì) ANR 。
第二個(gè)問題:因?yàn)榈谝淮螁?dòng)是什么數(shù)據(jù)都沒有的闯睹,那么我們就建立一個(gè) SharedPreference 戏羽,啟動(dòng)的時(shí)候先去從這里獲取數(shù)據(jù),如果沒有數(shù)據(jù)那么也就是沒有 dexopt 過楼吃,如果有數(shù)據(jù)那么肯定是 dexopt 過的始花,但是這個(gè) SharedPreference 我們得保證我們的程序只有這個(gè)地方可以修改,其他地方不能修改孩锡。
第三個(gè)問題:因?yàn)?App 的啟動(dòng)也是一張圖片浇垦,所以在 dexopt 的 Activity 的 layout 中,我們就把這張圖片設(shè)置上去就好了仍劈,當(dāng)關(guān)閉 dexopt 的 Activity 的時(shí)候讹弯,我們得關(guān)閉 Activity 的動(dòng)畫。同時(shí)為了不讓 dexopt 進(jìn)程發(fā)生 ANR 莫其,我們將 MultiDex.install() 過程放在了子線程中進(jìn)行。
第四個(gè)問題:Linux 的進(jìn)程間通信的方式有很多,Android 中還有 Binder 等爽彤,那么我們這里采用哪種方式比較好呢蹬跃?首先想到的是既然 dexopt 進(jìn)程結(jié)束了自然在主進(jìn)程的死循環(huán)中去判斷 dexopt 進(jìn)程是否存在丹喻。但是在實(shí)際操作中發(fā)現(xiàn)柄慰,dexopt 雖然已經(jīng)退出了,但是進(jìn)程并沒有馬上被回收掉蠢挡,所以這個(gè)方法走不通。那么用 Broadcast 廣播可以嗎腹尖?可是可以,但是增加了 Application 的負(fù)擔(dān)断凶,在拉起 dexopt 進(jìn)程前還得注冊(cè)一個(gè)動(dòng)態(tài)廣播介汹,接收到廣播之后還得注銷掉,所以這個(gè)也沒有采用撼港。那么最終采用的方式是判斷文件是否存在蒙揣,在拉起 dexopt 進(jìn)程前在某個(gè)安全的地方建立一個(gè)臨時(shí)文件,然后死循環(huán)判斷這個(gè)文件是否存在瓷炮,在 dexopt 進(jìn)程結(jié)束的時(shí)候刪除這個(gè)臨時(shí)文件,那么在主進(jìn)程的死循環(huán)中發(fā)現(xiàn)此文件不存在了土榴,就直接跳出循環(huán),繼續(xù) Application 初始化操作糯笙。
public class NoteApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//開啟dex進(jìn)程的話也會(huì)進(jìn)入application
if (isDexProcess()) {
return;
}
doInstallBeforeLollipop();
MultiDex.install(this);
}
@Override
public void onCreate() {
super.onCreate();
if (isDexProcess()) {
return;
}
//其他初始化
}
private void doInstallBeforeLollipop() {
//滿足3個(gè)條件够庙,1.第一次安裝開啟剔难,2.主進(jìn)程衫嵌,3.API<21(因?yàn)?1之后ART的速度比dalvik快接近10倍(畢竟5.0之后的手機(jī)性能也要好很多))
if (isAppFirstInstall() && !isDexProcessOrOtherProcesses() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
try {
createTempFile();
startDexProcess();
while (true) {
if (existTempFile()) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
setAppNoteFirstInstall();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
詳細(xì)代碼可見:Github — PhotoNoter/NoteApplication
總的來說桦锄,這種方式好處在于依賴集非常簡單,同時(shí)它的集成方式也是非常簡單碍粥,我們無須去修改與加載無關(guān)的代碼枕面。但是當(dāng)沒有啟動(dòng)過 App 的時(shí)候枕荞,被推送全家桶喚醒或者收到了廣播,雖然這里都是沒有界面的過程,但是運(yùn)用了這種加載方式的話會(huì)彈出 dexopt 進(jìn)程的 Activity,用戶看到會(huì)一臉懵比的虱而。
推薦插件: https://github.com/TangXiaoLv/Android-Easy-MultiDex
坑
Too many classes in –main-dex-list
UNEXPECTED TOP-LEVEL EXCEPTION:com.android.dex.DexException: Too many classes in –main-dex-list, main dex capacity exceeded at com.android.dx.command.dexer.Main.processAllFiles(Main.java:494) at com.android.dx.command.dexer.Main.runMultiDex(Main.java:332) at com.android.dx.command.dexer.Main.run(Main.java:243) at com.android.dx.command.dexer.Main.main(Main.java:214) at com.android.dx.command.Main.main(Main.java:106)
通過 sdk 的 mainDexClasses.rules 知道主 dex 里面會(huì)有 Application、Activity、Service少梁、Receiver著洼、Provider葵陵、Instrumentation绊困、BackupAgent 和 Annotation取视。當(dāng)這些類以及直接引用類比較多的時(shí)候折欠,都要塞進(jìn)主 dex 那先,就引發(fā)了 main dex capacity exceeded build error 。
為了解決這個(gè)問題,當(dāng)執(zhí)行 Create{flavor}{buildType}ManifestKeepList task 之前將其中的 activity 去掉,之后會(huì)發(fā)現(xiàn) /build/intermediates/multi_dex/{flavor}/{buildType}/manifest_keep.txt 文件中已經(jīng)沒有 Activity 相關(guān)的類了。
def patchKeepSpecs() {
def taskClass = "com.android.build.gradle.internal.tasks.multidex.CreateManifestKeepList";
def clazz = this.class.classLoader.loadClass(taskClass)
def keepSpecsField = clazz.getDeclaredField("KEEP_SPECS")
keepSpecsField.setAccessible(true)
def keepSpecsMap = (Map) keepSpecsField.get(null)
if (keepSpecsMap.remove("activity") != null) {
println "KEEP_SPECS patched: removed 'activity' root"
} else {
println "Failed to patch KEEP_SPECS: no 'activity' root found"
}
}
patchKeepSpecs()
詳細(xì)可以看 CreateManifestKeepList 的源碼:Github – CreateManifestKeepList
Too many classes in –main-dex-list
沒錯(cuò)狡恬,還是 Too many classes in –main-dex-list 的錯(cuò)誤兔乞。在美團(tuán)的自動(dòng)拆包中講到:
實(shí)際應(yīng)用中我們還遇到另外一個(gè)比較棘手的問題锚国, 就是Field的過多的問題煎楣,F(xiàn)ield過多是由我們目前采用的代碼組織結(jié)構(gòu)引入的,我們?yōu)榱朔奖愣鄻I(yè)務(wù)線谦去、多團(tuán)隊(duì)并發(fā)協(xié)作的情況下開發(fā)妆丘,我們采用的aar的方式進(jìn)行開發(fā)宣脉,并同時(shí)在aar依賴鏈的最底層引入了一個(gè)通用業(yè)務(wù)aar,而這個(gè)通用業(yè)務(wù)aar中包含了很多資源,而ADT14以及更高的版本中對(duì)Library資源處理時(shí),Library的R資源不再是static final的了,詳情請(qǐng)查看google官方說明,這樣在最終打包時(shí)Library中的R沒法做到內(nèi)聯(lián),這樣帶來了R field過多的情況,導(dǎo)致需要拆分多個(gè)Secondary DEX,為了解決這個(gè)問題我們采用的是在打包過程中利用腳本把Libray中R field(例如ID、Layout、Drawable等)的引用替換成常量,然后刪去Library中R.class中的相應(yīng)Field。
同樣,hu關(guān)于這個(gè)問題可以參考這篇大神的文章:當(dāng)Field邂逅65535 。
DexException: Library dex files are not supported in multi-dex mode
com.android.dex.DexException: Library dex files are not supported in multi-dex mode
? at com.android.dx.command.dexer.Main.runMultiDex(Main.java:322)
? at com.android.dx.command.dexer.Main.run(Main.java:228)
? at com.android.dx.command.dexer.Main.main(Main.java:199)
? at com.android.dx.command.Main.main(Main.java:103)
解決:
android {
dexOptions {
preDexLibraries = false
}
}
OutOfMemoryError: Java heap space
UNEXPECTED TOP-LEVEL ERROR:
? java.lang.OutOfMemoryError: Java heap space
解決:
android {
dexOptions {
javaMaxHeapSize "2g"
}
}
Android 分包之旅技術(shù)分享疑難解答
Q1:Facebook mutidex 方案為何要多起一個(gè)進(jìn)程互站,如果采用單進(jìn)程 線程去處理呢标捺?
答:install能不能放到線程里做闺兢?如果開新線程加載悔耘,而主線程繼續(xù)Application初始化—-——導(dǎo)致如果異步化看峻,multidex安裝沒有結(jié)束意味著dex還沒加載進(jìn)來冯勉,這時(shí)候如果進(jìn)程需要seconday.dex里的classes信息不就悲劇了—-某些類強(qiáng)行使用就會(huì)報(bào)NoClassDefFoundError.
FaceBook多dex分包方案
安裝完成之后第一次啟動(dòng)時(shí),是secondary.dex的dexopt花費(fèi)了更多的時(shí)間,認(rèn)識(shí)到這點(diǎn)非常重要轩猩,使得問題轉(zhuǎn)化為:在不阻塞UI線程的前提下彤委,完成dexopt,以后都不需要再次dexopt,所以可以在UI線程install dex了
我們現(xiàn)在想做到的是:既希望在Application的attachContext()方法里同步加載secondary.dex,又不希望卡住UI線程
FB的方案就是:
讓Launcher Activity在另外一個(gè)進(jìn)程啟動(dòng)拯欧,但是Multidex.install還是在Main Process中開啟该贾,雖然邏輯上已經(jīng)不承擔(dān)dexopt的任務(wù)
這個(gè)Launcher Activity就是用來異步觸發(fā)dexopt的 ,load完成就啟動(dòng)Main Activity;如果已經(jīng)loaded揩抡,則直接啟動(dòng)Main Process
Multidex.install所引發(fā)的合并耗時(shí)操作刃泌,是在前臺(tái)進(jìn)程的異步任務(wù)中執(zhí)行的,所以沒有anr的風(fēng)險(xiǎn)
Q2:當(dāng)沒有啟動(dòng)過 App 的時(shí)候,被推送全家桶喚醒或者收到了廣播(App已經(jīng)處于不是第一次啟動(dòng)過)
會(huì)喚醒除抛,而且會(huì)出現(xiàn)dexopt的獨(dú)立進(jìn)程頁面activity护蝶,一閃而過用戶會(huì)懵逼...
改進(jìn)采用新的思路會(huì)喚起新進(jìn)程堤魁,但是該進(jìn)程只會(huì)觸發(fā)一次...
如何保證只觸發(fā)一次赏表? 我們先判斷是否第一次安裝啟動(dòng)應(yīng)用攻泼,當(dāng)應(yīng)用不是第一次安裝啟動(dòng)時(shí)骡男,我們直接啟動(dòng)閃屏頁,并且結(jié)束掉子進(jìn)程即可。
Q3:處于第一次安裝成功之后传趾,app收到推送全家桶是否會(huì)被喚醒榕订?
不會(huì),因?yàn)樾枰状卧赼pplication執(zhí)行過一次推送的init代碼才會(huì)被喚醒
Q4:最終方案?
示例代碼參考 :