尊重原创 :http://blog.csdn.net/yuanzeyao/article/details/41809423


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

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

可能有些同学会说,解决这个问题很简单,我们只需要在Project.proterty中配置一句话就Ok啦,

dex.force.jumbo=true

是的,加入了这句话,确实可以让你的应用通过编译,但是在一些2.3系统的机器上很容易出现

INSTALL_FAILED_DEXOPT异常


对于以上两个异常,我们先来分析一下原因:

1、Android系统中,一个Dex文件中存储方法id用的是short类型数据,所以导致你的dex中方法不能超过65k

2、在2.3系统之前,虚拟机内存只分配了5M


知道了原因,我们就来一个个的解决上面的问题,首先对于65k的问题,我们在应用层是无法改变android系统的结构的,所以我们无法将数据类型从short改变为int或者其他类型,也就是说一个dex中的方法数不能超过65k是我们无法逾越的鸿沟,我们只能减少一个dex中的方法数,首先最容易想到的方案就是去掉一些无用的Jar包,以及将一些属性设置为public,从而可以去掉get/set方法,这种方法只能临时解决问题,随着时间的推移,总有一天还是会出现方法数超过65k的,毕竟一个应用一般是在加功能,不会减功能。



下面我来向大家介绍两种主流的解决方案,一种是以微信为代表的,将一些功能做成插件,动态加载,另一种方案是以facebook为代表的分包方案,将一个apk中的dex文件分割成多个dex文件,然后动态的去加载dex文件。其实这两种方案的核心思想是一样的,插件是把未来要开发的新功能做成apk和dex动态加载,而分包方案是将已经完成的功能分成多个dex文件动态加载,其实我个人觉得插件方案比分包方案更好的解决了65k的问题,因为插件方案不仅能够解决65k问题,还能让我们的应用体积减小,而分包只能解决65k的问题。


关于插件开发,做成动态加载,我在很早之前一篇文章中就写过其基本思想,有兴趣的同学可以看看

《实现Android 动态加载APK(Fragment or Activity实现)》

http://blog.csdn.net/yuanzeyao/article/details/38565345


下面我们重点介绍分包机制

我们知道一个apk文件里面有一个dex文件,这个dex文件里面都是经过优化了的class文件,所谓分包,就是讲一个dex文件分成多个dex文件,这里我们约定一下,第一个dex叫做main.dex,第二个叫做second.dex,通常在分包的时候,我们需要将应用启动就需要使用的类放入到main.dex中,把不是立马就需要使用的类放入到second.dex中,对于Android系统,他只会默认加载main.dex的,second.dex对于他来说可能只是一个资源文件,它是不会主动去加载second.dex,所以我在应用启动的过程中,我们需要为second.dex创建好一个类加载器,便于我在使用second.dex中的类时,能够里面加载该类。


关于如何加载second.dex也有好多做法,用的比较多的主要有一下几种

1、最简单的做法就是使用DexClassLoader进行加载,并将该DexClassLoader的父加载器设置为PathClassLoader

2、使用DexClassLoader加载,并将DexClassLoader的父加载器设置成PathClassLoader的父加载器,将PahtClassLoader的父加载器设置成DexClassLoader,仔细品味一下1和2的区别

3、将second.dex的路径放入到PathClassLoader的加载路径中


对于第2中方案,在有一种情况下是不能使用的,比如当second.dex通过DexClassLoader加载,但是second.dex中使用了一个类,这个类在main.dex中,这个时候就会抛出类找不到的异常,所以这种方案只能拥有second.dex不会用到main.dex类的时候


以上说的都是理论,下面我们来实战一下

我这里会介绍两种方案,一种是基于gradle构建Android项目,一种是基于Ant构建Android项目

方案一:基于gradle构建Android项目,并实现分包

环境要求:AndroidStudio0.9以上,gradle插件0.14.2以上


1、如果你的工程在eclipse中,那么你需要将该工程导入到Android中,此时需要你升级adt22以上

2、打开你工程的build.gradle文件,检查gradle插件是否是0.14.2版本之后,因为0.14.2之后gradle插件才支持分包



3、打开工程下某一个Moudle的build.gradle文件,添加对android-support-multidex.jar的依赖


4、去掉第三方jar包中重复的类



5、设置虚拟机堆内存空间大小,避免在编译期间OOM



6、gradle构建项目时,貌似默认是不会将so库加入工程的,所以为了避免此种情况发生,我们需要制定so库目录,对于从eclipse转换过来的工程,还需要制定src和资源文件路径



7、如果你的项目依赖了其他库, 分别在各个库工程中加入 multiDexEnabled = true 和 jniLibs.srcDirs =['libs']两个配置即可

8、如果你的项目没有自定义Application,那么你在AndroidManifest.xml中使用MultiDexApplication即可,如果你的项目有自定义Application,并且是继承是Application,那么只需要改为继承MultiDexApplication即可,如果你的项目时继承的其他Application,那么你需要重写

