Multidex
Dalvik执行的是由多个.class文件合并成的一个.dex文件,而在这其中,会对.dex包中所有的方法通过方法ID做一个索引,存储在一个链表中,而这个链表的长度用一个short类型来保存。short类型占两个字节,所以一个short最大值应该是65536,因此如果我们一个包中的方法数如果太多,以至于链表索引的长度超过了65546,就会有问题。虽然在新版本中Google已经修复了这一问题,但是为了兼容低版本系统,我们需要在方法数超过这一限制时做一些处理。
解决方法有很多,其中有一条就是分割dex,即如果一个dex放不下,就多分几个dex包,这样就不会有问题了。Google官方推出了multidex方案,可以有效的进行dex分包。multidex的原理很简单,在打包时主动分包,分为一个主dex包和多个次dex包,然后一起打包成apk。在应用启动时,会首先加载主dex包作为入口,然后依次加载次dex包。
配置使用
配置使用很简单,分为两步:
- 修改Gradle配置,使项目支持multidex。
1 | android { |
- 在代码中启动MultiDex。
具体来讲,有三种方式。第一种,通过在AndroidManifest文件中指定使用MultidexApplication
1 |
|
第二种,自定义Application并继承MultiDexApplication
1 | public class DemoApp extends MultiDexApplication { |
第三种,自定义Application,然后在attachBaseContext方法中启用MultiDex
1 | public class BaseApplication extends Application { |
其实上述三种方法最终目的都是调用MultiDex.install()
这一方法。
dex包拆分
dex拆包的步骤:
扫描整个工程代码,得到一个记录了标记主、从dex的main-dex-list。
根据main-dex-list,对把项目编译后的.class文件按主、从分开。
分别将主、从.class文件打包为主、从.dex文件。
生成mai-dex-list的工具在Android Sdk的Build Tools中,是一个名字叫mainDexClasses的脚本文件。其核心部分为先生成一个jar包,然后连同所有的文件一起作为参数,调用com.android.multidex.MainDexListBuilder:
1 | java -cp "$jarpath" com.android.multidex.MainDexListBuilder ${disableKeepAnnotated} "${tmpOut}" ${@} || exit 11 |
MainDexListBuilder源码可以在这里查看:MainDexListBuilder.java
主要做的工作就是将符合keep规则的部分加入到主dex中。因为分包之后的合包必须先执行主包再一一合包次包,因此有些必需的文件就得装在主dex包中,这里的keep规则就是为了能够将这部分文件做保护,这样在生成main-dex-list时就会默认放入主dex包中了。
dex包合并
初始化
根据前面MultiDex配置的方法,那么MultiDe在合并的时候就是在首次安装运行时,将从dex包逐一安装,入口就是Application中的这行代码:
1 | MultiDex.install(this); |
在install中:
1 | public static void install(Context context) { |
所以具体的安装是在doInstallation()中执行的,在调用这个方法时传入的主要参数有一个context引用,一个存放base apk的目录,一个app的data目录,一个字符串secondary-dexes作为从dex目录。
总体过程
1 | private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException { |
梳理一下,就是首先看一下sourceApk是否已经安装,如果没有则安装,然后用Context获取一个ClasLoader,清除掉旧的缓存,获取从dex包,然后用前面获取的那个ClassLoader去加载这些从dex包。然后依次来看一下那几个重要的方法。
首先是clearOldDexDir()
:
1 | private static void clearOldDexDir(Context context) throws Exception { |
所以这里是清除了/data/data//files/secondary-dexes
这一目录下的所有文件。
然后是getDexDir()
:
1 | private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException { |
总结一下,这里先创建一个/data/data//code_cache
目录,如果创建失败,改为创建一个/data/data//files/code_cache
目录,然后再在这一目录下创建一个secondary
目录,因此总的来说创建了一个/data/data//files/code_cache/secondary-dexes
或者/data/data//code_cache/secondary-dexes
目录,并返回。这样也跟刚才清除目录项对应了,clear的时候清除的就是创建失败时再次创建的这一目录。
再看创建一个extractor.load()
:
1 | List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException { |
所以load()方法首先要确保当前操作获取到的文件锁是有效的,应该是为了确保多进程操作的安全。然后根据传入的参数forceReload
以及是否有修改,来决定如何提取文件。如果可以,就会获取Existing文件,在此推测是缓存文件。否则,就要通过performExtractions()
方法来获取并通过putStoreApkInfo()
方法保存了。
看看this.loadExistingExtractions()
方法是否符合推测:
1 | private List<MultiDexExtractor.ExtractedDex> loadExistingExtractions(Context context, String prefsKeyPrefix) throws IOException { |
首先获取一个SP对象,里面保存了dex文件数量相关的数据。然后通过遍历,获取所有的dex文件,加入到一个list钟返回。如果期间发现获取不到,则抛出一个异常。结合前面的代码,抛出异常后就需要通过performExtractions()
方法重新提取一次:
1 | private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException { |
可以看到,会从2开始,不断的构造一个dex文件,然后提取这一文件到dexDir中,提取的核心方法为extract()
:
1 | private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException { |
具体的提取过程到这里就结束了,总结一下,就是通过load()方法,将源dex文件一一写入到新的secondary-dexs目录下对应的文件中,这一过程会将源apk文件进行一个解压,且只会从第二个dex文件开始,因为第一个就是主dex文件,主dex是已经安装好了的。
install具体过程
好了,现在所有的次dex包都已经提取出来,接下来就是安装了,安装的入口就是前面的installSecondaryDexes()
方法:
1 | private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException { |
安装dex包时,会根据不同的sdk版本有不同的方法。就以sdk>=19举例,他的install()方法如下:
1 | static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { |
在install中,主要就是通过反射机制,从ClassLoader中获取一个叫做pathlist的Field,再将dex包们通过makeElements()方法产生一个Element数组,然后调用expandFieldArray()方法,按字面意思应该是对数组进行了扩展。最后,查看是否有异常需要抛出。
在Android中,类加载器有两种,DexClassLoader和PathClassLoader,这两个加载器都是继承自BaseClassLoader的。pathlist就是定义在BaseClassLoader中的:
1 | /** |
再看看这个DexPathList具体是什么:
1 | /** |
所以他相当于是封装了一个存放dex路径的list。回到install方法中,按步骤看一下。首先是makeElements():
1 | /** |
看注释就知道,这里也是通过反射,调用了前面DexPathList的makeDexElements()方法,那就来看看吧:
1 | /** |
创建了一个dex/resource的数组:
1 | private static Element[] makeElements(List<File> files, File optimizedDirectory, |
然后是expandFieldArray()方法:
1 | /** |
结合注释就知道,这里是用一个包含了原来的elements的新的数组,加入了新的elemets。这里也是反射,实际发生了变化的是DexPathList的数组:
1 | /** |
总结一下,具体的安装过程就是将Dex包们封装为Element对象,再将这些Element对象插入到原来的Element数组中,充当为新的数组,而这整个过程都是通过反射来做的,具体的执行者就是DexPathList
总结
上面都是一些工具方法,主要用途就是通过反射,获取对象,调用指定方法。
dex合并就到这里了,总结一下合并的步骤:
- 检查当前系统能够自动合并包,以及如果不支持的话系统版本是否支持multidex。
- 清除旧的备用缓存目录,之所以叫备用是因为在创建目录时创建失败才会创建备用目录,这个备用目录是需要删除的。
- 创建新的缓存目录,然后提取从dex文件至缓存目录中。
- 安装缓存目录中断dex文件,这时根据当前系统的版本有不同的安装方法,但大体上逻辑是一样的。