Android 应用 (APK) 文件包含 Dalvik Executable (DEX) 文件形式的可执行字节码文件,这些文件包含用来运行应用已编译代码。Dalvik Executable 规范将可在单个 DEX 文件内引用的方法总数限制为 65,536,其中包括 Android 框架方法、库方法以及自己的代码中的方法,即“64K 引用限制”。

为什么引用MultiDex

由于Android 5.0(API 级别 21)之前的平台版本使用 Dalvik 运行时来执行应用代码。默认情况下,Dalvik 将应用限制为每个 APK 只能使用一个 classes.dex 字节码文件。不可避免地,当方法数超过64K限制时,编译就会报错,因此google通过多 dex 文件支持库来解决这个问题,即MultiDex。而Android 5.0(API 级别 21)及更高版本使用名为 ART 的运行时,它本身支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,扫描 classesN.dex 文件,并将它们编译成单个 .oat 文件,以供 Android 设备执行,因此MultiDex仅针对5.0以下的Android系统。

MultiDex分析

根据MultiDex的用法MultiDex.install()作为切入点开始分析,下面直接看install()方法

public static void install(Context context) {Log.i("MultiDex", "Installing application");if (IS_VM_MULTIDEX_CAPABLE) { //5.0以上,忽略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 {try {ApplicationInfo applicationInfo = getApplicationInfo(context);if (applicationInfo == null) {Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");return;}//执行dex安装doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);} catch (Exception var2) {Log.e("MultiDex", "MultiDex installation failure", var2);throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");}Log.i("MultiDex", "install done");}}

install里主要做了一些判断,判断VM是否支持多dex,如果已经支持,则忽略,否则执行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)) {...if (loader == null) {Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");} else {try {clearOldDexDir(mainContext);} catch (Throwable var24) {Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);}//创建文件夹/data/data/xxx/cache-code/secondary-dexesFile dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);IOException closeException = null;try {//用来提取apk文件里的dex文件List files = extractor.load(mainContext, prefsKeyPrefix, false);try {installSecondaryDexes(loader, dexDir, files);} catch (IOException var26) {if (!reinstallOnPatchRecoverableException) {throw var26;}Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);files = extractor.load(mainContext, prefsKeyPrefix, true);installSecondaryDexes(loader, dexDir, files);}...

通过MultiDexExtractor来提取APK文件里额外的Dex文件,方法如下

List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {...List files;//判断是否需要重新提取dexif (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {try {//加载已经提取了的dexfiles = 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文件提取files = this.performExtractions();putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);}} else {...//执行dex文件提取files = this.performExtractions();//把提取信息保存到sp,用来判断是否需要重新提取putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);}Log.i("MultiDex", "load found " + files.size() + " secondary dex files");return files;}}

Dex文件的提取先判断是否已经提取过,如果没有则执行提取,否则直接加载,先看提取方法

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {String extractedFilePrefix = this.sourceApk.getName() + ".classes";this.clearDexDir();List<MultiDexExtractor.ExtractedDex> files = new ArrayList();//解压/data/app/xxxx/xxx.apk文件ZipFile apk = new ZipFile(this.sourceApk);try {//额外的dex文件从编号2开始,即classes2.dexint secondaryNumber = 2;//获取classesX.dexfor(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {//创建xxx.apk.classesX.zip并把classesX.dex压缩进来String fileName = extractedFilePrefix + secondaryNumber + ".zip";MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);files.add(extractedFile);Log.i("MultiDex", "Extraction is needed for file " + extractedFile);int numAttempts = 0;boolean isExtractionSuccessful = false;//重试3次while(numAttempts < 3 && !isExtractionSuccessful) {++numAttempts;//执行压缩,把classesX.dex提取到base.apk.classesX.zipextract(apk, dexFile, extractedFile, extractedFilePrefix);...}++secondaryNumber;}} ...return files;}//具体提取过程,文件读写private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {InputStream in = apk.getInputStream(dexFile);ZipOutputStream out = null;//创建一个临时压缩文件tmp-xxx.apk.classesxxxx.zipFile tmp = File.createTempFile("tmp-" + extractedFilePrefix, ".zip", extractTo.getParentFile());Log.i("MultiDex", "Extracting " + tmp.getPath());try {out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));try {//把dex文件重命名为classes.dexZipEntry classesDex = new ZipEntry("classes.dex");classesDex.setTime(dexFile.getTime());out.putNextEntry(classesDex);byte[] buffer = new byte[16384];for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {out.write(buffer, 0, length);}...//temp压缩文件写入指定的压缩文件如xxx.apk.classes2.zipif (!tmp.renameTo(extractTo)) {throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");}...}

提取过程是对apk文件解压获取里面的dex文件如classes2.dex并把它压缩到/data/data/xxx/cache-code/secondary-dexes目录下,如xxx.apk.classes2.zip,每个zip下都有一个classes.dex,即要合并进来的额外的dex。提取完后,把提取信息保存下来,供下次使用,方法如下:

private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp, long crc, List<MultiDexExtractor.ExtractedDex> extractedDexes) {SharedPreferences prefs = getMultiDexPreferences(context);Editor edit = prefs.edit();edit.putLong(keyPrefix + "timestamp", timeStamp);  //用来校验edit.putLong(keyPrefix + "crc", crc);      //用来校验edit.putInt(keyPrefix + "dex.number", extractedDexes.size() + 1);  //这里就是dex的个数int extractedDexId = 2;for(Iterator var10 = extractedDexes.iterator(); var10.hasNext(); ++extractedDexId) {MultiDexExtractor.ExtractedDex dex = (MultiDexExtractor.ExtractedDex)var10.next();edit.putLong(keyPrefix + "dex.crc." + extractedDexId, dex.crc);edit.putLong(keyPrefix + "dex.time." + extractedDexId, dex.lastModified());}edit.commit();}

通过保持的信息,判断下次启动是否存在已经提取的dex(通过方法isModified()),如果存在,则直接加载即可,即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);  //执行提取后保存的信息,即dex个数List<MultiDexExtractor.ExtractedDex> files = new ArrayList(totalDexNumber - 1);for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {String fileName = extractedFilePrefix + secondaryNumber + ".zip";//ExtractedDex是一个文件MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);...//直接添加进来files.add(extractedFile);}return files;}

至此,提取dex文件的过程就分析完了,下面开始分析如何合并这些dex文件

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的过程,返回的是ExtractedDex,一个zip文件List files = extractor.load(mainContext, prefsKeyPrefix, false);...//开始合并dexinstallSecondaryDexes(loader, dexDir, files);...
}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 (VERSION.SDK_INT >= 19) { //不同版本下的实现不同MultiDex.V19.install(loader, files, dexDir);} else if (VERSION.SDK_INT >= 14) {MultiDex.V14.install(loader, files);} else {MultiDex.V4.install(loader, files);}}}
//合并的具体实现 SDK_INT >= 19
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {Field pathListField = MultiDex.findField(loader, "pathList");  //反射获取ClassLoader下的pathList对象Object dexPathList = pathListField.get(loader);    ...}

看到这里,先找一下ClassLoader的pathList对象(因为AS里不能直接查看,这里通过Android社区 查看源码)。ClassLoader有三个子类PathClassLoader、DexClassLoader和BaseDexClassLoader,这里直接找PathClassLoader跟BaseDexClassLoader即可,因为传进来的是PathClassLoader。发现在BaseDexClassLoader里找到了

public class BaseDexClassLoader extends ClassLoader {private final DexPathList pathList;...

pathList是一个DexPathList对象

final class DexPathList {private static final String DEX_SUFFIX = ".dex";private static final String JAR_SUFFIX = ".jar";private static final String ZIP_SUFFIX = ".zip";private static final String APK_SUFFIX = ".apk";/** class definition context */private final ClassLoader definingContext;/*** List of dex/resource (class path) elements.* Should be called pathElements, but the Facebook app uses reflection* to modify 'dexElements' (http://b/7726934).*/private final Element[] dexElements;

继续往下分析

 static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {Field pathListField = MultiDex.findField(loader, "pathList");Object dexPathList = pathListField.get(loader);  //拿到classloader里的pathList对象//执行合并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 {//反射调用dexPathList里makeDexElements()方法获取ElementsMethod makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);return (Object[])((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);  //反射获取dexPathList对象里的dexElements数组Object[] original = (Object[])((Object[])jlrField.get(instance));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);jlrField.set(instance, combined);}

通过上面代码可知,expandFieldArray()的作用是把dexPathList对象里的dexElements数组进行了扩展,原则是把原来的Element放前面,extraElements放后面,extraElements是通过makeDexElements()方法获取,实际是通过反射调用dexPathList里makeDexElements(),下面分析具体实现

 /*** Makes an array of dex/resource path elements, one per element of* the given array.*/private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,ArrayList<IOException> suppressedExceptions) {ArrayList<Element> elements = new ArrayList<Element>();/** Open all files and load the (direct or contained) dex files* up front.*/for (File file : files) {File zip = null;DexFile dex = null;String name = file.getName();if (name.endsWith(DEX_SUFFIX)) {    //.dex文件// Raw dex file (not inside a zip/jar).....//生成DexFiledex = loadDexFile(file, optimizedDirectory);....} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)  //.apk .jar .zip文件|| name.endsWith(ZIP_SUFFIX)) {zip = file;...//生成DexFiledex = loadDexFile(file, optimizedDirectory);...}}if ((zip != null) || (dex != null)) {elements.add(new Element(file, false, zip, dex));}}return elements.toArray(new Element[elements.size()]);}private static DexFile loadDexFile(File file, File optimizedDirectory)throws IOException {if (optimizedDirectory == null) {  //优化后的dex目录,api 19以上不为空return new DexFile(file);} else {String optimizedPath = optimizedPathFor(file, optimizedDirectory);return DexFile.loadDex(file.getPath(), optimizedPath, 0);}}
//DexFile
/** @param sourcePathName*  Jar or APK file with "classes.dex".  (May expand this to include*  "raw DEX" in the future.)* /
static public DexFile loadDex(String sourcePathName, String outputPathName,int flags) throws IOException {/** TODO: we may want to cache previously-opened DexFile objects.* The cache would be synchronized with close().  This would help* us avoid mapping the same DEX more than once when an app* decided to open it multiple times.  In practice this may not* be a real issue.*/return new DexFile(sourcePathName, outputPathName, flags);}

makeDexElements()用来创建Element,而Element创建需要DexFile,DexFile要么通过new DexFile(sourcePathName),要么通过DexFile.loadDex(sourcePathName, outputPathName, flags),两种方式都是new出来的,只不过传入的参数不同。sourcePathName可以是包含了classes.dex的jar/apk/zip文件,即上面通过MultiDexExtractor.load提取出来的zip文件。

优化方向

Multidex耗时在于要从apk文件里提取dex,然后压缩,再通过反射的方式把DexFile加入到dexPathList列表后面,但是提取出dex文件也是可以直接使用的,也就是说不用再次压缩。所以优化方向可以提取dex然后直接反射,跳过压缩的步骤,应该可行,但是还没实践过。

官方文档

MultiDex.install()源码分析相关推荐

  1. Android MultiDex 源码分析

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一.启用 MultiDex Android 5.0 和之后的版本 Android 5.0 之前的版本 二.MultiD ...

  2. MultiDex的加载dex源码分析

    工作流程 MultiDex的工作流程具体分为两个部分,一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 "mult ...

  3. 【Android 安全】DEX 加密 ( 多 DEX 加载 | 65535 方法数限制和 MultiDex 配置 | PathClassLoader 类加载源码分析 | DexPathList )

    文章目录 一.65535 方法数限制和 MultiDex 配置 二.多 DEX 加载引入 三.PathClassLoader 类加载源码分析 四.BaseDexClassLoader 类加载源码分析 ...

  4. JDK源码分析 NIO实现

    总列表:http://hg.openjdk.java.net/ 小版本:http://hg.openjdk.java.net/jdk8u jdk:http://hg.openjdk.java.net/ ...

  5. golang源码分析-启动过程概述

    golang源码分析-启动过程概述 golang语言作为根据CSP模型实现的一种强类型的语言,本文主要就是通过简单的实例来分析一下golang语言的启动流程,为深入了解与学习做铺垫. golang代码 ...

  6. kazoo源码分析:Zookeeper客户端start概述

    kazoo源码分析 kazoo-2.6.1 kazoo客户端 kazoo是一个由Python编写的zookeeper客户端,实现了zookeeper协议,从而提供了Python与zookeeper服务 ...

  7. celery源码分析-worker初始化分析(下)

    celery源码分析 本文环境python3.5.2,celery4.0.2,django1.10.x系列 celery的worker启动 在上文中分析到了Hub类的初始化,接下来继续分析Pool类的 ...

  8. 《深入理解Spark:核心思想与源码分析》——1.2节Spark初体验

    本节书摘来自华章社区<深入理解Spark:核心思想与源码分析>一书中的第1章,第1.2节Spark初体验,作者耿嘉安,更多章节内容可以访问云栖社区"华章社区"公众号查看 ...

  9. 【源码分析】极验验证官方SDK源码分析和实现思路

    前言 2016年就这么来了,新的一年,继续努力~ 最近,除了12306的验证码火起来以后,还有一个在界面上拖拽的验证码,也火了起来,就是这次要说的极验验证,在这个万众创新的时代,工具类产品能做到这样, ...

最新文章

  1. (JAVA学习笔记) 异常处理
  2. R语言 文本挖掘 tm包 使用
  3. 实现模糊查询并忽略大小写
  4. SpringSession+redis解决分布式session不一致性问题
  5. Python中面向对象初识到进阶
  6. 实验任务四:实现登陆界面
  7. 2012-2013QS计算机专业世界大学排名
  8. 期权定价_强化学习的期权定价
  9. 毕业设计__系友录ByJavaweb
  10. C程序逆向破解-实战WinRAR去广告(3)
  11. QGIS空间数据分析——空间数据基本处理与计算
  12. 微信js 已经填写JS接口安全域名了,仍然报invalid url domain
  13. 安全防御----防火墙
  14. 基于凸松弛算法的电力市场策略研究(Matlab代码实现)
  15. Linux 显示文件内行号显示
  16. 光遇显示服务器已满怎么办,sky光遇服务器已满怎么办_sky光遇服务器已满解决方法介绍-星芒手游网...
  17. dlink交换机(DLINK交换机灯)
  18. ‘数据分析实战’——营销组合分析(甲厨电公司案例)
  19. 电磁场理论笔记04:静电场的标量位
  20. 深度学习相关公开数据集

热门文章

  1. C++perror函数
  2. CSS3选择器:nth-child和:nth-of-type之间的差异——张鑫旭
  3. IT销售之道和做生意十忌
  4. Intel芯片组和AMD命名规则
  5. Ubuntu16.04离线安装SSH
  6. 4.1 向量空间与子空间
  7. 关于IDEA创建空白项目和文件夹会自动折叠的问题
  8. JProfile 分析OOM hprof文件
  9. 20世纪合成的灵异旧照(组图)
  10. scratch踢足球 电子学会图形化编程scratch等级考试一级真题和答案解析2022年9月