attachBaseContext

@Overrideprotected void attachBaseContext(Context base) {    super.attachBaseContext(base);    MultiDex.install(this);}

经过上述配置,你的项目应该是已经成功分包了。如果分包成功,那么你解压你的apk文件,会发现有两个dex文件,通过上述的配置过程,我们发现此方案我们无法控制哪些类在main.dex中,哪些类在second.dex中,通过此种方案配置分包,可以兼容API4-API20.其加载second.dex采用的是上述方案中的3





下面我们来看看基于Ant构建Android项目,并实现分包过程

在上述方案中,由于我们无法看到gradle构建项目的脚本,所以我们无法控制哪些类在第一个dex,哪些类在第二个dex,此方案中,我们采用Ant构建,Ant是允许用户自己定义构建方案的,比如我们可以通过自定义构建方案,将项目中某些第三方jar包放入到second.dex中,关于这个如何实现,请参考开源项目吧

https://github.com/mmin18/Dex65536.git

由于该项目加载second.dex所采用的方案是上述方案2,比如second.dex中的某些第三方jar包依赖main.dex中的某些类,这种方案就会实现,所以在此我将此方案去掉,换成了方案3,也就是将second.dex的路径设置到PathClassLoader的加载路径中

我只给出Android 4.4中的解决方案,其他系统大同小异

加载second.dex方法

/**@param loader PathClassLoader    @additionalClassPathEntries  要被加载的dex文件,这里就是我们的second.dex    @optimizedDirectory 就是dex文件解压的目录*/ private static void install(ClassLoader loader, List additionalClassPathEntries,                File optimizedDirectory)                        throws IllegalArgumentException, IllegalAccessException,                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {            /* The patched class loader is expected to be a descendant of             * dalvik.system.BaseDexClassLoader. We modify its             * dalvik.system.DexPathList pathList field to append additional DEX             * file entries.             */ //通过反射找到pathList的值            Field pathListField = findField(loader, "pathList");            Object dexPathList = pathListField.get(loader);            ArrayList suppressedExceptions = new ArrayList();//将second.dex 加入到PathClassLoader的加载路径中            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,                    new ArrayList(additionalClassPathEntries), optimizedDirectory,                    suppressedExceptions));            if (suppressedExceptions.size() > 0) {                for (IOException e : suppressedExceptions) {                    Log.w(TAG, "Exception in makeDexElement", e);                }                Field suppressedExceptionsField =                        findField(loader, "dexElementsSuppressedExceptions");                IOException[] dexElementsSuppressedExceptions =                        (IOException[]) suppressedExceptionsField.get(loader);                if (dexElementsSuppressedExceptions == null) {                    dexElementsSuppressedExceptions =                            suppressedExceptions.toArray(                                    new IOException[suppressedExceptions.size()]);                } else {                    IOException[] combined =                            new IOException[suppressedExceptions.size() +                                            dexElementsSuppressedExceptions.length];                    suppressedExceptions.toArray(combined);                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);                    dexElementsSuppressedExceptions = combined;                }                suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);            }        }

分包成功后,解压apk文件,进入assert文件夹,我们看到如下结构,libs.apk就是第三方jar编译后形成的dex文件


对于上面提到的第二个问题INSTALL_FAILED_DEXOPT,根本原因就是2.3版本之前dalvik虚拟机的内存只有5M,所以无论是插件方案还是分包方案在某些手机上还是会遇到该问题,毕竟我们仅仅是减少了每个dex中包的数量,但是方法总数是没有减少的,所以解决此问题的根本方法就是修改虚拟机内存至8M,这个需求在Java层是无法实现,但是可以在c层实现,具体实现流程可以参考开源项目:

https://github.com/viilaismonster/LinearAllocFix.git



至于该方法中用到的一些方法,可以到android-support-multidex.jar中找到,这里就不都贴出来了,如果那里没有写清楚,欢迎留言讨论...






更多相关文章

  1. Android异步加载图像小结
  2. Android gradle build 修改文件名称及目录
  3. Android 强大的图片加载缓存— Glide
  4. 让 Android 可以识别BMP图片文件,且目前Android所支持的所有图片
  5. Android中的gen文件为空或者不存在的处理方法
  6. Android 简单热修复(上)——Java类加载器
  7. Android异步加载全解析之开篇瞎扯淡
  8. android 使用异步加载图片
  9. Android APK 扩展文件

随机推荐

  1. 组件属性
  2. Android 15本经典教程和150多个实例源码
  3. android EditText提示居右无法显示的问题
  4. Android有效解决加载大图片时内存溢出的
  5. RelativeLayout里常用的位置属性
  6. ArcGIS for Android 离线数据编辑原理
  7. android 多媒体数据库详解
  8. android中自定义RadioButton
  9. 控件的间距为0
  10. Android中3种方法实现back键动作