MultiDex工作原理分析

530 阅读5分钟

工作流程

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

所以我们需要关心的是第二部分,这个过程的简单示意流程图如下。

MultiDex

MultiDex的入口是MultiDex.install(Context)

install()

private static final int MIN_SDK_VERSION = 4;

private static final boolean IS_VM_MULTIDEX_CAPABLE =
            isVMMultidexCapable(System.getProperty("java.vm.version"));
            
public static void install(Context context) {
        Log.i(TAG, "Installing application");
        //判断是否需要执行MULTIDEX
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
            return;
        }
        //sdk<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 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");
    }

doInstallation()

private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
                                   String secondaryFolderName, String prefsKeyPrefix,
                                   boolean reinstallOnPatchRecoverableException) throws IOExc
        IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
        InvocationTargetException, NoSuchMethodException, SecurityException,
        ClassNotFoundException, InstantiationException {
    synchronized (installedApk) {
        if (installedApk.contains(sourceApk)) {
            return;
        }
        installedApk.add(sourceApk);
        //如果当前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") + "\"");
        }
        /* The patched class loader is expected to be a ClassLoader capable of loading DEX
         * bytecode. We modify its pathList field to append additional DEX file entries.
         */
         //获取当前的ClassLoader实例,后面要做的工作,就是把其他dex文件加载后
         //把其DexFile对象添加到这个ClassLoader实例里就完事了。
        ClassLoader loader = getDexClassloader(mainContext);
        if (loader == null) {
            return;
        }
        try {
        // 清除旧的dex文件,注意这里不是清除上次加载的dex文件缓存。
              // 获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录。
              // 如果获取失败,则使用/data/data/<package>/files/code-cache目录。
              // 这里清除的是第二个目录。
          clearOldDexDir(mainContext);
        } catch (Throwable t) {
          Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
              + "continuing without cleaning.", t);
        }
        //获取缓存目录(/data/data/<package>/code-cache)。
        File 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 
        // extraction or optimizing dexopt is running in parallel.
        //创建MultiDexExtractor
        MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
        IOException closeException = null;
        try {
            //加载文件
            List<? extends File> files =
                    extractor.load(mainContext, prefsKeyPrefix, false);
            try {
            	//安装dex
                installSecondaryDexes(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;
        }
    }
}

具体代码的分析已经在上面代码的注释里给出了,从这里我们也可以看出,整个MultiDex.install(Context)的过程中,关键的步骤就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。

MultiDexExtractor

MultiDexExtractor的构造函数

MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
    Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
    this.sourceApk = sourceApk;
    this.dexDir = dexDir;
   //获取当前apk文件的crc值
    sourceCrc = getZipCrc(sourceApk);
    File lockFile = new File(dexDir, LOCK_FILENAME);
    lockRaf = new RandomAccessFile(lockFile, "rw");
    try {
        lockChannel = lockRaf.getChannel();
        try {
            Log.i(TAG, "Blocking on lock " + lockFile.getPath());
            //加上文件锁,防止多进程冲突
            cacheLock = lockChannel.lock();
        } catch (IOException | RuntimeException | Error e) {
            closeQuietly(lockChannel);
            throw e;
        }
        Log.i(TAG, lockFile.getPath() + " locked");
    } catch (IOException | RuntimeException | Error e) {
        closeQuietly(lockRaf);
        throw e;
    }
}

load()

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;
      // 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。
       // 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件。
    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 {
        if (forceReload) {
            Log.i(TAG, "Forced extraction must be performed.");
        } else {
            Log.i(TAG, "Detected that extraction must be performed.");
        }
        //重新解压,并保存解压出来的dex文件的信息。
        files = performExtractions();
        putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                files);
    }
    Log.i(TAG, "load found " + files.size() + " secondary dex files");
    return files;
}

performExtractions()

performExtractions方法负责抽取dex


    private List<ExtractedDex> performExtractions() throws IOException {
       //文件前缀
        final 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;

            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            while (dexFile != null) {
                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
                ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
                files.add(extractedFile);

                Log.i(TAG, "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                    numAttempts++;

                    // Create a zip file (extractedFile) containing only the secondary dex file
                    // (dexFile) from the apk.
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);

                    // Read zip crc of extracted dex
                    try {
                        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 file
                    Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
                            + " '" + extractedFile.getAbsolutePath() + "': length "
                            + extractedFile.length() + " - crc: " + extractedFile.crc);
                    if (!isExtractionSuccessful) {
                        // Delete the extracted file
                        extractedFile.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;
    }

这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。需要注意的时,如果是重新解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。

如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。

无论是通过使用缓存的dex文件,还是重新从apk中解压dex文件,获取dex文件列表后,下一步就是安装(或者说加载)这些dex文件了。最后的工作在MultiDex#installSecondaryDexes这个方法里面。

Dex安装

installSecondaryDexes()

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

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

V19与V14差别不大,只不过DexPathList#makeDexElements方法多了一个ArrayList参数,如果在执行DexPathList#makeDexElements方法的过程中出现异常,后面使用反射的方式把这些异常记录进DexPathList的dexElementsSuppressedExceptions字段里面。这里我们分析下V19的install方法。


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>();
    //makeDexElements方法反射获取DexPathList#makeDexElement
    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;
    }
}

makeDexElements()

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

expandFieldArray()

expandFieldArray负责为dexElements设置新的数组

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

无论是V4/V14还是V19,在创建DexFile对象的时候,都需要通过DexFile的Native方法openDexFile来打开dex文件,其具体细节暂不讨论(涉及到dex的文件结构,很烦,有兴趣请阅读dalvik_system_DexFile.cpp),这个过程的主要目的是给当前的dex文件做Optimize优化处理并生成相同文件名的odex文件,App实际加载类的时候,都是通过odex文件进行的。因为每个设备对odex格式的要求都不一样,所以这个优化的操作只能放在安装Apk的时候处理,主dex的优化我们已经在安装apk的时候搞定了,其余的dex就是在MultiDex#installSecondaryDexes里面优化的,而后者也是MultiDex过程中,另外一个耗时比较多的操作。(在MultiDex中,提取出来的dex文件被压缩成.zip文件,又优化后的odex文件则被保存为.dex文件。)

参考