Android 的classLoader在加载APK的时候限制了class.dex包含的Java方法总数不能超过65535,但是现在随便一个复杂一点的App,轻而易举就能超过65535。为了解决这个问题,google推出了官方的解决方案——Multidex
一、使用之后,相信很多人都遇到过以下几个问题:
1. Dalvik LinearAlloc Limit
安装时异常
Installation error: INSTALL_FAILED_DEXOPT
Please check logcat output for more details.
Launch canceled!
运行时异常
Application causes dalvik crash on gingerbread devices:
LinearAlloc exceeded capacity (8388608), last=6888
VM aborting
Fatal signal 11 (SIGSEGV) at 0xdeadd00d (code=1)
2. 首次安装启动时黑屏没有响应/ANR
3. 经常会报一些NoClassDefFoundError
二、针对这几个问题网上已经有很多讨论,现在总结一下常用的解决方案:
1、第一和第二个问题是由于Multidex 分包之后,主Dex的包过大,启动慢导致的。针对这个问题有以下解决方案:
(1).设置Multidex的分包参数,限制包的大小
a. --set-max-idx-number 用于限制每个dex的方法总数,设置为48000(经验值)可解决2.X系统上multidex导致的LinearAlloc Limit问题。
b. --minimal-main-dex 设置此参数后可让主dex的方法数尽可能的小,可以同--set-max-idx-number配合使用解决LinearAlloc Limit问题。
// hook the dex task for some additional parameters
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (project.android.defaultConfig.multiDexEnabled) {
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--minimal-main-dex'
// for test multidex dex , here set max idx 10000, this apk total methods is about 22251.
dx.additionalParameters += '--set-max-idx-number=10000'
dx.additionalParameters += '--multi-dex'
}
}
}
(2). 应用启动时显示一个欢迎页面,并且这个页面使用一个独立的init进程,目的是了过渡缓冲,让Multidex有充足的时间加载完成
<activity
android:name=".activity.WelcomeActivity"
android:process=":init"
android:screenOrientation="portrait" >
</activity>
Application在非init进程中加载dex:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
initProcessNameAndPackageName(base);
initLog();
if (!isProcessInit()) {
// other process install dex
MultiDex.install(this);
} else {
// init process continue
}
}
利用init进程的欢迎页,可以解决首次启动加载dex导致的黑屏和ANR问题
2、第三个问题需要定制Dex,才能解决
Multidex默认的分dex实现保证了应用内四大组件的class都在主dex中,但仍然会有NoClassXXX类型的crash出现。因为Android 加载Dex files采用的是Lazy Load,这会导致虚拟机中即使已经加载了某个class,但如果这个class不在主dex的class列表中,则主dex有可能引用不到这个class,从而导致NoClassDefFoundError。
为了解决这个问题,我们需要找出在应用启动后,虚拟机中已经加载但不在主dex中的class列表的所有class,记录到一个multidex.keep的文本文件中。关于multidex.keep文件的生成,需要在应用启动后一个合适的时机调用MultiDexUtils的getLoadedExternalDexClasses方法来手动收集:
/**
* Get all loaded external classes name in "classes2.dex", "classes3.dex" ....
* @param context
* @return get all loaded external classes
*/
public List<String> getLoadedExternalDexClasses(Context context) {
try {
final List<String> externalDexClasses = getExternalDexClasses(context);
if (externalDexClasses != null && !externalDexClasses.isEmpty()) {
final ArrayList<String> classList = new ArrayList<String>();
final java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
final ClassLoader cl = context.getClassLoader();
for (String clazz : externalDexClasses) {
if (m.invoke(cl, clazz) != null) {
classList.add(clazz.replaceAll("\\.", "/").replaceAll("$", ".class"));
}
}
return classList;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
上面手动获取了multidex.keep文件之后,接下来需要修改Gradle 编译脚本:在Gradle打包生成Dex文件之前将multidex.keep合并到主Dex中,从而保证主Dex的加载不会发生NoClassDefFoundError。
(1)首先Hook android gradle multidex list 相关 task:在createXXXMainDexClassList task之后插入一个自定义task
// hook the android gradle task : createXXXMainDexClassList
tasks.whenTaskAdded { task ->
android.applicationVariants.all { variant ->
if (task.name == "create${variant.name.capitalize()}MainDexClassList" ) {
task.finalizedBy "fix${variant.name.capitalize()}MainDexClassList"
}
}
}
(2)在构建变种variant中加入该自定义task的声明,两个关键步骤见code中的Step1、Step2
// hook the variant to add fixXXXMainDexClassList task.
android.applicationVariants.all { variant ->
task "fix${variant.name.capitalize()}MainDexClassList" << {
println "Fixing main dex keep file for $variant.name, while the build type is release."
if (new File("${rootProject.projectDir}/buildsystem/multidex.keep").exists()
&& variant.buildType.name == 'release'
&& project.android.defaultConfig.multiDexEnabled) {
File keepFile = new File("$buildDir/intermediates/multi-dex/${variant.dirName}/maindexlist.txt")
// Step1 利用multidex.keep的列表找到混淆后的class name
// Read proguard mapping file to find real class name in dex file
def mappingList = ["key":"value"];
File mapping = new File("$buildDir/outputs/mapping/${variant.dirName}/mapping.txt")
if (mapping.exists()) {
mapping.eachLine { line ->
if (!line.startsWith(" ") && line.endsWith(":")) {
String key = line.split("->")[0].trim();
String value = line.split("->")[1].trim().split(":")[0].trim();
mappingList.put(key, value);
}
}
}
keepFile.withWriterAppend { w ->
// Get a reader for the input file
w.append('\n')
// Step2 将对应的class list插进入multidex的构建产物maindexlist.txt 。
new File("${rootProject.projectDir}/buildsystem/multidex.keep").withReader { r ->
boolean hasFindMapping = false
// And write data from the input into the output
mappingList.each {
if (it.key.equals(r)) {
r = it.value;
hasFindMapping = true
}
}
w << r << '\n'
w.flush()
}
println "Updated main dex keep file for ${keepFile.getAbsolutePath()}"
}
} else {
println 'There is no multidex.keep file in your project root dir or build type is debug or multidex not enabled.'
}
}
}
通过主dex的class list定制和multidex.keep文件的维护,可以解决multidex导致的启动性能问题和大部分NoClassDefFoundError Crash
三、参考文章:
Android应用打破65K方法数限制
美团Android Dex自动拆包
其实你不知道MultiDex到底有多坑
Lazy Loading Dex files
Android’s multidex slows down app startup
android-classyshark
dex-method-count工具
jadx逆向工具