问题出现的原因是因为导入融云通信的包后,突然提示:

Error:The number of method references in a .dex file cannot exceed 64K. 
Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html。

解决办法:

在build.gradle里面加入multiDexEnabled true

    defaultConfig {...minSdkVersion 14targetSdkVersion 21...//加入multidex支持multiDexEnabled true}

在Application里面重写 attachBaseContext 方法

   @Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);MultiDex.install(this);}

运行期间又出现:Error:(6, 32) 错误: 程序包android.support.multidex不存在

在6.0以上的系统打包就会遇到这个问题,但是在6.0以下的系统打包没问题。

解决方案如下:

在build.gradle文件里加上

compile 'com.android.support:multidex:1.0.1'

搞定收工。

既然遇到这个问题了,就剖析下为什么Android会有65536的问题。

一,Android中65536的来源

一个 dex 文件的方法引用数不能大于 64k,64k 的准确值是(64 * 1024 = 65536)。

65536的限制是因为Android应用以DEX文件的形式存储字节码文件,在Dalvik字节编码规范里,方法引用索引method referenceindex只有16位,即(2^16)65536个。method reference,这里限制的是自己代码、Android框架、第三方库三者方法数量的总和。Android打包Dex的过程如下:

Main.java里执行:

-> main() -> run() ->不分包执行runMonoDex()(或者分包执行runMultiDex())-> writeDex()

DexFile执行:

->toDex() -> toDex0()

Section:

->Section 的prepare() -> UniformItemSection的prepare0() ->MemberIdsSection的orderItems() -> getTooManyMembersMessage()

在MemberIdsSection里执行了这样一段方法:

protected void orderItems() {int idx = 0;if (items().size() >DexFormat.MAX_MEMBER_IDX + 1) {throw newDexIndexOverflowException(getTooManyMembersMessage());}for (Object i : items()) {((MemberIdItem) i).setIndex(idx);idx++;}}

getTooManyMembersMessage核心代码如下:

private String getTooManyMembersMessage() {try {String memberType = this instanceofMethodIdsSection ? "method" : "field";formatter.format("Too many %s references:%d; max is %d.%n" +Main.getTooManyIdsErrorMessage() + "%n" +"References bypackage:",memberType, items().size(),DexFormat.MAX_MEMBER_IDX + 1);return formatter.toString();}}
}

当代码里检测到方法数量的上限后,就会报错,这里的限制是:DexFormat.MAX_MEMBER_IDX,下面代码找到它的出处:

public final classDexFormat {/*** Maximum addressable field or methodindex.* The largest addressable member is0xffff, in the "instruction formats" spec as field@CCCC or* meth@CCCC.*/public static final int MAX_MEMBER_IDX =0xFFFF;
}

这个MAX_MEMBER_IDX的值是一个int类型定值0xFFFF,转化为10进制就是65535,所以这里大小的限制是不能超过65536的。被设定为65536的原因是因为:invoke-kind (调用各类方法)指令中,方法引用索引数是 16 位的,也就是最多调用 2^16 = 65536 个方法。

二,MultiDex 工作流程:

Multidex在构建打包阶段将Class拆分到多个Dex,使之不超过单Dex最大方法数的限制,是Google官方对64K方法数问题的一种补救措施。即超越限制后,用多个Dex进行补救。下面是他的工作流程。

Android中65536问题剖析-编程之家

在运行阶段,Multidex提取别的非主Dex出来,然后动态装载执行。

三,使用 MultiDex 可能会造成的问题以及解决方案

    1. 分拆导致的crash

     问题:除了报VerifyError外,还有可能报Could not find class,NoClassDefFoundError, Could not find method等。这种错误是因为我们在main dex中调用的函数或类被放在了classes2.dex中,而在classes2.dex还没有被完全加载前,调用这些api就会导致这种问题。

     要确认是否是这个问题导致的错误,我们可以查看:
     app\build\intermediates\multi-dex\debug\maindexlist.txt 这个文本文件,这里列出来的类都会被放在主dex中。

      解决方案:编译过程中,multidex有一生成maindexlist.txt的步骤:createDebugMainDexClassList就是这里生成maindexlist.txt 的,每次编译都会重新生成一次,我们可以使用自定义的方式:multiDexKeepFile file(‘multiDexKeep.txt’)

