MultiDex的加载dex源码分析

工作流程

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-dexes
            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 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安装提取的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;
            }
        }
    }

在注释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.classes
    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;
        //从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.zip
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            //extractedFile路径为data/data/com.nercita.mpcms/code_cache/secondary-dexes/com.nercita.mpcms-1.apk.classes2.zip
            ExtractedDex 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 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;
}

其中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 Dalvik
            classesDex.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(一)之源码解析

------ 本文结束 ------
坚持原创技术分享,您的支持将鼓励我继续创作!