为什么方法数不能超过65535?搬上Dalvik工程师在SF上的回答,因为在Dalvik指令集里,调用方法的invoke-kind指令中,method reference index只给了16bits,最多能调用65535个方法,所以在生成dex文件的过程中,当方法数超过65535就会报错。细看指令集,除了method,field和class的index也是16bits,所以也存在65535的问题。一般来说,method的数目会比field和class多,所以method数会首先遇到65535问题,你可能都没机会见到field过65535的情况。

错误明明显示的是DexException: Too many classes in --main-dex-list,和field有什么关系?看看Google自家兄弟提的issue就知道了。在使用multidex的情况下,主dex不管method还是field数目超了65535都会报Too many classes的错误,不使用multidex,就报正常的错误了。

下面的情况是为什么我会遇到field超65535的问题:

实际应用中我们还遇到另外一个比较棘手的问题, 就是Field的过多的问题,Field过多是由我们目前采用的代码组织结构引入的,我们为了方便多业务线、多团队并发协作的情况下开发,我们采用的aar的方式进行开发,并同时在aar依赖链的最底层引入了一个通用业务aar,而这个通用业务aar中包含了很多资源,而ADT14以及更高的版本中对Library资源处理时,Library的R资源不再是static final的了,详情请查看google官方说明,这样在最终打包时Library中的R没法做到内联,这样带来了R field过多的情况。

我们上层十几个业务线为独立module,都依赖base,而base的资源id有个三四千,上层R文件会把下层的R文件合并过来,使用multidex后,会把manifest里的activity、service等和其直接引用类加到main dex中,所以很多R文件涌入,field超个65535很容易出现。

修改R

field这么多怎么办呢?大胆假设只保留最顶层的R文件,因为这个R文件会把下层R文件合并过来,所有的R引用都可以指向这个文件。下层的类要引用最上层的R文件,下层不可能依赖上层,所以修改源代码肯定是走不通的,那就改class文件字节码吧。遍历class文件,把R的引用都指向最上层,把其他没用的R文件删掉。

问题

思路有了,接下来的操作有以下问题:

  • 什么时候修改?

    dex过程是把全部class文件转换成dex文件,所以class字节码的修改要在dex之前,我们决定介入构建流程,在dex之前添加一个gradle任务,用来修改字节码。

  • 用什么修改?

    可以使用asm这个库,由于android gradle间接依赖asm,所以我们可以在build.gradle中直接import相关类。

  • 修改什么?

    当然是修改class文件,那么class文件的路径在哪里?主工程的build/intermediates/exploded-aar中包含了库工程aar解压后的内容,有很多jar文件,这些jar文件太过分散不好操作,由于使用了multidex,看一下dex任务的输入,发现是主工程的build/intermediates/multi-dex/common/debug/allclasses.jar文件,顾名思义,这个文件包含了所有的class文件,直接修改这个jar包里的class文件就可以了。

代码

将下面这段代码放在主工程的build.gradle就不报错了。代码中,unifyRImport任务能跑个10秒钟左右,说长不长,说短不短。