android {compileSdkVersion XXbuildToolsVersion "XX"defaultConfig {applicationId "x.x.x"minSdkVersion XXtargetSdkVersion XXversionCode XXversionName "XX"multiDexEnabled true//添加此行代码multiDexKeepFile file('multiDexKeep.txt')}}

内容和上面提到的createDebugMainDexClassList生成的maindexlist.txt一样,把这个multiDexKeep.txt文件放在app目录 下。multiDexKeep.txt内容可以如下:

com/test/Util.class
com/test/help/b.class

这样,被keep的class全都留在了classes.dex中。

     2. 首次启动可能出现ANR

         问题:无响应或者卡顿,因为把multidex的install放在了attachBaseContext中,而这个调用又是在MainActivity的onCreate之前的,所以如果2.dex,3.dex第一次加载时间很长,生成odex文件会耗费一定的时间, 就有可能会导致第一次启动出现ANR。

    解决方案:APP第一次启动,卸载、重装时都会做一遍2odex,具体可以查看

/data/data//code_cache/secondary-dexes/目录下的odex文件。把install放到异步线程里去做,写一个类似initAfterDex2Installed方法,来保证2.dex里的类不会提前被调用到,或者输出一个启动界面,停留几秒继进行加载。很多APP启动都有开机广告,或者开机画面,用来来解决app在2.dex加载之前部分功能无法使用的问题,保证某些耗时的操作在首屏启动不进行加载(一些避免ANR的思路)。

   如果MultiDex.install(this),放在后面或者异步来做的话,在MainActivity里的onCreate函数:
setContentView这里就出错了:

java.lang.NoClassDefFoundError: android.support.v7.appcompat.R$attrat android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:289)at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:246)at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:106)at com.cn.x.x.MainActivity.onCreate(MainActivity.java:86)

android.support.v7.appcompat.R$attr在classes2.dex中,在调用时,还没有完成classes2.dex的加载,所以如果要解决的话,或者把这个类放到maindex中,或者让MainActivity的onCreate函数延迟调用。使用插件化的方式来解决maindex的问题,把一些功能做成插件,保证这些dex在首屏启动时不需要被加载。

    或者,自己实现多dex框架,例如微信的实现框架,没有使用MultiDex,而是使用自己的Tinker动态加载dex的方案,也被用于热更新。QQ里有classes6.dex,也就是总共有6个dex,基本上也是在手Q启动界面还没出来时,所有的dex会全部完成2odex的转换,在手机上第一次运行还是会花费不少时间的。

    所以针对ANR还是不建议使用异步加载,合理设计和插件化。

   四,如何将指定的 class 打进 mainDex

        1.Gradle中的配置

        在Gradle中增加afterEvaluate区域。配置如下:

apply plugin: 'com.android.application'android {compileSdkVersion 23buildToolsVersion "22.0.1"defaultConfig {applicationId "com.example.text"minSdkVersion 17targetSdkVersion 23versionCode 1versionName "1.0"multiDexEnabled true}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}
}
afterEvaluate {tasks.matching {it.name.startsWith('dex')}.each { def listFile = project.rootDir.absolutePath+'/app/maindexlist.txt'if (dx.additionalParameters == null) { dx.additionalParameters = []}//方法数越界时生成多个dex文件dx.additionalParameters += '--multi-dex'//指定listFile中的类打包到主dex中dx.additionalParameters += '--main-dex-list=' +listFile//-main-dex-list所指定的类才能打包到主dex中,没有这个选项,上个选项就会失效dx.additionalParameters += '--minimal-main-dex'}
}dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.android.support:appcompat-v7:24.0.0-alpha1'compile 'com.android.support:multidex:1.0.1'
}

        2,创建一个maindexlist.txt

            根据上面builde.gradle中的配置,在app目录下创建一个maindexlist.txt,在这个txt里将想要放在主dex中的类写进去。(在\app\build\intermediates\multi-dex\debug目录下可以找到一个maindexlist.txt文件在它的基础上更改)。

搞定完工!

转载于:https://my.oschina.net/u/3761887/blog/1647272