Android dex分包方案

709 查看

当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象

1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT

2. 方法数量过多,编译时出错,提示:

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

 

出现这种问题的原因是

1. Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M

2. 一个dex文件最多只支持65536个方法。

 

针对上述问题,也出现了诸多解决方案,使用的最多的是插件化,即将一些独立的功能做成一个单独的apk,当打开的时候使用DexClassLoader动态加载,然后使用反射机制来调用插件中的类和方法。这固然是一种解决问题的方案:但这种方案存在着以下两个问题:

1. 插件化只适合一些比较独立的模块;

2. 必须通过反射机制去调用插件的类和方法,因此,必须搭配一套插件框架来配合使用;

 

由于上述问题的存在,通过不断研究,便有了dex分包的解决方案。简单来说,其原理是将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。faceBook曾经遇到相似的问题,具体可参考:

https://www.facebook.com/notes/facebook-engineering/under-the-hood-dalvik-patch-for-facebook-for-android/10151345597798920

文中有这么一段话:

However, there was no way we could break our app up this way–too many of our classes are accessed directly by the Android framework. Instead, we needed to inject our secondary dex files directly into the system class loader。

文中说得比较简单,我们来完善一下该方案:除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中,并在Application的onCreate回调中被注入到系统的ClassLoader。因此,对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。

 

下面通过一个简单的demo来讲述dex分包方案,该方案分为两步执行:


整个demo的目录结构是这样,我打算将SecondActivity,MyContainer以及DropDownView放入第二个dex包中,其它保留在第一个dex包。

一、编译时分包

整个编译流程如下:


 

除了框出来的两Target,其它都是编译的标准流程。而这两个Target正是我们的分包操作。首先来看看spliteClasses target。


 

由于我们这里仅仅是一个demo,因此放到第二个包中的文件很少,就是上面提到的三个文件。分好包之后就要开始生成dex文件,首先打包第一个dex文件: 


 

由这里将${classes}(该文件夹下都是要打包到第一个dex的文件)打包生成第一个dex。接着生成第二个dex,并将其打包到资资源文件中:


 

可以看到,此时是将${secclasses}中的文件打包生成dex,并将其加入ap文件(打包的资源文件)中。到此,分包完毕,接下来,便来分析一下如何动态将第二个dex包注入系统的ClassLoader。

 

二、将dex分包注入ClassLoader

这里谈到注入,就要谈到Android的ClassLoader体系。

 


由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,通过查阅开发文档,我们发现他们有如下使用场景:

1. 关于PathClassLoader,文档中写到: Android uses this class for its system class loader and for its application class loader(s),

由此可知,Android应用就是用它来加载;

2. DexClass可以加载apk,jar,及dex文件,但PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。

 

知道了两者的使用场景,下面来分析下具体的加载原理,由上图可以看到,两个叶子节点的类都继承BaseDexClassLoader中,而具体的类加载逻辑也在此类中:

BaseDexClassLoader:  

 

由上述函数可知,当我们需要加载一个class时,实际是从pathList中去需要的,查阅源码,发现pathList是DexPathList类的一个实例。ok,接着去分析DexPathList类中的findClass函数,

DexPathList:

 

上述函数的大致逻辑为:遍历一个装在dex文件(每个dex文件实际上是一个DexFile对象)的数组(Element数组,Element是一个内部类),然后依次去加载所需要的class文件,直到找到为止。

看到这里,注入的解决方案也就浮出水面,假如我们将第二个dex文件放入Element数组中,那么在加载第二个dex包中的类时,应该可以直接找到。

带着这个假设,来完善demo。

在我们自定义的BaseApplication的onCreate中,我们执行注入操作:

 

这是注入的关键函数,分析一下这个函数:

参数libPath是第二个dex包的文件信息(包含完整路径,我们当初将其打包到了assets目录下),然后将其使用DexClassLoader来加载(这里为什么必须使用DexClassLoader加载,回顾以上的使用场景),然后通过反射获取PathClassLoader中的DexPathList中的Element数组(已加载了第一个dex包,由系统加载),以及DexClassLoader中的DexPathList中的Element数组(刚将第二个dex包加载进去),将两个Element数组合并之后,再将其赋值给PathClassLoader的Element数组,到此,注入完毕。

 

现在试着启动app,并在TestUrlActivity(在第一个dex包中)中去启动SecondActivity(在第二个dex包中),启动成功。这种方案是可行。

 

但是使用dex分包方案仍然有几个注意点:

1. 由于第二个dex包是在Application的onCreate中动态注入的,如果dex包过大,会使app的启动速度变慢,因此,在dex分包过程中一定要注意,第二个dex包不宜过大。

2. 由于上述第一点的限制,假如我们的app越来越臃肿和庞大,往往会采取dex分包方案和插件化方案配合使用,将一些非核心独立功能做成插件加载,核心功能再分包加载。