工作流程

MultiDex的工作流程具体分为两个部分,一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 “multiDexEnabled true”),另一部分就是在启动Apk的时候,同时加载多个Dex文件(具体是加载Dex文件优化后的Odex文件,不过文件名还是.dex),这一部分工作从Android 5.0开始系统已经帮我们做了,但是在Android 5.0以前还是需要通过MultiDex Support库来支持(MultiDex.install(Context))。

所以这篇文章主要分析第二部分加载多个Dex文件

源码分析

MultiDex Support的入口是MultiDex.install(Context)

MultiDex.install(Context)

  public static void install(Context context) {Log.i(TAG, "Installing application");// 1. 判读是否需要执行MultiDex。if (IS_VM_MULTIDEX_CAPABLE) {Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");return;}//private static final int MIN_SDK_VERSION = 4;if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");}try {ApplicationInfo applicationInfo = getApplicationInfo(context);if (applicationInfo == null) {Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"+ " MultiDex support library is disabled.");return;}doInstallation(context,new File(applicationInfo.sourceDir),new File(applicationInfo.dataDir),CODE_CACHE_SECONDARY_FOLDER_NAME,NO_KEY_PREFIX,true);} catch (Exception e) {Log.e(TAG, "MultiDex installation failure", e);throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");}Log.i(TAG, "install done");}

获取ApplicationInfo,执行doInstallation方法。

MultiDex.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 {//如果这个方法已经调用过一次,就不能再调用了synchronized (installedApk) {if (installedApk.contains(sourceApk)) {return;}installedApk.add(sourceApk);//private static final int MAX_SUPPORTED_SDK_VERSION = 20;//如果当前Android版本已经自身支持了MultiDex,依然可以执行MultiDex操作但是会有警告。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") + "\"");}//1 返回一个能够读取字节码的ClassLoader,修改他的pathList成员增加DexFile ClassLoader loader = getDexClassloader(mainContext); if (loader == null) {return;}try {//2清除缓存目录/data/data/<package>/files/code-cache/secondary-dexes目录clearOldDexDir(mainContext);} catch (Throwable t) {Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "+ "continuing without cleaning.", t);}//3获取dex缓存目录,具体看下面分析/data/data/<package>/code_cache/secondary-dexesFile dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);// MultiDexExtractor is taking the file lock and keeping it until it is closed.// Keep it open during installSecondaryDexes and through forced extraction to ensure no// extraction or optimizing dexopt is running in parallel.//4 通过MultiDexExtractor加载缓存文件MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);IOException closeException = null;try {List<? extends File> files =extractor.load(mainContext, prefsKeyPrefix, false);try {//5安装提取的dexinstallSecondaryDexes(loader, dexDir, files);// Some IOException causes may be fixed by a clean extraction.} catch (IOException e) {if (!reinstallOnPatchRecoverableException) {throw e;}Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "+ "forced extraction", e);files = extractor.load(mainContext, prefsKeyPrefix, true);installSecondaryDexes(loader, dexDir, files);}} finally {try {extractor.close();} catch (IOException e) {// Delay throw of close exception to ensure we don't override some exception// thrown during the try block.closeException = e;}}if (closeException != null) {throw closeException;}}}

在注释1处返回一个能够读取字节码的ClassLoader,在注释2处清理/data/data/<package>/code-cache的dex缓存目录,在注释3处获取dex缓存目录。

MultiDex.getDexDir

MultiDex在获取dex缓存目录是,会优先获取**/data/data//code-cache作为缓存目录,如果获取失败,则使用/data/data//files/code-cache**目录,而后者的缓存文件会在每次App重新启动的时候被清除。

private static File getDexDir(Context context, File dataDir, String secondaryFolderName)throws IOException {// private static final String CODE_CACHE_NAME = "code_cache";//优先获取/data/data/<package>/code-cache作为缓存目录。File cache = new File(dataDir, CODE_CACHE_NAME);try {mkdirChecked(cache);} catch (IOException e) {//使用/data/data/<package>/files/code-cache目录cache = new File(context.getFilesDir(), CODE_CACHE_NAME);mkdirChecked(cache);}File dexDir = new File(cache, secondaryFolderName);mkdirChecked(dexDir);return dexDir;
}

回到doInstallation继续看注释4通过MultiDexExtractor提取缓存文件,然后注释5安装提取的dex。这两个步骤是整个MultiDex.install(Context)的过程中最重要的。接下来看看MultiDexExtractor

MultiDexExtractor.load

获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。

    List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)throws IOException {Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +prefsKeyPrefix + ")");if (!cacheLock.isValid()) {throw new IllegalStateException("MultiDexExtractor was closed");}List<ExtractedDex> files;//是否是强制性提取或者源文件(覆盖安装)发生了变化if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {try {//加载之前已经提取过的dex文件files = loadExistingExtractions(context, prefsKeyPrefix);} catch (IOException ioe) {Log.w(TAG, "Failed to reload existing extracted secondary dex files,"+ " falling back to fresh extraction", ioe);//加载失败后执行真正的提取过程,然后保存dex文件信息files = performExtractions();putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,files);}} else {//重新解压,并保存解压出来的dex文件的信息if (forceReload) {Log.i(TAG, "Forced extraction must be performed.");} else {Log.i(TAG, "Detected that extraction must be performed.");}files = performExtractions();putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,files);}Log.i(TAG, "load found " + files.size() + " secondary dex files");return files;}

主要是先加载之前已经提取过的dex文件loadExistingExtractions,如果加载失败后执行真正的提取过程performExtractions,然后保存dex文件信息putStoredApkInfo

MultiDexExtractor.loadExistingExtractions

private List<MultiDexExtractor.ExtractedDex> loadExistingExtractions(Context context, String prefsKeyPrefix) throws IOException {Log.i("MultiDex", "loading existing secondary dex files");String extractedFilePrefix = this.sourceApk.getName() + ".classes";SharedPreferences multiDexPreferences = getMultiDexPreferences(context);int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + "dex.number", 1);List<MultiDexExtractor.ExtractedDex> files = new ArrayList(totalDexNumber - 1);for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {String fileName = extractedFilePrefix + secondaryNumber + ".zip";MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);if (!extractedFile.isFile()) {throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");}extractedFile.crc = getZipCrc(extractedFile);long expectedCrc = multiDexPreferences.getLong(prefsKeyPrefix + "dex.crc." + secondaryNumber, -1L);long expectedModTime = multiDexPreferences.getLong(prefsKeyPrefix + "dex.time." + secondaryNumber, -1L);long lastModified = extractedFile.lastModified();if (expectedModTime != lastModified || expectedCrc != extractedFile.crc) {throw new IOException("Invalid extracted dex: " + extractedFile + " (key \"" + prefsKeyPrefix + "\"), expected modification time: " + expectedModTime + ", modification time: " + lastModified + ", expected crc: " + expectedCrc + ", file crc: " + extractedFile.crc);}files.add(extractedFile);}return files;
}

