一、为什么要进行Dex分包

Android下单个Dex文件存在65535函数数量的限制,而一个功能稍微复杂点的APP很容易超过这个限制,为此,我们引入了Dex分包,将一个App所有class分别打包为classes1.dex, classes2.dex, classes3.dex…

二、Dex加载时机

首先,从原理上分析,多个Dex的加载必须要在class被load进内存之前,否则会导致loadClass过程中出现ClassNotFoundException,而Dex的加载是通过MultiDex.install(context)方法进行的,这个方法的内部实现细节我们稍后分析。可以看到这个方法是需要context参数的,而在APP启动过程中,最早可以拿到context的回调就是Application.attachBaseContext(),对于App启动流程不清楚的可以点这里:App启动流程,所以Dex的加载我们会放在App的Application中的attachBaseContext()回调中进行:

// APP自定义的Application
public class MyApplication extends Application {@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);MultiDex.install(this);}
}

三、Dex加载原理

我们先来看看MultiDex.install()的源码

public static void install(Context context) {Log.i(TAG, "install");//判断Android系统是否已经支持了MultiDex,如果支持了就不需要再去安装了,直接返回if (IS_VM_MULTIDEX_CAPABLE) {Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");return;}// 如果Android系统低于MultiDex最低支持的版本就抛出异常if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");}try {// 获取应用信息ApplicationInfo applicationInfo = getApplicationInfo(context);// 如果应用信息为空就返回,比如说运行在一个测试的Context下。if (applicationInfo == null) {// Looks like running on a test Context, so just return without patching.return;}// 同步方法synchronized (installedApk) {// 获取已经安装的APK的全路径String apkPath = applicationInfo.sourceDir;if (installedApk.contains(apkPath)) {return;}// 把路径添加到已经安装的APK路径中installedApk.add(apkPath);// 如果编译版本大于最大支持版本,报一个警告if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "+ Build.VERSION.SDK_INT + ": SDK version higher than "+ MAX_SUPPORTED_SDK_VERSION + " should be backed by "+ "runtime with built-in multidex capabilty but it's not the "+ "case here: java.vm.version=\""+ System.getProperty("java.vm.version") + "\"");}/* 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.*/ClassLoader loader;try {// 获取ClassLoader,实际上是PathClassLoaderloader = context.getClassLoader();} catch (RuntimeException e) {/* Ignore those exceptions so that we don't break tests relying on Context like* a android.test.mock.MockContext or a android.content.ContextWrapper with a* null base Context.*/Log.w(TAG, "Failure while trying to obtain Context class loader. " +"Must be running in test mode. Skip patching.", e);return;}// 在某些测试环境下ClassLoader为nullif (loader == null) {// Note, the context class loader is null when running Robolectric tests.Log.e(TAG,"Context class loader is null. Must be running in test mode. "+ "Skip patching.");return;}try {// 清除老的缓存的Dex目录,来源的缓存目录是"/data/user/0/${packageName}/files/secondary-dexes"clearOldDexDir(context);} catch (Throwable t) {Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "+ "continuing without cleaning.", t);}// 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);// 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex// 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);if (checkValidZipFiles(files)) {// 1这个是核心的加载dex的地方// 如果抽取的文件是有效的,就安装secondaryDexinstallSecondaryDexes(loader, dexDir, files);} else {Log.w(TAG, "Files were not valid zip files. Forcing a reload.");// Try again, but this time force a reload of the zip file.// 如果抽取出的文件是无效的,那么就强制重新加载,这么做的话速度就慢了一点,有一些IO开销files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);if (checkValidZipFiles(files)) {// 强制加载后,如果文件有效就安装,否则就抛出异常installSecondaryDexes(loader, dexDir, files);} else {// Second time didn't work, give upthrow new RuntimeException("Zip files were not valid.");}}}} catch (Exception e) {Log.e(TAG, "Multidex installation failure", e);throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");}Log.i(TAG, "install done");
}

