问题出现的原因是因为导入融云通信的包后,突然提示:
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 14
targetSdkVersion 21
...
//加入multidex支持
multiDexEnabled true
}
在Application里面重写 attachBaseContext 方法
@Override
protected 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进行补救。下面是他的工作流程。
在运行阶段,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 XX
buildToolsVersion "XX"
defaultConfig {
applicationId "x.x.x"
minSdkVersion XX
targetSdkVersion XX
versionCode XX
versionName "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$attr
at 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 23
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.example.text"
minSdkVersion 17
targetSdkVersion 23
versionCode 1
versionName "1.0"
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles 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文件在它的基础上更改)。
搞定完工!