当前位置: 代码迷 >> Android >> Android中的 Multiple dex files define 编译异常引发的思考
  详细解决方案

Android中的 Multiple dex files define 编译异常引发的思考

热度:532   发布时间:2016-04-24 11:11:28.0
Android中的 Multiple dex files define 编译错误引发的思考

昨天我龙哥问我一个问题,他说如果一个工程中,有一个com.x.A枚举,导入的第三方jar中也有一个com.x.A枚举,那么我在工程中用A枚举的时候,会用到那个枚举呢?我当时一想,这个不是类(枚举是个特殊类)定义冲突吗?应该在编译的时候就报错呢,而且这个问题我之前遇到过,所以我很自信的和他说,这个应该在编译的时候就报错,结果他来了一句:没有呀?运行成功了,而且导入的是工程中的那个枚举A,我擦,我一想这不是打我脸吗?我记得非常清楚,是会报错的呀,所以我就自己写了一个AndroidDemo工程:


工程很简单,那么运行一下:


妈蛋,果然报错呀,编译就失败了,所以我又很有底气的去找他理论,说我这都可以的呀,然后他也说没有问题,那么问题就陷入僵局了,但是我不能这么算了呀,我就怀疑是环境的问题?工程的问题?编译器的问题?一顿怀疑之后,那就尝试,首先,猜测我们两的调用方式是否一样,他使用maven的,而我用的是build paths方式的,这个有点不同,所以我就叫他改成我这样的,同时他使用注解的方式用NodeCode的,我是用NodeCode a = NodeCode.A方式,然后叫我哥把用法改成和我一模一样的,编译一下之后,他还是没有报错?那么是不是环境问题呢?他用的是idea,我用的是Eclipse,然后我用idea工具试了一下,也是报错的,那么不是编辑器的问题,蛋疼了一会之后,仔细看看那个错误,发现是dex字眼,突然想到,我哥是开发Web的,我是开发Android的,他的工程是Web工程,我是Android工程,这是不是问题呢?所以我立马新建一个Java工程来测试一下(其实Java工程和JavaWeb工程都一样,因为都是用JVM虚拟机的,用javac进行编译的):


运行:


尼玛,尽然可以,擦,果然是工程问题,而且,我们在工程中的NodeCode中打印信息

发现打印的信息,可以得知,默认优先导入的是工程中的那个枚举类。


那么问题弄清楚了,Android中项目不可以,Java项目可以

但是我们得看看为什么呢?所以只能通过源码去看问题了,但是在看源码之前,其实我们可以猜测一下问题的根源?

Android中的是dex文件,我们知道dex文件是用dx命令将多个class文件合成得到的,所以dex是一个文件,他有具体的格式,关于dex的格式说明,不了解的同学可以查看这篇文章:http://www.wjdiankong.cn:8888/blog/?p=508,而我们知道一个jar或者是运行的Java工程,都是编译之后在bin目录下的class文件,我们猜测编译时都报错了,那么肯定是发生在dx执行的那一块,所以我去找dx命令的源码,源码位置:源码目录\dalvik\dx\src\com\android\dx\command

主要看main方法:


我们再到这个类看看:com.android.dx.command.dexer.Main

入口方法main:


这里解析命令行参数,然后执行run方法:


我们看到这里有一个很重要的方法,就是合并引用第三方的jar的dex内容,正是我们想要知道的结果,进入看看:


方法的注释:

/**
 * Merges the dex files in library jars. If multiple dex files define the
 * same type, this fails with an exception.
 */

到这里,其实我们看到这个方法的注释说明,就知道了如果定义了相同的types就会抛出一个异常,我们在进入看看什么类型,抛出来的异常和我们看到的是一样吗?

有一个重要的类:DexMerger 位于:com.android.dx.merge.DexMerger

这个类的构造方法会传递两个DexBuffer进去,第一个DexBuffer是我们之前一定操作好的dex内容,第二个DexBuffer是我们需要合并的Library的dex内容,构造好类之后,在调用merge方法:


merge方法中调用了mergeDexBuffers方法:


我擦,看到这里是不是有点熟悉,merge很多方法,而且这些merge的动作就是我们之前解析dex文件格式中说道的那几种类型,但是这里我们是类定义冲突,那么肯定是看mergeClassDefs方法:


在这个方法中会先得到有序的type类型,然后在设置classDefs区域的偏移值和大小,在看看getSortedTypes方法:


这里其实是将dexA和dexB中的typeIds进行排序合并,在看看readSortableTypes方法:


好吧,终于看到核心的内容了,在这里会判断这个type是否已经存在了,如果存在的话,就会抛出异常信息,而且这个异常信息就是和我们之前看到的一样:


到这里我们可以知道,dex文件中是不允许有相同的类(包名+类名),而且这里的包名+类名就是type。这个在之前解析dex文件格式中有说道,这里就不解释了。

其实我们可以想想,dex是一个文件,他有自己的文件结构,相同的内容是不可能存在的,会出现冲突。

所以现在我们知道为何Android工程不行,原因就是dx在进行class到dex转化的时候就出错了,也就是发生在编译期。

但是在Java工程中是可以的,其实想一下还是合理的,因为如果我们是将一个Java工程变成一个Jar文件,其实jar文件其实是一个压缩包,压缩包里面是不会存在相同的文件的(包名会转化成指定的文件目录),如果有的话,也不会出现冲突,不会出现问题的,但是我们从上面的例子可以看到,会优先导入本工程中的类,所以这个可以查看一下jar工具的源码,地址:

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/sun/tools/jar/Main.java


看这里的main方法,其实他内部用了ZipFile/ZipEntry/ZipInputStream这三个类,来组件一个jar的,操作也很简单,首先得到所有的class文件,然后将包名转化成文件目录即可。但是这里并没有找到如果有相同的第三方jar包含相同的类,会不会进行覆盖的代码的地方,所以这一步靠大家了,但是从上面的例子可以看到,默认用的是工程中的那个类。

这里在多说一句吧,就是我们在导出一个jar的时候,如果要携带第三方的jar的话,导出来我所知道的是两个方法:

1、使用Eclipse插件:fat_jar


2、使用ant脚本跑出来一个


关于网上还有一个方法就是用Eclipse自带的Export,但是需要自己写一个MANIFEST.MF文件,不过这种方法我没成功过,不知道行不行,不过导出来携带第三方的jar的jar,应该是这种样式:

其实,我们可以看出来上面的那两种方式的原理:

首先将本工程中的class文件变成jar,然后在操作libs下的第三方的jar内容,那么下面就是相当于将多个jar进行合并的操作,其实这个就很简单了,我们自己都可以写一个程序,使用ZipFile+ZipEntry即可,如果发现有相对应的ZipEntry的话,就跳过即可,那么最终就可以合并多个jar了。当然这个只是我们的猜想,但是从上面的那个例子可以看到,Java工程可以导入多个包含相同类的jar,不会发生冲突,但是Android工程不行,原因是dx将多个class转化成dex时会进行判断。


需要思考的问题:

当遇到这个问题的第一时间应该想到是编译器出现的问题,所以这个和Android中的DVM和Java中的JVM没有关系,因为虚拟机是在运行期才会用到。所以我们应该从Android的编译过程去看问题,这是解决问题的思路问题。


得到的知识点:Android工程中是不允许存在相同的类,Java工程是可以的


PS: 关注微信,最新Android技术实时推送



  相关解决方案