MultiDex 源码分析

      Multidex 是为了解决应用程序函数数超过65k而出现的,主要是通过打包时把函数拆分到多个 dex 文件,并在程序启动的时候再动态的读入的方式来解决问题, 详细的描述和 Multidex 的使用方式可以参考官网的这篇文章 https://developer.android.com/tools/building/multidex.html。

      本文从源码的角度看看 Multidex 究竟做了什么,  整个项目只有 4 个 java 文件。

MultiDex 源码分析_第1张图片

使用 Multidex 的时候, 只需要直接继承 MultiDexApplication :
 
   
public class MultiDexApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }
}
在 attachBaseContext 中直接调用了 MultiDex.install。   attachBaseContext 这个函数, 是在应用程序启动过程中最早被系统触发到的可以做点事情的回调, MultiDex 的逻辑写在这里, 才能保证在应用程序启动之前把所有的 dex 全部读入,保证程序完整。

ZipUtil 里面是几个 crc32 校验的工具函数, 不是重点, 略过、


      那重点就是 MultiDex.java 和 MultiDexExtractor.java 这两个文件。

      MultiDex.install 首先是做了 两件事情, 一个是检查你当前的虚拟机是不是原生就支持 MultiDex , 如果自带支持的就直接返回了,虚拟机自己搞定; 另外是检查系统版本,小于 4 的不支持, 直接返回。 大于20  会抛个警告给你看看, 但是程序还接着跑。

注:有的童鞋可能不太清楚,Android 4.4 引入了 ART 运行时,而 ART 原生就是支持 MultiDex 的, 所以这里先判断一下设备是不是 ART 运行时,如果不是才有必要执行后续的程序。  至于如何判断出来当前的运行时版本, 官网有这么一段说明:
You can verify which runtime is in use by calling System.getProperty("java.vm.version") . If ART is in use, the property's value is  "2.0.0"  or higher.  
来源:  https://developer.android.com/guide/practices/verifying-apps-art.html


     一通检查过后,来到了 MultiDex 的核心代码:
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
List files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
if (checkValidZipFiles(files)) {
    installSecondaryDexes(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.
    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);

    if (checkValidZipFiles(files)) {
        installSecondaryDexes(loader, dexDir, files);
    } else {
        // Second time didn't work, give up
        throw new RuntimeException("Zip files were not valid.");
    }
}
做了两件事:
(1) 借助 MultiDexExtractor.load 读取了一个 List
(2) 把上面获取到的 List, 连同当前使用的 ClassLoader ,一起传入 installSecondaryDexes

下面就分别看下 这两步核心的代码:
一、MultiDexExtractor.load
 
    
List files;
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
    try {
        files = loadExistingExtractions(context, sourceApk, dexDir);
    } catch (IOException ioe) {
        Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
                + " falling back to fresh extraction", ioe);
        files = performExtractions(sourceApk, dexDir);
        putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);

    }
} else {
    Log.i(TAG, "Detected that extraction must be performed.");
    files = performExtractions(sourceApk, dexDir);
    putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}

这里是两个分支, 一个是读取已经解压出来的包含 dex 的压缩文件-loadExistingExtractions ,另一个是现在去解压 - performExtractions 
(1) performExtractions 
直接把 apk 文件作为 ZipFile 打开, 遍历找到里面名字是 “classesX.dex” ( X = 数字 ) 的文件 , 重新创建一个 Zip 压缩包, 并把这个 dex 文件改名为 ”classes.dex” 再存到新的 zip 包里, 就是把名字里面的数字扣掉了, 同时, 新创建 出来的, 只包含一个 classes.dex 文件的压缩包, 会被添加到一个 List 里面, 最后返回。

(2)loadExistingExtractions 
这个就简单多了, 直接去指定的目录里面, 把已经解压好的、只包含一个 classes.dex 文件的 zip 纪录到一个 List 中, 然后返回。

到这里, 第一部 load 就完成了, 现在拿到了一个 List 集合, 每一个 File 是 一个 zip 包,包中只有一个从当前 apk 文件中提取出来的 classes.dex 文件。 


二、installSecondaryDexes
这个代码结构在 Android 源码的 XxxCompat 中经常出现,很经典, 一定要贴出来看看、、
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files)
        throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
        InvocationTargetException, NoSuchMethodException, IOException {
    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, dexDir);
        } else {
            V4.install(loader, files);
        }
    }
}

这里就是最后执行 install 操作的入口了, 从代码上就能看出来, 在 api 14 和 19 这两个版本中,官方对 dex 加载的地方做了一些调整, 所以根据不同的版本做分别实现,挑个老的下手吧,看下 V4.install 

V4.install, 函数很简单, 总共没几行我就全部贴出来了

private static final class V4 {
    private static void install(ClassLoader loader, List additionalClassPathEntries)
                    throws IllegalArgumentException, IllegalAccessException,
                    NoSuchFieldException, IOException {
        /* The patched class loader is expected to be a descendant of
         * dalvik.system.DexClassLoader. We modify its
         * fields mPaths, mFiles, mZips and mDexs to append additional DEX
         * file entries.
         */
        int extraSize = additionalClassPathEntries.size();

        Field pathField = findField(loader, "path");

        StringBuilder path = new StringBuilder((String) pathField.get(loader));
        String[] extraPaths = new String[extraSize];
        File[] extraFiles = new File[extraSize];
        ZipFile[] extraZips = new ZipFile[extraSize];
        DexFile[] extraDexs = new DexFile[extraSize];
        for (ListIterator iterator = additionalClassPathEntries.listIterator();
                iterator.hasNext();) {
            File additionalEntry = iterator.next();
            String entryPath = additionalEntry.getAbsolutePath();
            path.append(':').append(entryPath);
            int index = iterator.previousIndex();
            extraPaths[index] = entryPath;
            extraFiles[index] = additionalEntry;
            extraZips[index] = new ZipFile(additionalEntry);
            extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
        }

        pathField.set(loader, path.toString());
        expandFieldArray(loader, "mPaths", extraPaths);
        expandFieldArray(loader, "mFiles", extraFiles);
        expandFieldArray(loader, "mZips", extraZips);
        expandFieldArray(loader, "mDexs", extraDexs);
    }
}

可以看到这里只干了一件事, 通过反射, 拿到 classloader 中的 “path” 属性, 然后把上一步 拿到的 List 中 File 的路径全部拼到”path”里面, 中间用 冒号分割。
“path” 里面添加了内容以后,相应的 “mPaths”, “mFiles”, “mZips” 和 “mDexs” 这几个数组属性的内容也要与 “path”对应, expandFieldArray 这个函数就是通过反射 获取上述几个属性, 构造出所需的数据结构, 然后塞到各个 属性数组中,到这里 MultiDex 的所有工作就结束了。

注: V14  和  V19 中做的事情跟 V4 都差不多, 就是属性变了变名字, 构造数组直接使用了新版本中添加的函数,这里就不再赘述了。


总结
     一个首次安装的程序启动时,MultiDex 的整个工作过程, 概括起来就是两句话:
1、把 apk 中的 classesX.dex 文件解压出来, 单独压缩到独立的 zip 压缩包中, 并把这些压缩包的路径包装在一个 List 中返回。
2、通过反射, 修改 classloader 中的 “path” 属性(新版中是“pathList”),把上一步的到的所有 zip 的路径拼进去。




你可能感兴趣的:(MultiDex 源码分析)