MultiDex原理

MultiDex原理

分包机制对于 Android 5 以下的手机耗时更长

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");
        }
}

可以发现,5.0以下的手机不支持加载多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 {
    //获取非主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);
       } 
    }
}

需要获取额外的非主dex文件,由MultiDexExtractor获取

List 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文件时,有两个逻辑,一个是初次加载或者缓存读取失败调用performExtractions方法,缓存成功则调用loadExistingExtractions方法。实际上使用缓存的performExtractions方法是很耗时的

private List performExtractions() throws IOException {
    String extractedFilePrefix = this.sourceApk.getName() + ".classes";
    this.clearDexDir();
    List 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文件,把遍历得到的又压缩成zip文件,最后返回一个zip文件列表

只有第一个加载的时候才会调用上面这个解压和压缩的过程,第二次就可以读取sp中保存的dex信息,直接返回zip文件列表,就会直接调用installSecondaryDexes方法进行安装

安装的实现:
1.反射 ClassLoader 的 pathList 字段

2.找到 pathList 字段对应的类的 makeDexElements 方法

3.通过 MulitDex.expandFiledArray 方法扩展 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); //dex2、dex3...拷贝到新的数组
        jlrField.set(instance, combined); //将dexElements 重新赋值为新的数组
    }

就是创建一个新的数组,把原来数组内容(主dex)和要增加的内容(dex2、dex3...)拷贝进去,反射替换原来的dexElements为新的数组
MultiDex原理_第1张图片

Tinker热修复的原理也是通过反射将修复后的dex添加到这个dex数组去,不同的是热修复是添加到数组最前面,而MultiDex是添加到数组后面。

对于 MultiDex 在 5.0 以下的手机耗时的问题,今日头条给了一个优化方案

  1. 在主进程Application 的 attachBaseContext 方法中判断如果需要使用MultiDex,则创建一个临时文件,然后开一个进程(LoadDexActivity),显示Loading,异步执行MultiDex.install 逻辑,执行完就删除临时文件并finish自己。

  2. 主进程Application 的 attachBaseContext 进入while代码块,定时轮循临时文件是否被删除,如果被删除,说明MultiDex已经执行完,则跳出循环,继续正常的应用启动流程。

  3. MultiDex执行完之后主进程Application继续走,ContentProvider初始化和Application onCreate方法,也就是执行主进程正常的逻辑。

注意:LoadDexActivity 必须要配置在main dex中。

你可能感兴趣的:(MultiDex原理)