一、65536是什么样的数?
2的16次方 或者 16进制的 0xFFFF
下边这个error是不是很熟悉
较高版本的Android构建系统下的提示(Android 7.0及以下):
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
较高版本的Android构建系统的报错信息(Android 8.0)
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
二、为什么会出现64K的限制呢?
一般排查问题我们需要从问题本身入手,那么log是最重要的信息。
在构建流程中出现这种问题,根据提示我们大概猜到是因为方法数过多,而这些方法是存在于编译后的.class
文件中的,而.class
最后要存在于dex文件中。
那么如此分析的话,问题应该存在于dex的打包流程当中,这个需要以后深入了解一下。
根据前人的一些分析,我们来看看MemberIdsSection文件。
注意:
源码路径是 /dalvik/dx/src/com/android/dx/dex/file/MemberIdsSection.java
不是/dalvik/dexgen/src/com/android/dexgen/dex/file/MemberIdsSection.java
代码不多,如下:
1 /*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dx.dex.file;
18
19import com.android.dex.DexFormat;
20import com.android.dex.DexIndexOverflowException;
21
22import java.util.Formatter;
23import java.util.Map;
24import java.util.TreeMap;
25import java.util.concurrent.atomic.AtomicInteger;
26
27/**
28 * Member (field or method) refs list section of a {@code .dex} file.
29 */
30public abstract class MemberIdsSection extends UniformItemSection {
31
32 /**
33 * Constructs an instance. The file offset is initially unknown.
34 *
35 * @param name {@code null-ok;} the name of this instance, for annotation
36 * purposes
37 * @param file {@code non-null;} file that this instance is part of
38 */
39 public MemberIdsSection(String name, DexFile file) {
40 super(name, file, 4);
41 }
42
43 /** {@inheritDoc} */
44 @Override
45 protected void orderItems() {
46 int idx = 0;
47
48 if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
49 throw new DexIndexOverflowException(getTooManyMembersMessage());
50 }
51
52 for (Object i : items()) {
53 ((MemberIdItem) i).setIndex(idx);
54 idx++;
55 }
56 }
57
58 private String getTooManyMembersMessage() {
59 Map membersByPackage = new TreeMap();
60 for (Object member : items()) {
61 String packageName = ((MemberIdItem) member).getDefiningClass().getPackageName();
62 AtomicInteger count = membersByPackage.get(packageName);
63 if (count == null) {
64 count = new AtomicInteger();
65 membersByPackage.put(packageName, count);
66 }
67 count.incrementAndGet();
68 }
69
70 Formatter formatter = new Formatter();
71 try {
72 String memberType = this instanceof MethodIdsSection ? "method" : "field";
73 formatter.format("Too many %1$s references to fit in one dex file: %2$d; max is %3$d.%n" +
74 "You may try using multi-dex. If multi-dex is enabled then the list of " +
75 "classes for the main dex list is too large.%n" +
76 "References by package:",
77 memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
78 for (Map.Entry entry : membersByPackage.entrySet()) {
79 formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
80 }
81 return formatter.toString();
82 } finally {
83 formatter.close();
84 }
85 }
86
87}
在48行到49中,我们看到如下可能抛出异常的情况
if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
throw new DexIndexOverflowException(getTooManyMembersMessage());
}
getTooManyMembersMessage()
函数内(72行到77行)有如下异常信息字符串构造
String memberType = this instanceof MethodIdsSection ? "method" : "field";
formatter.format("Too many %1$s references to fit in one dex file: %2$d; max is %3$d.%n" +
"You may try using multi-dex. If multi-dex is enabled then the list of " +
"classes for the main dex list is too large.%n" +
"References by package:",
memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
同时我们还要注意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;
根据注释,我们来到Dalvik 字节码,根据表格中的解释如下图:
可以看到类型索引(16 位),由此可以知道,无论是方法数还是字段数都不能超过65536,这也就是为什么在构建流程中出现65536的报错信息。
由此可以得出结论:
invoke-kind (调用各类方法)指令中,方法引用索引数是 16 位的,也就是最多调用 2^16 = 65536 个方法,这就是 DexFormat 中 MAX_MEMBER_IDX 为 0xFFFF 的原因。
所以单个dex的方法或者字段数量不能超过65536
也可以简单的说:Apk 文件本质上是个压缩文件,它里面包含的class.dex文件是可执行的Dalvik字节码文件,这个.dex文件中存放的是所有编译后的Java代码。Dalvil可执行文件规范限制了(事实上是最初设计上的一个失误)单个.dex文件最多能引用的方法数是65536个,这其中包含了Android Framework、APP引用的第三方函数库以及APP自身的方法。
三、Android 官方是如何解决65536问题的
android 5.0 (API level 21)之前,系统使用的是Dalvik虚拟机来执行Android应用,默认情况下,Dalvik为每个APK只生成一个classes.dex文件,为了避免单个.dex文件方法数超过64K的问题,我们需要拆分这个单一的classes.dex文件,拆分后可能存在类似于classes.dex、classes2.dex等多个.dex文件。在应用启动后,会先加载classes.dex文件,我们称之为主(Primary)dex文件,应用启动后才会依次加载其他.dex文件,这些统称为从(Secondary)dex文件。为了规避这个64K限制,Google推出了一个名为MultiDex Support Library的函数库,当我们下载了Android Support Libraries之后,可以在
从API21及之后的版本Android使用名为ART的虚拟机来代替Dalvik虚拟机,ART天然支持从APK文件中加载多个.dex文件,在应用安装期间,他会执行一个预编译操作,扫描APK中的classes(..N).dex文件并将它们编译成一个单一的.oat文件,在应用运行时去加载这个.oat文件,而不是一个个的加载.dex文件。
四、最好还是避免出现64K限制
当app开始触碰到64K方法数的天花板时,不建议立即使用MultiDex Support Library来将apk中的单一.dex拆分成多个,从而规避64K方法数限制引起的编译错误问题。
最佳实践是永远保持应用的方法数低于64K,永远没有机会使用MultiDex Support Library。
因为使用MultiDex是下下策,在大多数情况下会降低应用的性能。
减少应用方法数的方法:
- 检查应用的直接和简洁第三方依赖
- 使用Proguard移除无用的代码:配置并在Release版本中使用ProGuard,它通过分析字节码压缩功能,能够检测并移除没有使用到的类、字段、方法和属性
如果使用以上方法没法将方法数限制在64K以下,那么就需要使用MultiDex,需要在应用module的build.gradle文件的defaultConfig中添加multiDexEnabled true,接着引入MultiDexApplication 初始化MultiDex。
五、MultiDex局限性
MultiDex Support Library是一个不得已而为之的方案,它本身不是完美的,将它集成到项目中,需要经过完整的测试才能上线,可能会出现应用性能下降等问题,具体局限性如下:
- 应用首次启动时Dalvik虚拟机会对所有的.dex文件执行dexopt操作,生成ODEX文件,这个过程很复杂且非常耗时,如果应用的从dex文件太大,可能会导致出现ANR。
- 在Android 4.0(API level 14)之前的系统上,由于Dalvik linearAlloc的bug,使用MultiDex的应用可能启动失败。
- 引入MultiDex机制时,必然会存在主dex文件和从dex文件,应用启动所需要的类都必须放到主dex文件中,否则会出现NoClassDefFoundError的错误。
六、MeltiDex源码分析
基于com.android.support:multidex:1.0.3版本分析
6.1 判断安卓虚拟机的逻辑
程序入口 MultiDex.install();
public static void install(Context context) {
Log.i("MultiDex", "Installing application");
if (IS_VM_MULTIDEX_CAPABLE) {
//判断VM是否支持Multidex,如果是ART虚拟机默认情况下会启用 MultiDex,并且不需要MultiDex支持库
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if (VERSION.SDK_INT < 4) {
//最低兼容SDK版本是4,这样的手机基本都是看不着了吧
throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
//执行到这里说明当前是Dalvik虚拟机,该进行多dex拆分
doInstallation();
...........
}
}
我们再看看IS_VM_MULTIDEX_CAPABLE
如何定义,做了什么逻辑:
//System.getProperty("java.vm.version") 获取当前虚拟机版本 例:2.1.0
private static final boolean IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
/**
* 1、通过正则表达式将版本号分成major(主版本号)和minor(次版本号)。
* 2、通过判断主版本和次版本是否大于一个常量来判定虚拟机是否支持MultiDex。
*/
static boolean isVMMultidexCapable(String versionString) {
boolean isMultidexCapable = false;
if (versionString != null) {
Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if (matcher.matches()) {
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = major > 2 || major == 2 && minor >= 1;
} catch (NumberFormatException var5) {
}
}
}
Log.i("MultiDex", "VM with version " + versionString + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
return isMultidexCapable;
}
isVMMultidexCapable()
返回true
说明是ART虚拟机自身就支持MultiDex不需要再做任何处理,flase
说明是Dalvik虚拟机自身不支持 MultiDex ,该执行doInstallation()
6.1.1有争议的一个问题
网上搜索很多技术大佬的博客,大部分说:可以通过调用 System.getProperty(“java.vm.version”)来检测当前使用的是哪个虚拟机,如果使用的是ART虚拟机的话,属性值会大于等于2.0.0(重点就是这个=2.0.0)
在这里我纠正下,正确的说法是:如果使用的是ART虚拟机的话,属性值应该大于等于2.1.0(>=2.1.0),有什么依据这样说?用真机(模拟器)安卓系统4.4测试,会发现 System.getProperty(“java.vm.version”)=2.0.0 ,把结果带入isVMMultidexCapable()方法里,返回的是false,说明当前应该(api 4.4版本)是Dalvik虚拟机才对,有悖于技术大佬博客上面所说的等于2.0.0就是ART虚拟机。
6.2 Dex解压和压缩
如果上一步判断是Dalvik虚拟机,执行到了doInstallation()
/**
* applicationInfo.sourceDir获取应用APK所在目录 /data/app/{packageName}-s_ZR1N24kyfFdRoazc7SLw==/base.apk
* applicationInfo.dataDir获取数据所在目录 /data/user/0/{packageName}
*/
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
Set var6 = installedApk;
//考虑到多线程并发下加锁,保证执行一次
synchronized(installedApk) {
//如果应用 没有安装,把installedApk添加到集合中,安装应用的路径:/data/app/packageName/base.apk
if (!installedApk.contains(sourceApk)) {
installedApk.add(sourceApk);
if (VERSION.SDK_INT > 20) {
Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
}
ClassLoader loader;
try {
loader = mainContext.getClassLoader(); //上下文对象中获取ClassLoader对象,提取出来的Dex需要通过ClassLoader真正的被加载执行;
} catch (RuntimeException var25) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
return;
}
if (loader == null) {//说明获取ClassLoader 对象失败
Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
} else {
try {
clearOldDexDir(mainContext);//清理老的缓存DEX文件
} catch (Throwable var24) {
Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
}
//创建一个存放dex的目录 getDexDir()有详细的注释
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
// 把APK中的dex提取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
//apk路径:data/app/packageName/base.apk
//dexDir 路径: data/user/0/packageName/code_cache/secondary-dexes
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
IOException closeException = null;
try {
// 调用MultiDexExtractor.load方法,第一次是没有缓存的,需要IO操作,会非常耗时 返回dex文件列表
List files = extractor.load(mainContext, prefsKeyPrefix, false);
try {
installSecondaryDexes(loader, dexDir, files); //安装提取出来的Dex文件。
} catch (IOException var26) {
if (!reinstallOnPatchRecoverableException) {
throw var26;
}
//出现异常 重新提取dex文件,并安装提取出来的dex文件
Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
files = extractor.load(mainContext, prefsKeyPrefix, true);
installSecondaryDexes(loader, dexDir, files);
}
} finally {
try {
extractor.close();
} catch (IOException var23) {
closeException = var23;
}
}
if (closeException != null) {
throw closeException;
}
}
}
}
}
上面代码进行各种预校验以及获取需要的信息,重点方法MultiDexExtractor.load()
:提取dex
List extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
Log.i("MultiDex", "MultiDexExtractor.load(" + this.sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
if (!this.cacheLock.isValid()) { //文件锁是否还有效,无效抛异常
throw new IllegalStateException("MultiDexExtractor was closed");
} else {
List files;
//forceReload判断文件是否重新加载,isModified()是判断sourceApk文件是否做过修改(简单点说这个条件就是没有覆盖安装过)
if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
try {
files = this.loadExistingExtractions(context, prefsKeyPrefix);//加载之前已经解压过的dex(可以理解缓存过的)
} catch (IOException var6) {
Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
files = this.performExtractions(); //出现异常重新执行提取dex
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);//出现异常保存apk时间戳 Crc码等信息缓存下来用于下次比对。
}
} else {
if (forceReload) {
Log.i("MultiDex", "Forced extraction must be performed.");
} else {
Log.i("MultiDex", "Detected that extraction must be performed.");
}
files = this.performExtractions();//走到else{}说明 没有缓存,本质上提取的是dex文件
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);//把apk 信息缓存下来
}
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
}
看上面的代码是不是有点懵,我先给大家梳理下大概的逻辑:
load()
方法里面有两种逻辑,缓存过的loadExistingExtractions()
和没缓存过的performExtractions()
。
第一次获取dex,没有缓存过任何信息,应先执行performExtractions()。这是一个IO耗时操作(下面会细说),完成这个操作后把信息缓存下来(因为IO很耗时 不能每次都去操作),下一次则读取缓存的loadExistingExtractions(),速度会更快些。
private List performExtractions() throws IOException {
// 格式:base.apk.classes"
String extractedFilePrefix = this.sourceApk.getName() + ".classes";
this.clearDexDir(); //清理dex文件
List files = new ArrayList();
ZipFile apk = new ZipFile(this.sourceApk);// 把.apk转换成.zip
try {
int secondaryNumber = 2;
//apk本质上就是归档文件上面步骤已经把apk变成了zip文件 ,for循环遍历zip文件 ,获取的dex文件
// classes2.dex classesN.dex
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
//获取的应该是base.apk.classes2.zip
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
//创建base.apk.classes2.zip 文件
MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);//
// 添加到文件列表(base.apk.classes2.zip 添加到/data/user/0/packageName/files/code_cache/secondary-dexes文件下)
files.add(extractedFile);
Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
int numAttempts = 0;
boolean isExtractionSuccessful = false; //是否提取成功
while(numAttempts < 3 && !isExtractionSuccessful) {
++numAttempts;
//将classes2.dex文件写到压缩文件classes2.zip里去,最多重试三次
extract(apk, dexFile, extractedFile, extractedFilePrefix);
try {
extractedFile.crc = getZipCrc(extractedFile);
isExtractionSuccessful = true;
} catch (IOException var18) {
isExtractionSuccessful = false;
Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
}
Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
if (!isExtractionSuccessful) {
//未校验通过则删除。
extractedFile.delete();
if (extractedFile.exists()) {
Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'");
}
}
}
if (!isExtractionSuccessful) {
throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
}
++secondaryNumber;
}
} finally {
try {
apk.close();
} catch (IOException var17) {
Log.w("MultiDex", "Failed to close resource", var17);
}
}
return files; //返回dex的压缩文件列表
}
上面的逻辑就是解压apk(apk来自applicationInfo.sourceDir()),遍历出里面的dex文件,例如 classes.dex, classesN.dex,然后又压缩成classes.zip,classesN.zip,然后返回zip文件列表。
总结:第一次加载才会执行耗时IO操作,第二次进来读取缓存中保存的dex信息,直接返回文件列表,所以第一次启动的时候比较耗时。
6.3 安装dex
dex列表已经返回了,该执行 installSecondaryDexes();进行dex安装
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
//针对不同的api 分别进行逻辑处理
if (!files.isEmpty()) {
if (VERSION.SDK_INT >= 19) {
MultiDex.V19.install(loader, files, dexDir);
} else if (VERSION.SDK_INT >= 14) {
MultiDex.V14.install(loader, files);
} else {
MultiDex.V4.install(loader, files);
}
}
}
看下 api19(v14 v4这是对不同版本做了处理 )dex安装处理了什么逻辑 MultiDex.V19.install()
private static final class V19 {
private V19() {
}
static void install(ClassLoader loader, List extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
//反射获取ClassLoader 的 pathList 字段
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
//生成的Dex文件对应的Element数组
//将Element数组插入到原有的dexElments数组后面
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
if (suppressedExceptions.size() > 0) {
Iterator var6 = suppressedExceptions.iterator();
while(var6.hasNext()) {
IOException e = (IOException)var6.next();
Log.w("MultiDex", "Exception in makeDexElement", e);
}
//反射获取到dexElements字段
Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
IOException exception = new IOException("I/O exception during makeDexElement");
exception.initCause((Throwable)suppressedExceptions.get(0));
throw exception;
}
}
private static Object[] makeDexElements(Object dexPathList, ArrayList files, File optimizedDirectory, ArrayList suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
}
}
上面代码做了以下的几个操作
反射获取到 pathList 字段
找到pathList 字段对应的类的makeDexElements 方法(也用到了反射)
通过MultiDex.expandFieldArray 这个方法扩展 dexElements 数组
就是创建一个新的数组,把主dex要增加的内容(classesdex2、classesdexN)拷贝进去,反射替换原来的dexElements为新的数组。
6.4详细源码注释
github地址有需要可以去clone :https://github.com/AndroidProg/MultiDexSources
七、使用 MultiDex 后首次启动 app 有什么优化方向
简单来说,安装完成并初次启动APP的时候,5.0以下某些低端机会出现ANR或者长时间卡顿不进入引导页,而罪魁祸首是MultiDex.install(Context context)的dexopt过程耗时过长。因此需要在初次启动时做特别处理。
解决这个问题的思路以及详细解释,大致分为3步:
- 在
Application.attachBaseContext(Context base)
中,判断是否初次启动,以及系统版本是否小于5.0,如果是,跳到2;否则,直接执行MultiDex.install(Context context)
。 - 开启一个新进程,在这个进程中执行
MultiDex.install(Context context)
。执行完毕,唤醒主进程,自身结束。主进程在开启新进程后,自身是挂起的,直到被唤醒。 - 唤醒的主进程继续执行初始化操作。
这个解决思路对现有代码几乎是没有改动的,并且简单粗暴:既然不能直接调用MultiDex.install(Context context),那么干脆开启新进程调用,因为开启新进程时,主进程已经成为后台进程,即使挂起也不会ANR。
开启新进程加载太麻烦,能不能开新线程加载,而主线程继续Application初始化?显然不行。因为multidex安装没有结束,意味着dex还没加载进来,某些类强行使用就会报NoClassDefFoundError。
方案中有一个小难点是:主进程如何得知加载进程完成加载?我们使用标记来记录。MODE_MULTI_PROCESS
标记使得SharedPreference得以进程间共享,主进程轮询sp文件即可。但是,这个标记在6.0被废除,Google不保证行为的准确性。而且……我们有一万个理由,能不写轮询就不写。那么我们该如何优雅的实现呢?
没错,今天我们使用Messenger的方式来实现:
话不多说,直接上终极版解决代码:
BaseApplication.class
public abstract class BaseApplication extends Application{
@SuppressWarnings("MismatchedReadAndWriteOfArray")
private static final byte[] lock = new byte[0];
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//Android 5.0以上不需要特别处理
if (!isAsyncLaunchProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
if (needWait(base)) {
/*
第一次启动APP由于MultiDex将会非常缓慢,某些低端机可能ANR。
因此这里的做法是挂起主进程,开启:async_launch进程执行dexopt。
dexopt执行完毕,主进程重新变为前台进程,继续执行初始化。
主进程在这过程中变成后台进程,因此阻塞将不会引起ANR。
*/
DexInstallDeamonThread thread = new DexInstallDeamonThread(this, base);
thread.start();
//阻塞等待:async_launch完成加载
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
thread.exit();
Log.d("BaseApplication", "dexopt finished. alloc MultiDex.install()");
} else {
MultiDex.install(this);
}
}
}
public boolean isAsyncLaunchProcess() {
String processName = SystemUtils.getCurrentProcessName(this);
return processName != null && processName.contains(":async_launch");
}
@SuppressWarnings("deprecation")
private boolean needWait(Context context) {
//这里实现不唯一,读取一个全局的标记,判断是否初次启动APP
//这个标记应当随着版本升级而重置
SharedPreferences sp = SPUtils.getVersionSharedPreferences(context);
return sp.getBoolean(BaseSPKey.FIRST_LAUNCH, true);
}
private static class DexInstallDeamonThread extends Thread {
private Handler handler;
private Context application;
private Context base;
private Looper looper;
public DexInstallDeamonThread(Context application, Context base) {
this.application = application;
this.base = base;
}
@SuppressLint("HandlerLeak")
@Override
public void run() {
Looper.prepare();
looper = Looper.myLooper();
handler = new Handler() {
@SuppressWarnings("deprecation")
@Override
public void handleMessage(Message msg) {
synchronized (lock) {
lock.notify();
}
SPUtils
.getVersionSharedPreferences(application)
.edit()
.putBoolean(BaseSPKey.FIRST_LAUNCH, false)
.apply();
}
};
Messenger messenger = new Messenger(handler);
Intent intent = new Intent(base, LoadResActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("MESSENGER", messenger);
base.startActivity(intent);
Looper.loop();
}
public void exit() {
if (looper != null) looper.quit();
}
}
}
LoadResActivity.class
public class LoadResActivity extends AppCompatActivity {
private Messenger messenger;
@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.activity_load_res);
Log.d("LoadResActivity", "start install");
Intent from = getIntent();
messenger = from.getParcelableExtra("MESSENGER");
LoadDexTask dexTask = new LoadDexTask();
dexTask.execute();
}
class LoadDexTask extends AsyncTask {
@Override
protected Void doInBackground(Void... params) {
try {
MultiDex.install(getApplication());
Log.d("LoadResActivity", "finish install");
messenger.send(new Message());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Void o) {
finish();
System.exit(0);
}
}
@Override
public void onBackPressed() {
//无法退出
}
}
app/AndroidManifest.xml加入:
SystemUtils.class
public class SystemUtils {
//...
/**
* 获取当前进程名。
*/
public static String getCurrentProcessName(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return null;
}
for (ActivityManager.RunningAppProcessInfo processInfo : activityManager.getRunningAppProcesses()) {
if (processInfo.pid == Process.myPid()) {
return processInfo.processName;
}
}
return null;
}
}
最后,因为有大量代码在install之前执行,因此必须把代码中用到的类放到主dex中(需要build tool版本大于21):
app/build.gradle
android {
//...
defaultConfig {
//...
//定义main dex中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
}
}
app/mainDexClasses.pro
#这里只是示例,需要把所有install之前用到的类写进来
-keep public class * extends java.lang.Thread { *; }
-keep public class com.synaric.common.utils.SystemUtils { *; }
-keep public class com.synaric.common.utils.SPUtils { *; }
-keep interface android.content.SharedPreferences { *; }
-keep class android.os.Handler { *; }
-keep class com.synaric.common.BaseSPKey { *; }
-keep class android.os.Messenger { *; }
-keep class android.content.Intent { *; }
八、如何将指定的 class 打进 mainDex
defaultConfig {
//分包1
multiDexEnabled true
multiDexKeepProguard file('multiDexKeep.pro') // keep specific classes using proguard syntax
multiDexKeepFile file('maindexlist.txt') // keep specific classes
}
这样才是真正可以的指定需要的类到maindex的方式,至于配置哪些类到maindex的文件maindexlist.txt可以在app\build\intermediates\multi-dex\debug目录下的mainfest_keep.txt文件找到,然后把自己的类复制粘贴到里面去就可以了。这个文件大致这样:
retrofit2/http/PATCH.class
android/support/design/widget/FloatingActionButtonIcs.class
android/support/design/R$layout.class
android/support/v4/view/NestedScrollingParent.class
android/support/v4/media/session/MediaControllerCompat.class
android/support/annotation/IdRes.class
android/support/v4/view/ViewPager$4.class
android/support/design/widget/Snackbar$2.class
retrofit2/http/Field.class
android/support/annotation/ColorInt.class
感谢
https://www.jianshu.com/p/e4bed5790407
https://blog.csdn.net/weixin_42009516/article/details/79986263
https://www.jianshu.com/p/6c02935a84f7
https://www.jianshu.com/p/c2d7b76ff063
https://blog.csdn.net/qq_17265737/article/details/79074494