从dexDir路径(/data/data/pkgName/code_cache/secondary-dexes)下提取如下zip文件列表:

/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip

/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes3.zip

……

/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classesN.zip

MultiDexExtractor.performExtractions

private List<ExtractedDex> performExtractions() throws IOException {//匹配的前缀,示例:com.nercita.mpcms-1.apk.classesfinal String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;// It is safe to fully clear the dex dir because we own the file lock so no other process is// extracting or running optimizing dexopt. It may cause crash of already running// applications if for whatever reason we end up extracting again over a valid extraction.clearDexDir();List<ExtractedDex> files = new ArrayList<ExtractedDex>();final ZipFile apk = new ZipFile(sourceApk);try {int secondaryNumber = 2;//从sourceApk的文件中找到classes2.dex…classesN.dex的ZipEntry入口,依次调用extract(apk, dexFile, extractedFile, extractedFilePrefix):ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);while (dexFile != null) {//com.nercita.mpcms-1.apk.classes2.zipString fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;//extractedFile路径为data/data/com.nercita.mpcms/code_cache/secondary-dexes/com.nercita.mpcms-1.apk.classes2.zipExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);files.add(extractedFile);Log.i(TAG, "Extraction is needed for file " + extractedFile);int numAttempts = 0;boolean isExtractionSuccessful = false;//每个dex的提取都尝试三次;while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {numAttempts++;// Create a zip file (extractedFile) containing only the secondary dex file// (dexFile) from the apk.//真正的提取。将源Apk解压,将非主Dex文件写为zip文件。extract(apk, dexFile, extractedFile, extractedFilePrefix);// Read zip crc of extracted dextry {extractedFile.crc = getZipCrc(extractedFile);isExtractionSuccessful = true;} catch (IOException e) {isExtractionSuccessful = false;Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);}// Log size and crc of the extracted zip fileLog.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")+ " '" + extractedFile.getAbsolutePath() + "': length "+ extractedFile.length() + " - crc: " + extractedFile.crc);if (!isExtractionSuccessful) {// Delete the extracted fileextractedFile.delete();if (extractedFile.exists()) {Log.w(TAG, "Failed to delete corrupted secondary dex '" +extractedFile.getPath() + "'");}}}if (!isExtractionSuccessful) {throw new IOException("Could not create zip file " +extractedFile.getAbsolutePath() + " for secondary dex (" +secondaryNumber + ")");}secondaryNumber++;dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);}} finally {try {apk.close();} catch (IOException e) {Log.w(TAG, "Failed to close resource", e);}}return files;
}

