Android Dex 64k Method Size Limit

###问题
Android 开发一年以上或者更久,大部分会遇到如下问题:

  • 64K

Unable to execute dex: method ID not in [0, 0xffff]: 65536
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

  • LinearAlloc

ERROR/dalvikvm(4620): LinearAlloc exceeded capacity (5242880), last=…

如果你遇到这个问题,那么恭喜你,因为你的app可以存活这么久(o)/~, 开完玩笑,我们进入正题,了解一下上面问题产生的原因。

###原因

  • 64K

.dex 文档是 Dalvik EXecutable,里面存的是 dex byte code,可在 Davlik VM 上执行。你 unzip 解开 .apk 后悔看到一个 classes.dex 文档,就是它了。不过,dex method 64k 上限跟 dex 文档的格式无关。根据stackoverlfow 回应的说法,是因为 Dalvik 指令集里,执行 method 的 invoke-kind index 大小只给了 16bit,所以一個 Android APP只能执行前 65536 个 method,后面多的都不能用。
因为是指令集的限制,所以新一代 ART Runtime 也受同樣的限制。.dex 文档记录了method 总数,可以用 Android SDK 內附的 dexdump 指令查看你的 app 定义了多少个 method:(Linux Shell 脚本)

```shell
cd android-sdk-macosx
./build-tools/19.1.0/dexdump -f  /path/to/your/apk | grep method_ids_size
```
输出结果:

```shell
method_ids_size     : 51306
```
5万个 method 很多啊,快到64k上限了,一个app会有如此多的method?后面来分析这一大票 method 是什么鬼。

如果你使用Android Studio 那么可以考虑Github上的一个小插件 [dex-method-counts][1], 或者如果你不希望这个插件额外占用你的 app method, 那么直接使用 JS 脚本检测你的APK method也是个好主意,这两者在统计method思路上是一致的,这里给出 部分 JS 代码
```JavaScript
//Written by Juraj Novák (inloop.eu)
...

DexFile.prototype.getMethodRefs = function () {
    var refs = [];
    for (var i = 0; i < methods.length; i++) {
        var method = methods[i];
        refs[i] = {};
        refs[i].className = classNameFromTypeIndex(method.classIdx);
        refs[i].argArray = argArrayFromProtoIndex(method.protoIdx);
        refs[i].returnType = returnTypeFromProtoIndex(method.protoIdx);
        refs[i].name = strings[method.nameIdx];
    }
    return refs;
};

DexFile.prototype.getHeader = function () {
    return dexFile;
};

function loadStrings(dv) {
    var curStrings = [];
    var offsets = [];
    dv.seek(dexFile.stringIdsOff);

    for (var i = 0; i < dexFile.stringIdsSize; i++) {
        offsets[i] = dv.getInt32();
    }

    dv.seek(offsets[0]);
    for (var i = 0; i < dexFile.stringIdsSize; i++) {
        dv.seek(offsets[i]);
        curStrings[i] = dv.readStringUtf();
    }
    strings = strings.concat(curStrings);
}

function loadTypes(dv) {
    var curTypes = [];
    dv.seek(dexFile.typeIdsOff);
    for (var i = 0; i < dexFile.typeIdsSize; i++) {
        curTypes[i] = dv.getInt32();
    }
    types = types.concat(curTypes);
}

function loadProtos(dv) {
    var curProtos = [];
    dv.seek(dexFile.protoIdsOff);
    for (var i = 0; i < dexFile.protoIdsSize; i++) {
        curProtos[i] = {};
        curProtos[i].shortyIdx = dv.getInt32();
        curProtos[i].returnTypeIdx = dv.getInt32();
        curProtos[i].parametersOff = dv.getInt32();
    }

    for (var i = 0; i < dexFile.protoIdsSize; i++) {
        var offset = curProtos[i].parametersOff;
        curProtos[i].types = [];
        if (offset != 0) {

            dv.seek(offset);
            var size = dv.getInt32();       // #of entries in list

            for (var j = 0; j < size; j++) {
                curProtos[i].types[j] = dv.getInt16() & 0xffff;
            }
        }
    }
    protos = protos.concat(curProtos)
}

function loadFields(dv) {
    var curFields = [];
    dv.seek(dexFile.fieldIdsOff);
    for (var i = 0; i < dexFile.fieldIdsSize; i++) {
        curFields[i] = {};
        curFields[i].classIdx = dv.getInt16() & 0xffff;
        curFields[i].typeIdx = dv.getInt16() & 0xffff;
        curFields[i].nameIdx = dv.getInt32();
    }
    fields = fields.concat(curFields);
}

function loadMethods(dv) {
    var curMethods = [];
    dv.seek(dexFile.methodIdsOff);
    for (var i = 0; i < dexFile.methodIdsSize; i++) {
        curMethods[i] = {};
        curMethods[i].classIdx = dv.getInt16() & 0xffff;
        curMethods[i].protoIdx = dv.getInt16() & 0xffff;
        curMethods[i].nameIdx = dv.getInt32();
    }
    methods = methods.concat(curMethods);
}

function loadClasses(dv) {
    var curClasses = [];
    dv.seek(dexFile.classDefsOff);
    for (var i = 0; i < dexFile.classDefsSize; i++) {
        curClasses[i] = {};
        curClasses[i].classIdx = dv.getInt32();
        curClasses[i].accessFlags = dv.getInt32();
        curClasses[i].superclassIdx = dv.getInt32();
        curClasses[i].interfacesOff = dv.getInt32();
        curClasses[i].sourceFileIdx = dv.getInt32();
        curClasses[i].annotationsOff = dv.getInt32();
        curClasses[i].classDataOff = dv.getInt32();
        curClasses[i].staticValuesOff = dv.getInt32();
    }
    classes = classes.concat(curClasses);
}
...

};
```
还有一部分JS,这里省略。了解上面的基本思路即可,具体如何统计 不是重点, 重点是我们来看看经过上面JS统计出来的结果如下:
![此处输入图片的描述][2]

