Android Multidex 遇到的问题 首次安装启动时没有响应/ANR

转载自 https://blog.csdn.net/wangbaochu/article/details/51178881

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: 在冷启动时因为需要安装DEX文件,如果DEX文件过大时,处理时间过长,很容易引发ANR(Application Not Responding);

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有充足的时间加载完成


 
 
  1. <activity
  2. android:name= ".activity.WelcomeActivity"
  3. android:process= ":init"
  4. android:screenOrientation= "portrait" >
  5. </activity>
         Application在非init进程中加载dex:


 
 
  1. @Override
  2. protected void attachBaseContext(Context base) {
  3. super.attachBaseContext(base);
  4. initProcessNameAndPackageName(base);
  5. initLog();
  6. if (!isProcessInit()) {
  7. // other process install dex
  8. MultiDex.install( this);
  9. } else {
  10. // init process continue
  11. }
  12. }
      利用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方法来手动收集:


 
 
  1. /**
  2. * Get all loaded external classes name in "classes2.dex", "classes3.dex" ....
  3. * @param context
  4. * @return get all loaded external classes
  5. */
  6. public List<String> getLoadedExternalDexClasses(Context context) {
  7. try {
  8. final List<String> externalDexClasses = getExternalDexClasses(context);
  9. if (externalDexClasses != null && !externalDexClasses.isEmpty()) {
  10. final ArrayList<String> classList = new ArrayList<String>();
  11. final java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod( "findLoadedClass", new Class[]{String.class});
  12. m.setAccessible( true);
  13. final ClassLoader cl = context.getClassLoader();
  14. for (String clazz : externalDexClasses) {
  15. if (m.invoke(cl, clazz) != null) {
  16. classList.add(clazz.replaceAll( "\\.", "/").replaceAll( "$", ".class"));
  17. }
  18. }
  19. return classList;
  20. }
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }
  24. return null;
  25. }
上面手动获取了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(<span class="hljs-string">"<span class="hljs-variable">$buildDir</span>/intermediates/multi-dex/<span class="hljs-subst">${variant.dirName}</span>/maindexlist.txt"</span>)

        
        <span class="hljs-comment">// Step1 利用multidex.keep的列表找到混淆后的class name</span>
        <span class="hljs-comment">// Read proguard  mapping file to find real class name in dex file</span>
        def mappingList = [<span class="hljs-string">"key"</span>:<span class="hljs-string">"value"</span>];
        File mapping = new File(<span class="hljs-string">"<span class="hljs-variable">$buildDir</span>/outputs/mapping/<span class="hljs-subst">${variant.dirName}</span>/mapping.txt"</span>)
        <span class="hljs-keyword">if</span> (mapping.exists()) {
            mapping.eachLine { line -&gt;
                <span class="hljs-keyword">if</span> (!line.startsWith(<span class="hljs-string">" "</span>) &amp;&amp; line.endsWith(<span class="hljs-string">":"</span>)) {
                    String key = line.split(<span class="hljs-string">"-&gt;"</span>)[<span class="hljs-number">0</span>].trim();
                    String value = line.split(<span class="hljs-string">"-&gt;"</span>)[<span class="hljs-number">1</span>].trim().split(<span class="hljs-string">":"</span>)[<span class="hljs-number">0</span>].trim();
                    mappingList.put(key, value);
                }
            }
        }
        keepFile.withWriterAppend { w -&gt;
            <span class="hljs-comment">// Get a reader for the input file</span>
            w.append(<span class="hljs-string">'\n'</span>)

            <span class="hljs-comment">// Step2 将对应的class list插进入multidex的构建产物maindexlist.txt 。</span>
            new File(<span class="hljs-string">"<span class="hljs-subst">${rootProject.projectDir}</span>/buildsystem/multidex.keep"</span>).withReader { r -&gt;
                boolean hasFindMapping = <span class="hljs-literal">false</span>
                <span class="hljs-comment">// And write data from the input into the output</span>
                mappingList.each {
                    <span class="hljs-keyword">if</span> (it.key.equals(r)) {
                        r = it.value;
                        hasFindMapping = <span class="hljs-literal">true</span>
                    }
                }
                w &lt;&lt; r &lt;&lt; <span class="hljs-string">'\n'</span>
                w.flush()
            }
            println <span class="hljs-string">"Updated main dex keep file for <span class="hljs-subst">${keepFile.getAbsolutePath()}</span>"</span>
        }
    } <span class="hljs-keyword">else</span> {
        println <span class="hljs-string">'There is no multidex.keep file in your project root dir or build type is debug or multidex not enabled.'</span>
    }
}

}通过主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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值