其中loadExistingExtractionsputStoredApkInfo方法是通过SharedPreferences读写dex文件的数目totalDexNumber,apk文件的crc值,修改时间。putStoredApkInfo后,下一次直接loadExistingExtractions就可以了

MultiDexExtractor.extract

private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,String extractedFilePrefix) throws IOException, FileNotFoundException {InputStream in = apk.getInputStream(dexFile);ZipOutputStream out = null;// Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir()File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,extractTo.getParentFile());Log.i(TAG, "Extracting " + tmp.getPath());try {out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));try {ZipEntry classesDex = new ZipEntry("classes.dex");// keep zip entry time since it is the criteria used by DalvikclassesDex.setTime(dexFile.getTime());out.putNextEntry(classesDex);byte[] buffer = new byte[BUFFER_SIZE];int length = in.read(buffer);while (length != -1) {out.write(buffer, 0, length);length = in.read(buffer);}out.closeEntry();} finally {out.close();}if (!tmp.setReadOnly()) {throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +"\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");}Log.i(TAG, "Renaming to " + extractTo.getPath());if (!tmp.renameTo(extractTo)) {throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +"\" to \"" + extractTo.getAbsolutePath() + "\"");}} finally {closeQuietly(in);tmp.delete(); // return status ignored}
}

参数如下:

  • apk: Apk文件/data/app/apkName.apk
  • dexFile: Apk文件zip解压后得到的从dex文件,classes2.dex…classesN.dex
  • extractedFile: dexFile写入的目标文件/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip等
  • extractedFilePrefix: 前缀apkName.apk.classes

将Apk文件解压后得到的classes2.dex, …, classesN.dex文件的内容依次写入到/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip, …, /data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classesN.zip压缩文件的classes.dex文件中。

MultiDex.installSecondaryDexes

 private static void installSecondaryDexes(ClassLoader loader, File dexDir,List<? extends File> files)throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,InvocationTargetException, NoSuchMethodException, IOException, SecurityException,ClassNotFoundException, InstantiationException {if (!files.isEmpty()) {if (Build.VERSION.SDK_INT >= 19) {V19.install(loader, files, dexDir);} else if (Build.VERSION.SDK_INT >= 14) {V14.install(loader, files);} else {V4.install(loader, files);}}}

在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同,所以这里做了V4/V14/V19的兼容(Magic Code)

V19.install

