在整理MultiDex优化以前,先了解一下Apk的编译流程,这样有助于后面针对MultiDex优化。html
Android Studio 按下编译按钮后发生了什么?java
1. 打包资源文件,生成R.java文件(使用工具aapt,这个工具在Android 使用 aapt 命令查看 apk 包名 提到过,感兴趣的能够了解一下)数组
2. 处理aidl文件,生成java代码(没有aidl 则忽略)缓存
3. 编译 java 文件,生成对应.class文件(java compiler)app
4. class 文件转换成dex文件(dex)框架
5. 打包成没有签名的apk(使用工具apkbuilder)异步
6. 使用签名工具给apk签名(使用工具Jarsigner)ide
在第4步,将class文件转换成dex文件,默认只会生成一个dex文件,单个dex文件中的方法数不能超过65536,否则编译会报错,可是咱们在开发App时确定会集成一堆库,方法数通常都是超过65536的,解决这个问题的办法就是:一个dex装不下,用多个dex来装,gradle增长一行配置:multiDexEnabled true。工具
具体配置方案能够参考:Android 分包 MultiDex 策略总结。oop
虽然配置好了MultiDex分包策略,可是咱们发如今Android 4.4 的手机上仅执行 MultiDex.install(context) 就可能消耗1秒多的时间,那么为何会这么耗时呢?这里先分析一下MultiDex的原理。
首先咱们来看一下MultiDex.install()方法具体执行的内容:
public static void install(Context context) { Log.i("MultiDex", "Installing application"); if (IS_VM_MULTIDEX_CAPABLE) { //5.0 以上VM基本支持多dex,啥事都不用干 Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled."); } else if (VERSION.SDK_INT < 4) { // throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + "."); } else { ... doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true); ... Log.i("MultiDex", "install done"); } }
从上面的源码能够看到,若是虚拟机自己就支持加载多个dex文件,那就啥都不用作;若是是不支持加载多个dex(5.0如下是不支持的),则走到 doInstallation 方法。
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 { //获取非主dex文件 File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName); MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir); IOException closeException = null; try { // 1. 这个load方法,第一次没有缓存,会很是耗时 List files = extractor.load(mainContext, prefsKeyPrefix, false); try { //2. 安装dex installSecondaryDexes(loader, dexDir, files); } } }
看一下 1. MultiDexExtractor#load 具体都执行了哪些内容:
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException { if (!this.cacheLock.isValid()) { throw new IllegalStateException("MultiDexExtractor was closed"); } else { List files; if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) { try { //读缓存的dex files = this.loadExistingExtractions(context, prefsKeyPrefix); } catch (IOException var6) { Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6); //读取缓存的dex失败,多是损坏了,那就从新去解压apk读取,跟else代码块同样 files = this.performExtractions(); //保存标志位到sp,下次进来就走if了,不走else putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files); } } else { //没有缓存,解压apk读取 files = this.performExtractions(); //保存dex信息到sp,下次进来就走if了,不走else putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files); } Log.i("MultiDex", "load found " + files.size() + " secondary dex files"); return files; } }
查找dex文件,有两个逻辑,有缓存就调用loadExistingExtractions方法,没有缓存或者缓存读取失败就调用performExtractions方法,而后再缓存起来。使用到缓存,那么performExtractions 方法想必应该是很耗时的,分析一下代码:
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException { //先肯定命名格式 String extractedFilePrefix = this.sourceApk.getName() + ".classes"; this.clearDexDir(); List<MultiDexExtractor.ExtractedDex> files = new ArrayList(); ZipFile apk = new ZipFile(this.sourceApk); // apk转为zip格式 try { int secondaryNumber = 2; //apk已是改成zip格式了,解压遍历zip文件,里面是dex文件, //名字有规律,如classes1.dex,class2.dex for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) { //文件名:xxx.classes1.zip String fileName = extractedFilePrefix + secondaryNumber + ".zip"; //建立这个classes1.zip文件 MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName); //classes1.zip文件添加到list files.add(extractedFile); Log.i("MultiDex", "Extraction is needed for file " + extractedFile); int numAttempts = 0; boolean isExtractionSuccessful = false; while(numAttempts < 3 && !isExtractionSuccessful) { ++numAttempts; //这个方法是将classes1.dex文件写到压缩文件classes1.zip里去,最多重试三次 extract(apk, dexFile, extractedFile, extractedFilePrefix); ... } //返回dex的压缩文件列表 return files; }
这里的逻辑就是解压apk,遍历出里面的dex文件,例如class1.dex,class2.dex,而后又压缩成class1.zip,class2.zip...,而后返回zip文件列表。
只有第一次加载才会执行解压和压缩过程,第二次进来读取sp中保存的dex信息,直接返回file list,因此第一次启动的时候比较耗时。dex文件列表找到了,回到上面MultiDex#doInstallation方法的注释2,找到的dex文件列表,而后调用installSecondaryDexes方法进行安装,怎么安装呢?方法点进去看SDK 19 以上的实现:
private static final class V19 { private V19() { } static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { Field pathListField = MultiDex.findField(loader, "pathList");//1 反射ClassLoader 的 pathList 字段 Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList(); // 2 扩展数组 MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); ... } private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions)); } }
1. 反射ClassLoader 的 pathList 字段
2. 找到pathList 字段对应的类的makeDexElements 方法
3. 经过MultiDex.expandFieldArray 这个方法扩展 dexElements 数组,怎么扩展?看下代码:
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field jlrField = findField(instance, fieldName); Object[] original = (Object[])((Object[])jlrField.get(instance)); //取出原来的dexElements 数组 Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length)); //新的数组 System.arraycopy(original, 0, combined, 0, original.length); //原来数组内容拷贝到新的数组 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); //dex二、dex3...拷贝到新的数组 jlrField.set(instance, combined); //将dexElements 从新赋值为新的数组 }
就是建立一个新的数组,把原来数组内容(主dex)和要增长的内容(dex二、dex3...)拷贝进去,反射替换原来的dexElements为新的数组,以下图:
Tinker热修复的原理也是经过反射将修复后的dex添加到这个dex数组去,不一样的是热修复是添加到数组最前面,而MultiDex是添加到数组后面。这样讲可能还不是很好理解?来看看ClassLoader怎么加载一个类的就明白了~
不论是 PathClassLoader仍是DexClassLoader,都继承自BaseDexClassLoader,加载类的代码在 BaseDexClassLoader中,具体文件路径以下:/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java。
代码如图:
1.构造方法经过传入dex路径,建立了DexPathList。
2. ClassLoader的findClass方法最终是调用DexPathList 的findClass方法
接下来看一下DexPathList源码/dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList里面定义了一个dexElements 数组,findClass方法中用到,看下
findClass方法逻辑很简单,就是遍历dexElements 数组,拿到里面的DexFile对象,经过DexFile的loadClassBinaryName方法加载一个类。
最终建立Class是经过native方法,就不追下去了,你们有兴趣能够看下native层是怎么建立Class对象的。
那么问题来了,5.0如下这个dexElements 里面只有主dex(能够认为是一个bug),没有dex二、dex3...,MultiDex是怎么把dex2添加进去呢?
答案就是反射DexPathList的dexElements字段,而后把dex2添加进去,固然,dexElements里面放的是Element对象,只有dex2的路径,必须转换成Element格式才行,因此反射DexPathList里面的makeDexElements 方法,将dex文件转换成Element对象便可。
dex二、dex3...经过makeDexElements方法转换成要新增的Element数组,最后一步就是反射DexPathList的dexElements字段,将原来的Element数组和新增的Element数组合并,而后反射赋值给dexElements变量,最后DexPathList的dexElements变量就包含新加的dex在里面了。
makeDexElements方法会判断file类型,上面讲dex提取的时候解压apk获得dex,而后又将dex压缩成zip,压缩成zip,就会走到第二个判断里去。仔细想一想,其实dex不压缩成zip,走第一个判断也没啥问题吧,那谷歌的MultiDex为何要将dex压缩成zip呢?
在Android开发高手课中看到张绍文也提到这一点:
也就是说,这个压缩过程是多余的,后面咱们会介绍一下头条App参考谷歌的MultiDex优化这个多余的压缩过程,后续会介绍一下头条的方案。
这里咱们先总结一下ClassLoader的加载原理 <==> ClassLoader.loadClass -> DexPathList.loadClass -> 遍历dexElements数组 ->DexFile.loadClassBinaryName。
通俗点说就是:ClassLoader加载类的时候是经过遍历dex数组,从dex文件里面去加载一个类,加载成功就返回,加载失败则抛出Class Not Found 异常。
在明白ClassLoader加载类原理以后,咱们能够经过反射dexElements数组,将新增的dex添加到数组后面,这样就保证ClassLoader加载类的时候能够重新增的dex中加载到目标类,通过分析后最终整理出来的原理图以下:
咱们了解了MultiDex原理以后,就应该考虑如何优化MultiDex了。
MultiDex的优化的重点在于解决install过程耗时,耗时的缘由主要是涉及到解压apk取出dex、压缩dex、将dex文件经过反射转换成DexFile对象、反射替换数组。
想到优化此耗时问题,首先咱们会想到异步,也就是开启一个子线程执行install操做,可是这样作真的可行吗?实践事后就发现,方案存在很大的问题。
这个方案的思路为:在闪屏页开一个子线程去执行MultiDex.install,而后加载完才跳转到主页。须要注意的是闪屏页的Activity,包括闪屏页中引用到的其它类必须在主dex中,否则在MultiDex.install以前加载这些不在主dex中的类会报错Class Not Found。
如何保证闪屏页在主dex里面呢?这里咱们可使用Gradle来配置:
defaultConfig { //分包,指定某个类在main dex multiDexEnabled true multiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的这些类的混淆规制,没特殊需求就给个空文件 multiDexKeepFile file('maindexlist.txt') // 指定哪些类要放到main dex }
maindexlist.txt 文件指定哪些类要打包到主dex中,内容格式以下
com/lanshifu/launchtest/SplashActivity.class
可是,真正在已有项目中用使用这种方式,会发现编译运行在Android 4.4的机器上,启动闪屏页,加载完准备进入主页直接报错NoClassDefFoundError。NoClassDefFoundError 在这里出现知道就是主dex里面没有该类,通常状况下,这个方案的报错会出如今三方库的中,尤为是ContentProvider相关的逻辑。
应用进程不存在的状况下,从点击桌面应用图标,到应用启动(冷启动),大概会经历如下流程:
Launcher startActivity
AMS startActivity
Zygote fork 进程
ActivityThread main()
4.1. ActivityThread attach
4.2. handleBindApplication
4.3 attachBaseContext
4.4. installContentProviders
4.5. Application onCreate
ActivityThread 进入loop循环
Activity生命周期回调,onCreate、onStart、onResume...
整个启动流程咱们能干预的主要是 4.三、4.5 和6,应用启动优化主要从这三个地方入手。理想情况下,这三个地方若是不作任何耗时操做,那么应用启动速度就是最快的,可是现实很骨感,不少开源库接入第一步通常都是在Application onCreate方法初始化,有的甚至直接内置ContentProvider,直接在ContentProvider中初始化框架,不给你优化的机会。
子线程install的方案之因此出现问题也正是由于上述的原理所说,即:ContentProvider初始化太早了,若是不在主dex中,还没启动闪屏页就已经crash了。
总结一下这种方案的缺点:
1. MultiDex加载逻辑放在闪屏页的话,闪屏页中引用到的类都要配置在主dex。
2. ContentProvider必须在主dex,一些第三方库自带ContentProvider,维护比较麻烦,要一个一个配置。
下面咱们看一下今日头条是如何优化MultiDex的。
1.在主进程Application 的 attachBaseContext 方法中判断若是须要使用MultiDex,则建立一个临时文件,而后开一个进程(LoadDexActivity),显示Loading,异步执行MultiDex.install 逻辑,执行完就删除临时文件并finish本身。
2. 主进程Application 的 attachBaseContext 进入while代码块,定时轮循临时文件是否被删除,若是被删除,说明MultiDex已经执行完,则跳出循环,继续正常的应用启动流程。
3.MultiDex执行完以后主进程Application继续走,ContentProvider初始化和Application onCreate方法,也就是执行主进程正常的逻辑。
注意:LoadDexActivity 必需要配置在main dex中。