直接看我这里标的1部分,前面的主要是一些抽取dex文件列表和校验的逻辑,下面是installSecondaryDexes(loader, dexDir, files)的源码

private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,InvocationTargetException, NoSuchMethodException, IOException {if (!files.isEmpty()) {// 这里根据不同SDK_VISION走了不同的方法if (Build.VERSION.SDK_INT >= 19) {V19.install(loader, files, dexDir);} else if (Build.VERSION.SDK_INT >= 14) {V14.install(loader, files, dexDir);} else {V4.install(loader, files);}}
}

开启套娃模式,再来看看V19.install的源码:

/*** Installer for platform versions 19.*/
private static final class V19 {private static void install(ClassLoader loader, List<File> 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.*/// 传递的loader是PathClassLoader,findFidld()方法是遍历loader及其父类找到pathList字段// 实际上就是找到BaseClassLoader中的DexPathListField pathListField = findField(loader, "pathList");// 获取PathClassLoader绑定的DexPathList对象Object dexPathList = pathListField.get(loader);ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();// 扩展DexPathList对象的Element数组,数组名是dexElements// makeDexElements()方法的作用就是调用DexPathList的makeDexElements()方法来创建dex元素expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));// 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况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);}}/*** A wrapper around* {@code private static final dalvik.system.DexPathList#makeDexElements}.*/// 通过反射的方式调用DexPathList#makeDexElements()方法// dexPathList 就是一个DexPathList对象private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory,ArrayList<IOException> suppressedExceptions)throws IllegalAccessException, InvocationTargetException,NoSuchMethodException {// 获取DexPathList的makeDexElements()方法Method makeDexElements =findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,ArrayList.class);// 调用makeDexElements()方法,根据外界传递的包含dex文件的源文件和优化后的缓存目录返回一个Element[]数组return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,suppressedExceptions);}
}

四、MultiDex的一些问题与解决方案

1. MultiDex的性能问题

多dex在初始化时,通过刚才的代码分析我们知道,实际上在Application.attachBaseContext()中执行的MultiDex.install()方法内部会进行,包括dex读取、验证、合并、插入等一系列操作,首次启动还会做dex——>odex的转换,而默认情况下这些操作都是在UI线程进行的,且由于dex初始化非常靠前(实际上是在Activity初始化之前就完成了),所以如果分包过多实际上会被APP启动速度产生一定的负向影响。根据 Carlos Sessa的测试,启用multidex后,4.4或以下的设备,app的启动时间平均会增加15%,更严重的情况,甚至在启动时候会出现黑屏。

解决思路:

目前业界主流的解决思路主要有两种:
① 单独开启线程对MultiDex进行初始化,在通常在闪屏页开启,初始化完成后进去主页面;
② 单独开启进程并创建临时文件,初始化完成后删除临时文件并finish辅助进程,主进程会轮询临时文件是否存在,删除则进入主页面。(这个也是头条的方案,实际上头条还对MultiDex中多余的一次zip压缩进行了优化),详细方案可自行检索。

2. Multidex分包后主包方法数依旧超出65535问题

这种主要发生在一些大型APP,尤其是存在很多第三方依赖的APP,即时进行了初步分包,其主包方法数往往还是超过65535。

解决思路:

首先要明白分包的核心思路是主页面及其所依赖的类必须要在主包中,其他的可以放在子dex中启动后加载即可。使用SAX自行解析AndroidMainfest.xml,抽取出组件信息,将原始的Manifest_keep.txt内容替换掉,去除启动不需要的Activity组件,保证启动加载的类最小。
在gradle中添加multiDexExt扩展块,通过指定类名或通配符来设置必须编译在MainDex中类,在扩展块中指定的类都会被添加到maindexlist.txt文件汇中。详细可参考网易的优化思路:传送门

Android 进阶——MultiDex分包与动态加载原理剖析相关推荐

  1. Android插件化开发之动态加载技术简单易懂的介绍方式

    转载地方:https://segmentfault.com/a/1190000004062866 基本信息 Author:kaedea GitHub:android-dynamical-loading ...

  2. 携程Android App插件化和动态加载实践

    转载自:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?email=947091870@qq.com 编者按:本文为携程无 ...

  3. Android插件化开发之动态加载本地皮肤包进行换肤

    Android插件化开发之动态加载本地皮肤包进行换肤 前言: 本文主要讲解如何用开源换肤框架 android-skin-loader-lib来实现加载本地皮肤包文件进行换肤,具体可自行参考框架原理进行 ...

  4. 美团Android DEX自动拆包及动态加载简介

    概述 作为一个android开发者,在开发应用时,随着业务规模发展到一定程度,不断地加入新功能.添加新的类库,代码在急剧的膨胀,相应的apk包的大小也急剧增加, 那么终有一天,你会不幸遇到这个错误: ...

  5. Android插件化开发之动态加载的类型

    https://segmentfault.com/a/1190000005113493 基本信息 Author:kaedea GitHub:android-dynamical-loading 现在网络 ...

  6. Android插件化开发之动态加载技术系列索引

    动态加载介绍 在Android开发中采用动态加载技术,可以达到不安装新的APK就升级APP功能的目的,可以用来到达快速发版的目的,也可以用来修复一些紧急BUG. 现在使用得比较广泛的动态加载技术的核心 ...

  7. 进阶Frida--Android逆向之动态加载dex Hook(三)

    前段时间看到有朋友在问在怎么使用frida去hook动态加载的dex之类的问题,确实关于如何hook动态加载的dex,网上的资料很少,更不用说怎么使用frida去hook动态加载的dex了.(frid ...

  8. Android插件化开发之动态加载三个关键问题详解

    本文摘选自任玉刚著<Android开发艺术探索>,介绍了Android插件化技术的原理和三个关键问题,并给出了作者自己发起的开源插件化框架. 动态加载技术(也叫插件化技术)在技术驱动型的公 ...

  9. Android插件化开发之动态加载基础之ClassLoader工作机制

    类加载器ClassLoader 早期使用过Eclipse等Java编写的软件的同学可能比较熟悉,Eclipse可以加载许多第三方的插件(或者叫扩展),这就是动态加载.这些插件大多是一些Jar包,而使用 ...

最新文章

  1. java连接Hbase数据库
  2. 创建Student Course SC表
  3. Linux下JNI实现
  4. Code Snippets
  5. 在Visual Studio Code里编写ABAP代码
  6. 决策树算法学习笔记(提升篇)
  7. SPAW Editor .NET Edition v.2乱用:使用代码调整编辑器高度
  8. mybatis的动态sql学习注意点!!!
  9. Android 缓存处理和图片处理
  10. new 操作符干了什么?
  11. 盘口的买一是卖股票还是买股票?
  12. matlab bwdist
  13. 计算机网络第二章-----物理层
  14. 基于ssm实验室管理系统mysql
  15. 算法导论------渐近记号Θ、Ο、o、Ω、ω详解
  16. linux 双显示器 异常,终于搞定双显示器了
  17. IPV4与IPV6练习
  18. 约翰霍普金斯大学计算机专业,约翰霍普金斯大学计算机科学专业介绍_计算机科学专业排名及就业方向和前景-小站留学...
  19. 入门JAVA第十六天 数据库
  20. Beta发布——美工+文案

热门文章

  1. 任性 CSS 实现 Donut loading
  2. python常见的数据类型有哪些?
  3. 自动驾驶/智能网联港口货运示范应用现状
  4. 英语发音规则---R字母
  5. 6个使用的Python脚本
  6. hugginface/diffusers 原理
  7. 烧心吃什么马上能缓解11 oracle,烧心吃什么马上能缓解,盘点五种食物有效缓解烧心...
  8. 哪些食物慢性胃炎患者更适合多吃?
  9. 【记录】 layui导出表格设置单元格格式
  10. 亚商投资顾问 早餐FM/0526新兴产业