然后重点来了,com包下有42912个method,是不是很吓人,展开以后我们一探究竟
![此处输入图片的描述][3]
有木有发现,我们自己的代码占了2万多,其他比较过分的分别是 Tencent,alibaba,umeng的sdk,另外还有fasterxml,这个大名鼎鼎,不过人家一开始就不是专门给Android开发的,所以一些流弊的包我们拿过来用的时候,一定要经过自己的判断,适合自己的才是好的。比如这个fasterxml就可以用google的json包代替,下面给出了常用的android 第三方包method统计。

![此处输入图片的描述][4]
  • LinearAlloc

有关 LinearAlloc 的问题,网路上已经有很好的解说。简单说就是 Android 程式执行前会将 class 读进 LinearAlloc 这块 buffer 裡,它的大小在 Android 2.3 之前是 5MB,到了 4.0 后才改成 8MB 或 16MB。5MB 太小了,通常你还没踩到 64k method 限制时,就会先踩到 LinearAlloc 的问题。
这个问题到了 4.0 才改善,但是 2.3 还有约10 % 的市场,所以我们还是得面对它。注意 5MB buffer 的限制不是 classes.dex 的档案大小的限制,像我们自家的 App classes.dex 的大小已经 7 MB 了,还是可以在 2.3 执行。最主要还是看 class 结构的複杂性,以及总 method 数。

看完上面两个问题产生的原因了,我们来综合看一下经常遇到的问题

  • INSTALL_FAILED_DEXOPT

安装 apk 时,如果出现上面提到的两种错误,你通常会看到错误讯息有 INSTALL_FAILED_DEXOPT 这行。dexopt 是 dex optimization 的意思,这一步骤会发生在安装完 apk 之后,它会检验 .dex 裡面的指令集是不是合法,也会验 method 的上限数。超过上限的话,app 还没启动就被这一步挡下,直接喷错。dexopt 也会试著将所有 class/method 都读进 VM 验证,这自然会运用到 LinearAlloc buffer。如果 buffer 不够也是直接喷了。所以程式太大的话,通通会死在 dexopt 这过程里。
上面的错误都已经很清晰的知道原因了,那么我们该如何判断app能安全在android2.3运行呢,接下来我们给出一般的经验值仅供参考。

  • 参考值56000 method(当然也可以判断 classes.dex文件不超过8M大小)

开发进行期间,维持 method 数在 65536 以下 (未 proguard)
开发时通常我们会用 Android 4.0 以上的手机来测,所以不用管56000 method 数的限制。但要确保尚未做 proguard 之前,总 method 数要小于 65536。相信我,如果开发时每次 build 都要做 proguard 才能将 method 数压在 65536 下,你会想死,每 build 一次都要几分钟以上啊。

###解决方案

有了上面的分析,下面解决方案就比较直接了,方法无外乎下面几条。

  1. 精简method
  2. 拆分包

对于以上两个方案说一下体会。

  1. 精简method 可以通过选择合适的第三方包,或者启用proguard,或者删除不用的方法等等,这种方式代价很小,但是毕竟有突破64k的时候,所以,也仅仅是临时的方案。
  2. 拆分包。android官方给出了一个不完整的解决方案,multiDex在gradle中配置即可,比较简单,但是解决不完整,详细可以参考其实你不知道MultiDex到底有多坑
    另外有几篇介绍如何弥补android官方这个漏洞的解决方案,比较复杂,适合喜欢钻研的同学,这里列一篇文章Android拆分与加载Dex的多种方案对比
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值