private static final class V19 {static void install(ClassLoader loader,List<? extends File> additionalClassPathEntries,File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,NoSuchFieldException, InvocationTargetException, NoSuchMethodException,IOException {/* 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的pathList字段Field pathListField = findField(loader, "pathList");Object dexPathList = pathListField.get(loader);ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();//扩展DexPathList中的dexElements数组字段;expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));if (suppressedExceptions.size() > 0) {for (IOException e : suppressedExceptions) {Log.w(TAG, "Exception in makeDexElement", e);}Field suppressedExceptionsField =findField(dexPathList, "dexElementsSuppressedExceptions");IOException[] dexElementsSuppressedExceptions =(IOException[]) suppressedExceptionsField.get(dexPathList);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(dexPathList, dexElementsSuppressedExceptions);IOException exception = new IOException("I/O exception during makeDexElement");exception.initCause(suppressedExceptions.get(0));throw exception;}}/*** A wrapper around* {@code private static final dalvik.system.DexPathList#makeDexElements}.*///将刚刚提取出来的zip文件包装成Element对象private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory,ArrayList<IOException> suppressedExceptions)throws IllegalAccessException, InvocationTargetException,NoSuchMethodException {Method makeDexElements =findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,ArrayList.class);return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,suppressedExceptions);}//合并到一个新的数组private static void expandFieldArray(Object instance, String fieldName,Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,IllegalAccessException {Field jlrField = findField(instance, fieldName);Object[] original = (Object[]) jlrField.get(instance);Object[] combined = (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);jlrField.set(instance, combined);}}
  1. 反射获取ClassLoader中的pathList字段;
  2. 反射调用DexPathList对象中的makeDexElements方法,将刚刚提取出来的zip文件包装成Element对象;
  3. 将包装成的Element对象扩展到DexPathList中的dexElements数组字段里;
  4. makeDexElements中有dexopt的操作,是一个耗时的过程,产物是一个优化过的odex文件

参考资料:

MultiDex工作原理分析和优化方案

Multidex(一)之源码解析

MultiDex的加载dex源码分析相关推荐

  1. DataX Transformer从入口到加载的源码分析及UDF扩展与使用

    DataX GitHub DataX Transformer 目录 1 前言 2 需求说明 3 解决方案分析 4 解密算法 5 Hive UDF 5.1 测试数据 5.2 新建 Maven 项目 5. ...

  2. YOLO3 数据处理与数据加载 Keras源码分析

    YOLO3 Keras 源码:https://github.com/qqwweee/keras-yolo3 前言 本文从主要是从源码层面对 YOLO3 的数据处理相关内容进行分析与讲解.通常,一个功能 ...

  3. AsyncTask异步加载的源码分析与实现实例

    一 . AsyncTask Android的Lazy Load主要体现在网络数据(图片)异步加载.数据库查询.复杂业务逻辑处理以及费时任务操作导致的异步处理等方面.在介绍Android开发过程中,异步 ...

  4. Android UIL图片加载缓存源码分析-内存缓存

    本篇文章我们来分析一下著名图片加载库Android-Universal-Image-Loader的图片缓存源码. 源码环境 版本:V1.9.5 GitHub链接地址:https://github.co ...

  5. Volley 图片加载相关源码解析

    转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/47721631: 本文出自:[张鸿洋的博客] 一 概述 最近在完善图片加载方面的 ...

  6. Android开发之WebView加载HTML源码包含转义字符实现富文本显示的方法

    老套路先看效果图: WebView加载带有转移字符的HTML源码 再看转义后的字符的效果图: 先看WebView加载HTML源码的方法如下: webview.loadDataWithBaseURL(n ...

  7. FPGA - Zynq - 加载 - FSBL源码解析1

    FPGA - Zynq - 加载 - FSBL源码解析1 前文回顾 FSBL的数据段和代码段如何链接 建个Example工程,不要光顾着看,自己动动手掌握的更快. 查看链接文件,原来存储空间是这样有条 ...

  8. 未能加载文件或程序集rsy3_abp vnext2.0之核心组件模块加载系统源码解析

    abp vnext是abp官方在abp的基础之上构建的微服务架构,说实话,看完核心组件源码的时候,很兴奋,整个框架将组件化的细想运用的很好,真的超级解耦.老版整个框架依赖Castle的问题,vnext ...

  9. abp vnext2.0之核心组件模块加载系统源码解析

    abp vnext是abp官方在abp的基础之上构建的微服务架构,说实话,看完核心组件源码的时候,很兴奋,整个框架将组件化的细想运用的很好,真的超级解耦.老版整个框架依赖Castle的问题,vnext ...

最新文章

  1. hdu4560 不错的建图,二分最大流
  2. MySQL查询指定字段
  3. centos selinux_如何临时或永久地禁用SELinux
  4. linux操作命令comm,Linux
  5. 红帽Openshift:入门–云中的Java EE6
  6. Confluence 6 workbox 通知包含了什么
  7. php获取手机的mac地址,Android手机获取Mac地址的方法
  8. MongoTemplate 使用aggregate聚合查询
  9. 从1.5k到18k, 一个程序员的5年成长之路 2019-03-15
  10. python删除csv某一行_用Python一步从csv中删除特定的行和列
  11. 【Flink】Flink Elasticsearch client is not connected to any Elasticsearch nodes
  12. 用Java搭建一套访问redis的API
  13. Failed to start Zabbix Agent.
  14. 重构代码 —— 提取出类
  15. java第三方类库实现图片等比缩放
  16. 常用软件版本号及软件安装包格式
  17. 从零实现GPT-2,瞎写笑傲江湖外传,金庸直呼内行
  18. 光年SEO日志分析系统2.0
  19. linux如何切换到设备,如何编写Linux设备驱动程序(转)
  20. 各种通信铁塔和机房类型介绍,别再傻傻分不清了

热门文章

  1. 物联lot是什么意思_什么是NB-loT物联网技术,这里带你看懂
  2. 我们读过的培生经典书系
  3. 彻底理解mmap()
  4. QEMU/KVM虚拟机Win11黑屏问题解决
  5. Proxyee-down的下载与安装教程
  6. Monotonic Matrix (LGV)
  7. 台式计算机如何定时关机,台式电脑如何设置定时关机
  8. Ubuntu 18.04 安装后的主题美化与软件安装
  9. html dt dd dl英文,dl dt dd是什么的缩写
  10. 自考-知识点总结-数据库系统原理 04735