import org.apache.commons.compress.utils.IOUtils
import org.objectweb.asm.* import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry ext {
dpPackagePrefix = 'com/dianping/'
libDrawableClass = 'com/dianping/nova/R\$drawable.class'
} byte[] unifyR(InputStream inputStream, String rootPackagePrefix) {
ClassReader cr = new ClassReader(inputStream);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
mv = new MethodVisitor(Opcodes.ASM4, mv) {
@Override
void visitFieldInsn(int opcode, String owner, String fName, String fDesc) {
if (owner.contains(dpPackagePrefix) && owner.contains("R\$") && !owner.contains(rootPackagePrefix)) {
super.visitFieldInsn(opcode, rootPackagePrefix + "R\$" + owner.substring(owner.indexOf("R\$") + 2), fName, fDesc);
} else {
super.visitFieldInsn(opcode, owner, fName, fDesc);
}
}
}
return mv;
} };
cr.accept(cv, 0);
return cw.toByteArray();
} afterEvaluate {
def manifestFile = android.sourceSets.main.manifest.srcFile
def packageName = new XmlParser().parse(manifestFile).attribute('package')
def rootPackagePrefix = packageName.replace('.', '/') + '/'
println packageName
android.applicationVariants.each { variant ->
def dx = tasks.findByName("dex${variant.name.capitalize()}")
def unifyRImport = "unifyRImport${variant.name.capitalize()}"
task(unifyRImport) << {
Set<File> inputFiles = dx.inputs.files.files
inputFiles.each {
if (it.name.endsWith(".jar")) {
println it
JarFile jarFile = new JarFile(it);
Enumeration enumeration = jarFile.entries();
File tmpFile = new File(it.getParent() + File.separator + "classes.jar.tmp");
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile)); while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement();
String entryName = jarEntry.getName();
ZipEntry zipEntry = new ZipEntry(entryName); InputStream inputStream = jarFile.getInputStream(jarEntry);
if (entryName.startsWith(dpPackagePrefix) && entryName.endsWith(".class")) {
if (!entryName.contains("R\$")) {
jarOutputStream.putNextEntry(zipEntry);
jarOutputStream.write(unifyR(inputStream, rootPackagePrefix));
} else {
//NovaLib中R$drawable有被反射使用,不删除
if (entryName.startsWith(rootPackagePrefix) || entryName.equals(libDrawableClass)) {
jarOutputStream.putNextEntry(zipEntry);
jarOutputStream.write(IOUtils.toByteArray(inputStream));
}
}
} else {
jarOutputStream.putNextEntry(zipEntry);
jarOutputStream.write(IOUtils.toByteArray(inputStream));
}
jarOutputStream.closeEntry();
}
jarOutputStream.close();
jarFile.close();
tmpFile.renameTo(it);
}
}
} tasks.findByName(unifyRImport).dependsOn dx.taskDependencies.getDependencies(dx)
dx.dependsOn tasks.findByName(unifyRImport)
}
}

在mac上跑的好好的,windows跑不通了,还是报class太多的错误。我一想两个系统文件分隔符不同,不会是路径上出了问题吧,最后竟然发现是tmpFile.renameTo(it);这行命令没有重命名成功,Google一搜发现遇到这坑的不在少数,a重命名成b,如果b已经存在,mac的做法直接覆盖,windows就会重命名失败。所以最后在rename前面加了一句,it.delete();

dex任务增量

代码欢乐的跑了几天,有哥们提意见了,什么代码就不改,为什么点击run还要跑一两分钟?在这段时间的背后AS背地里做了什么?数百头母驴为何半夜惨叫?小卖部安全套为何屡遭黑手?女生宿舍内裤为何频频失窃?连环强奸母猪案,究竟是何人所为?老尼姑的门夜夜被敲,究竟是人是鬼?数百只小母狗意外身亡的背后又隐藏着什么?这一切的背后, 是人性的扭曲还是道德的沦丧?是性的爆发还是饥渴的无奈?唉,真是崇拜爱哥

我们可以在AS中看到dex任务又重新跑了一遍,主要时间就花在这上面了。之前的博客讲过,任务增量构建要求输入和输出较上次没有区别,dex重新跑说明输入或者输出有变化,输出是多个dex文件我们没有改动,输入allclasses.jar虽然有更改,但因为源码不变,第二次运行allclasses.jar应该和上次一样的,不应该重新跑啊。比较了两次运行的allclasses.jar的md值,发现还真是不一样啊,看来问题就出在这里了。

zip哈希

关键是为什么前后两次运行allclasses.jar的哈希值不同呢?

话说之前向maven上打包上传aar的时候,发现代码资源都不改动,上传上去的aar哈希值竟然不同,为什么呢?一个简单的a.txt前后zip压缩两次,得到的zip文件哈希值也不同,用beyond compare看了下二进制,还真的不一样。那就去看看zip算法)吧,可以看到header中有时间戳相关的东西,应该就是这导致同样的文件zip压缩后哈希值不同。

jar打包也是用的zip算法,因为第一次运行我们修改了allclasses.jar,导致第二次运行时,某个任务的输出发生了变化,所以会重新运行生成allclasses.jar,前后两次的allclasses.jar哈希值就发生了变化,dex任务就要重新跑了。

增量思路

之前的问题,主要还是没有把allclasses.jar及时还原。因为allclasses.jar是dex的输入,所以我们需要在dex之后把allclasses.jar还原,既然需要还原,那就需要在修改allclasses.jar的时候有个备份(classes.bak)。还有个问题,每次unifyRImport任务运行时,都要重新去生成精简后的allclasses.jar,这一步可以加上缓存,根据allclasses.jar的md5值命名缓存文件(.jar.opt),如果有缓存直接复制成allclasses.jar就可以了。

代码

