@linux1s1s
2015-11-04T15:57:20.000000Z
字数 5014
阅读 2765
AndroidBuild
Android 开发一年以上或者更久,大部分会遇到如下问题:
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 脚本)
cd android-sdk-macosx
./build-tools/19.1.0/dexdump -f /path/to/your/apk | grep method_ids_size
输出结果:
method_ids_size : 51306
5万个 method 很多啊,快到64k上限了,一个app会有如此多的method?后面来分析这一大票 method 是什么鬼。
如果你使用Android Studio 那么可以考虑Github上的一个小插件 dex-method-counts, 或者如果你不希望这个插件额外占用你的 app method, 那么直接使用 JS 脚本检测你的APK method也是个好主意,这两者在统计method思路上是一致的,这里给出 部分 JS 代码
//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统计出来的结果如下:
然后重点来了,com包下有42912个method,是不是很吓人,展开以后我们一探究竟
有木有发现,我们自己的代码占了2万多,其他比较过分的分别是 Tencent,alibaba,umeng的sdk,另外还有fasterxml,这个大名鼎鼎,不过人家一开始就不是专门给Android开发的,所以一些流弊的包我们拿过来用的时候,一定要经过自己的判断,适合自己的才是好的。比如这个fasterxml就可以用google的json包代替,下面给出了常用的android 第三方包method统计。
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 一次都要几分钟以上啊。
有了上面的分析,下面解决方案就比较直接了,方法无外乎下面几条。
对于以上两个方案说一下体会。