问题:项目方法超 65535 编译报错。
产生原因:
我们都知道 Android apk 构建中会把所有 class 文件都转化为 Dex 文件,而单个 Dex 中方法数不能超过 2 的 16 次方,即 65535。既然是编译构建 dex 过程中发生的错误,肯定是由转化 dex 的工具 dx 抛出来的异常。
By the way : 各个版本 SDK Build tools 都可以从这里下载
我们先来认识下 dx(build tools version 27.0.3 ,dx version 1.14) 工具,它在 SDK build tools 下。找到命令看看 Usage
命令行执行:
$ /Users/handsomeyang/Library/Android/sdk/build-tools/27.0.3/dx --help
输出:
|
|
可见 dx 工具描述为:把一系列 class 文件 或者 jar 包 转化为 dex 文件的工具。
顺便介绍一下 dx 的比较常见 options 参数,后面还是说到,这里简单介绍下:
- - - multi-dex :能够生成多个 dex files
- - -main-dex-list=
: 参数 file 是列出 main dex (即 classes.dex) 中所包含的类信息。通俗点就是你想要主 Dex 中有哪些类不被分出去就写在 file 里,把 file 传进参数命令里; 通常用来解决分包造成的 classNotDefException 问题。 - - - minimal-main-dex : 只有- -main-dex-list=
指定的 file 中的类被保留在主Dex,用来减小 main dex 压力。
简单给大家演示一下 dx 工具命令行用法:(注意如果报错很可能是 jar 包的问题, 比如 classpath 之类的问题)
- jar —> dex :
- command :
dx --dex --debug --verbose --output=/Users/handsomeyang/Desktop/demo /Users/handsomeyang/Desktop/Demo.jar
默认会生成 classes.dex ,当然是在如果你方法数、字段数没超 65535 的情况下,但是超了会发生什么呢:smiley:
- Multidex : 注意要设置
--set-max-idx-number=1000
哦,默认应该是 jar 包方法数超过 65535 才会分包。而且这个--set-max-idx-number=1000
是对生成的每个 dex 文件都起限制的。 - command :
dx --dex --debug --verbose --multi-dex --set-max-idx-number=1000 --output=/Users/handsomeyang/Desktop/demo /Users/handsomeyang/Desktop/Demo.jar
看下控制台输出
|
|
然后,就会在/Users/handsomeyang/Desktop/demo
目录下生成诸如 classes.dex
、calssses2.dex
、classes3.dex
… 而且每个 dex 方法数都不超过 1000。
其实这个 dx 工具就是一个 bash 脚本(windows 下是batch), 用文本编辑器打开 dx (这个是bash):
|
|
其实我们看注释就够了,dx 脚本只是对 dx.jar 的使用做了封装,实际上 dx 工具只做了两件事:一是找到同级目录下 libs 下 dx.jar 的 path,二是读取你设置的 Java 虚拟机内存配置参数(可选)。
这里我们留意个细节:
|
|
使用 dx 工具操作 apk 或者 dex ,有时候会报 OOM 提示 java heap size
之类关键字,就是执行 dx 命令 Java Process 内存最大参数太小了,可以调大试试。(应该只有我们产品用的到吧,因为我们产品有命令行工具用到了 dx 工具,而且出现过 OOM,所以有点印象,当提醒我自己哈哈)。
关键在这一行:exec java $javaOpts -jar "$jarpath" "$@"
其实就是:java -jar /Users/handsomeyang/Library/Android/sdk/build-tools/27.0.3/lib/dx.jar
你会发现跟直接执行 dx 命令 输出是一样的 :)
就是说 class 转 dex 实际上是由 dx.jar 来完成的。 既然是可运行的 jar 包, 里面肯定有 META-INF 目录,一般这个目录下面的 MANIFEST.MF 文件会保存 jar 包 启动类 路径。直接打开 MANIFESET.MF:
|
|
这里把 dx.jar 拷贝到 IDE 任何一个项目 libs 下,依赖上就能看到 dx.jar 源代码,而且支持跳转。
这里简单带大家梳理下 dx multidex 的工作流程,其他功能自己可以看看。
com.android.dx.command.Main
中的 Main 方法主要起一个枢纽的功能,把传进的参数做解析,就是上面 [- - xxx ] 之类的,其实就是一堆 if else 分支语句。不同功能,会把相应参数再次传到相应类的 Main 方法中。我们找我们关心的 dex 转换处理:
|
|
找到下一个类 com.android.dx.command.dexer.Main
, 其实无非也是先解析参数,把参数封装到一个 Arguments
的内部类中。
|
|
在解析并且封装 Main 方法中的参数的时候还有个小插曲要介绍:
|
|
|
|
这里简单提出来 parse
方法的一段 if else
分支,注释很清楚了,就是 转换 dex 的时候可以通过--set-max-idx-number=
来改 dex 默认的引用 id 上限(默认 65535),当然只能往小了改。
然后调用 runDx
方法,runDx
方法判断了是否需要 multidex 分多个dex 处理 ,需要 multidex 处理的走 runMultidex()
这个方法。我们看需要 multidex 处理的情况,这里贴一下代码块:
|
|
(关键地方我都注释了)
classes.dex、classes2.dex、classes3.dex… 来历
|
|
上面的runMultidex
一共两个关键点:
class —> dex 的逻辑;
65535 问题的产生;
我们先看 dex 转换过程:
processAllFiles
这个方法名字已经很见名知意了:
|
|
经过一系列判读和操作,最终走到processClass
这个方法,看名字也知道是转换关键; processClass
中启动了一个带返回值的线程 callable ,那我们看下call
(相当于 Thread 的 run 方法) 方法。注意这里的 processClass
指的是每一个要被转换的 Class 字节。
|
|
总之这段代码内容就是,先定义 一个 DexFile 输出文件,然后每次 add 一个 Class 字节做处理,如果加上这个 Class 的方法后者字段数超过了 定义的 DexFile id limit ,就再次 new 一个新的 DexFile。
理一下 multidex 分包转化 class 为 dex 的逻辑:runMultiDex
—> processAllFiles
—> 判断 inputFile 类型,如果是压缩类型,就解压后处理每一个 class 字节 —>processFileBytes
—> 过滤掉resource 等字节,留下 class byte —> processClass
—> 判断是否超过 DexFile id limit,超过就 new 新的 DexFile 继续处理字节 —> 最后把转化好格式的字节 写入多个 DexFile 中。具体的 class 转 Dex 格式就比较枯燥了,就不介绍了,了解下流程为我们分析 65535 做铺垫。
那我们来看runMultidex
中 65535 问题的产生根源。
如果不启用 multidex 功能,项目很容易产生 65535 ,但是开启了 multidex 有时候仍然会造成 65535 (这个以后分析),但是开启不开启 multidex 功能,程序丢 65535 异常的原因肯定是一样的,就是向 DexFile 写入字节的时候,方法数超过了 DexFile 的默认最大上限 65535。
|
|
跟踪 DexWriter
方法最终到 DexFile 的 toDex0
方法,里面会对 Dex 文件格式每一个部分进行检查;比如:this.methodIds.prepare();
猜测对 method count 判断在这里进行。跟踪进去后发现,MethodIdsSection
和 FiledIdsSection
的父类都有一个 getTooManyMembersMessage
方法,我贴下面(为了封装这一个共有方法而提的父类)。
|
|
错误信息提示:如果 Dex id 引用太多,你可以开启 multidex 模式,如果已经开启,main dex 中的引用太多了。
这就是在编译期间 Field 和 Method 会抛 Too many field references: 131000; max is 65536.
异常根源。