afterEvaluate {
def manifestFile = android.sourceSets.main.manifest.srcFile
def packageName = new XmlParser().parse(manifestFile).attribute('package')
def rootPackagePrefix = packageName.replace('.', '/') + '/' android.applicationVariants.each { variant ->
def dx = tasks.findByName("dex${variant.name.capitalize()}")
Set<File> inputFiles = dx.inputs.files.files
def allClassesJar;
inputFiles.each {
if (it.name.endsWith(".jar")) {
allClassesJar = it;
}
} if (allClassesJar != null) {
def unifyRImport = "unifyRImport${variant.name.capitalize()}"
def bakJar = new File(allClassesJar.getParent(), allClassesJar.name + ".bak")
task(unifyRImport) << {
File unifyRJar = new File(allClassesJar.getParent(), "${md5(allClassesJar)}.jar.opt")
if (!unifyRJar.exists()) {
allClassesJar.getParentFile().eachFile { file ->
if (file.name.endsWith(".jar.opt")) {
file.delete()
}
}
JarFile jarFile = new JarFile(allClassesJar);
Enumeration enumeration = jarFile.entries();
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(unifyRJar)); while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement();
String entryName = jarEntry.getName();
ZipEntry zipEntry = new ZipEntry(entryName); InputStream inputStream = jarFile.getInputStream(jarEntry);
if (entryName.startsWith(dpPackagePrefix) && entryName.endsWith(".class")) {
if (!entryName.contains("R\$")) {
jarOutputStream.putNextEntry(zipEntry);
jarOutputStream.write(unifyR(inputStream, rootPackagePrefix));
} else {
//NovaLib中R$drawable有被反射使用,不删除
if (entryName.startsWith(rootPackagePrefix) || entryName.equals(libDrawableClass)) {
jarOutputStream.putNextEntry(zipEntry);
jarOutputStream.write(IOUtils.toByteArray(inputStream));
}
}
} else {
jarOutputStream.putNextEntry(zipEntry);
jarOutputStream.write(IOUtils.toByteArray(inputStream));
}
jarOutputStream.closeEntry();
}
jarOutputStream.close();
jarFile.close();
} if (bakJar.exists()) {
bakJar.delete()
}
allClassesJar.renameTo(bakJar)
copyFileUsingStream(unifyRJar, allClassesJar)
}
tasks[unifyRImport].dependsOn dx.taskDependencies.getDependencies(dx)
dx.dependsOn tasks[unifyRImport] //还原allclasses.jar
def assemble = tasks.findByName("assemble${variant.name.capitalize()}")
def restoreClassesJar = "restore${variant.name.capitalize()}"
task(restoreClassesJar) << {
if (bakJar.exists()) {
allClassesJar.delete()
bakJar.renameTo(allClassesJar)
}
}
tasks[restoreClassesJar].dependsOn dx
assemble.dependsOn tasks[restoreClassesJar]
}
}
}

这次主要修改了afterEvaluate里面的东西,然后新加了自定义的md5和copyFileUsingStream方法,groovy都有些脚本的特性了,获取md5和复制文件还要自己撸,我也是醉了。

dex增量

至此,Field 65535的问题基本上算是完美解决了。但是你会发现改了一行代码,build的时间还是很久,主要耗时的任务就是dex,这个怎么搞?

两种方案,BuckLayoutCast。Buck是facebook出品的,微信很早就用上了,但有很多规则,侵入性较强,代码改动大。 LayoutCast是我司屠大师研发的,对项目改动非常小,应该也有借鉴buck的一些思路。

稍微看了一下buck的思路,buck的dex粒度非常小,每个module都会打成一个dex,最后合并成一个大的dex,修改代码后,只需要重新生成代码所在的dex,然后通过adb传递到手机,动态替换该dex即可,都不需要重新生成apk,也节省了安装的时间。

转自 http://www.cnblogs.com/android-blogs/p/5778997.html

