Android Multidex 遇到的问题

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逆向工具


评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值