Android中65536问题剖析

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

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文件在它的基础上更改)。

搞定完工!

猜你喜欢

转载自my.oschina.net/u/3761887/blog/1647272