Android方法数不能超过65535的更多相关文章

  1. Android为什么方法数不能超过65535

    言归正传,来聊聊为什么方法数不能超过65535?搬上Dalvik工程师在SF上的回答,因为在Dalvik指令集里,调用方法的invoke-kind指令中,method reference index只 ...

  2. Android方法数methods超过65536

    当Android App中的方法数超过65535时,如果往下兼容到低版本设备时,就会报编译错误: Cannot fit requested classes in a single dex file. ...

  3. 彻底解决Android 应用方法数不能超过65K的问题

    作为一名Android开发者,相信你对Android方法数不能超过65K的限制应该有所耳闻,随着应用程序功能不断的丰富,总有一天你会遇到一个异常: Conversion to Dalvik forma ...

  4. (转载)Android 方法数超过64k、编译OOM、编译过慢解决方案。

    Android 方法数超过64k.编译OOM.编译过慢解决方案.   目前将项目中的leancloud的即时通讯改为环信的即时通讯.当引入easeui的时候 出现方法数超过上限的问题. 搜索一下问题, ...

  5. Android 方法数超过64k、编译OOM、编译过慢解决方案。

    目前将项目中的leancloud的即时通讯改为环信的即时通讯.当引入easeui的时候 出现方法数超过上限的问题. 搜索一下问题,解决方法很简单. 这里简单记录一下,顺序记录一下此解决方案导致的另一个 ...

  6. Android方法引用数超过65535优雅解决

    随着应用不断迭代更新,业务线的扩展,应用越来越大(比如:集成了各种第三方SDK或者公共开源的Library文件.jar文件)这样一来,项目耦合性就很高,重复作用的类就越来越多了,SO:问题就来了.相信 ...

  7. Android方法数超出限定的问题(multiDex,jumboMode)

    在Android项目开发中,项目代码量过大或通过引入很多jar导致代码量急剧增加,会出现错误: android.dex.DexIndexOverflowException: Cannot merge ...

  8. 解决Android 应用方法数不能超过65K的问题

    Conversion to Dalvik format failed:Unable toexecute dex: method ID not in [0, 0xffff]: 65536 假设你的应用出 ...

  9. 针对android方法数64k的限制,square做出的努力。精简protobuf

    1.早期的Dalvik VM内部使用short类型变量来标识方法的id,dex限制了程序的最大方法数是65535,如果超过最大限制,无法编译,把dex.force.jumbo=true添加到proje ...

随机推荐

  1. 最新spring官网(spring.io)下载方法

    这里介绍的是用于WEB开发的spring-frame框架的下载方法. 如果想下载其他的spring产品,直接进入http://projects.spring.io,选择自己要的即可.下载方法同下. 要 ...

  2. 【渗透课程】第二篇上-http请求协议的简单描述

    HTTP协议剖析 什么是HTTP协议?如何发起请求?我认为这样讲大家能够理解: 浏览器访问网站也是http请求的一个过程.当你打开浏览器,访问一个URL (协议://服务器IP:端口/路径/文件)的时 ...

  3. tensorflow安装调试总结(持续更新)

    这段时间需要部署tensorflow到linux上,由于堡垒机不能连外网,所以pip.apt-get.wget.git统统不能用,然后就是各种调试了,下面整理了一些遇到的问题和解决方案,供大家参考(C ...

  4. Android持续集成之Jenkins 部署

    Android持续集成之Jenkins 部署 [TOC] 0x00安装 准备工作如下: Tomcat8.5下载地址 Jenkins下载链接 1 将下载的jenkins.war包放至tomcat下的we ...

  5. Python 的经典入门书籍有哪些?

    是不是很多人跟你说,学Python开发就该老老实实地找书来看,再配合死命敲代码?电脑有了,软件也有了,心也收回来了?万事俱备,唯独只欠书籍?没找到到合适的书籍?可以看看这些. 1.Python基础教程 ...

  6. 使用CefSharp 在C#用户控件中嵌入Chrome浏览器使用方法

    CEF(Chromium Embedded Framework, 嵌入式Chromium框架)是C/C++开发的库 目前 Google Chrome(Google浏览器),Chromium浏览器,Op ...

  7. Springmvc_validation 效验器

    springmvc-validation效验器的使用介绍 对于任何一个应用来说,都会做数据的有效性效验,但是只在前端做并不是很安全,考虑到安全性這个时候会要求我们在服务端也对数据进行有效验证,spri ...

  8. Java基础学习 —— bat处理文件

    bat处理文件:就是一次性可以执行多个命令的文件 为什么要学bat处理文件? 快速运行一个软件我一般都会打包成jar包的形式来执行jar双击对图形界面管用 但是对控制台的程序是不起作用的.对于控制台的 ...

  9. 五,ESP8266 TCP服务器多连接

    一些时间去准备朋友的元器件了... 接着写,,争取今天写完所有的文章,,因为答应了朋友下周5之前要做好朋友的东西 对于TCP大家在玩AT指令的时候有没有发现客户端最多连接5个,,,再连接就不行了?? ...

  10. java初阶

    java的开发工具分成 IDE(integrated developmentenvironment )和JDk(Java Development Kit) 一个.java中只能有一个public类且至 ...