Android Multidex 遇到的问题

来源:互联网 发布:java 多线程 线程池 编辑:程序博客网 时间:2024/06/03 15:05

Android 的classLoader在加载APK的时候限制了class.dex包含的Java方法总数不能超过65535,但是现在随便一个复杂一点的App,轻而易举就能超过65535。为了解决这个问题,google推出了官方的解决方案——Multidex

一、使用之后,相信很多人都遇到过以下几个问题:

1. Dalvik LinearAlloc Limit
安装时异常 

Installation error: INSTALL_FAILED_DEXOPTPlease check logcat output for more details.Launch canceled!
运行时异常 
Application causes dalvik crash on gingerbread devices:LinearAlloc exceeded capacity (8388608), last=6888VM abortingFatal 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 parametersafterEvaluate {    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 : createXXXMainDexClassListtasks.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逆向工具


0 0
原创粉丝点击