Multidex记录三:源码解析

897 阅读7分钟

个人博客地址 http://dandanlove.com/

Multidex记录一:介绍和使用
Multidex记录二:缺陷&解决
Multidex记录三:源码解析

记录Multidex源码解析

为什么要用记录呢,因为我从开始接触Android时我们的项目就在65535的边缘。不久Google就出了multidex的解决方案。我们也已经接入multidex好多年,但我自己还没有接入,知其然而不知其所以然。出了问题只能自己找源码来分析。前两篇文章 Multidex记录一:介绍和使用Multidex记录二:缺陷&解决 分别讲述了怎么接入和接入时遇到的问题,本博文只是对multidex源码学习过程中的分析和理解的记录。

关于Multidex的相关知识点前两章已经讲的差不多了,这篇文章只分析Multidex的安装。

流程图

multidex-flowchart.png

源码分析

我们先来看看MultiDex的安装日志:

I/MultiDex: VM with version 1.6.0 does not have multidex support
    Installing application
    MultiDexExtractor.load(/data/app/com.xxx.xxx-1.apk, false, )
I/MultiDex: Blocking on lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock
    /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock locked
    Detected that extraction must be performed.
I/MultiDex: Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip
    Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes1415547735.zip
I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip
    Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip: 4238720 - crc: 2971858359
    Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip
    Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes-1615165740.zip
I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip
    Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip: 3106018 - crc: 3138243730
    Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip
    Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes-469912688.zip
I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip
    Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip: 2163715 - crc: 1148318293
    load found 3 secondary dex files
I/MultiDex: install done

第二次启动时的日志:

I/MultiDex: VM with version 1.6.0 does not have multidex support
    Installing application
    MultiDexExtractor.load(/data/app/com.xxx.xxx-1.apk, false, )
    Blocking on lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock
    /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock locked
I/MultiDex: loading existing secondary dex files
    load found 3 secondary dex files
    install done

初始化信息

public final class MultiDex {
    static final String TAG = "MultiDex";
    //老版本dex文件存放路径
    private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
    //dex文件存放路径 code_cache/secondary-dexes
    private static final String SECONDARY_FOLDER_NAME;
    //Multidex最高支持的版本,大于20Android系统已支持
    private static final int MAX_SUPPORTED_SDK_VERSION = 20;
    //Multidex最低支持的版本
    private static final int MIN_SDK_VERSION = 4;
    //vm的版本信息
    private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
    //apk路径
    private static final Set<String> installedApk;
    //是否支持Multidex
    private static final boolean IS_VM_MULTIDEX_CAPABLE;

    static {
        //SECONDARY_FOLDER_NAME=code_cache/secondary-dexes
        SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
        installedApk = new HashSet();
        //VM是否已经支持自动Multidex
        IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
    }

Multidex安装

public static void install(Context context) {
    //VM是否已经支持自动Multidex
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    } else if (VERSION.SDK_INT < 4) {
        //Multidex最低支持的版本
        throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } else {
        try {
            /***部分代码省略***/
            //多线程锁
            synchronized(installedApk) {
                //apkPath = data/data/com.xxx.xxx/
                String apkPath = applicationInfo.sourceDir;
                if (installedApk.contains(apkPath)) {
                    return;
                }
                installedApk.add(apkPath);
                /***部分代码省略***/
                //清除 /data/data/com.xxx.xxx/files/secondary-dexes 目录下的文件
                try {
                    clearOldDexDir(context);
                } catch (Throwable var8) {
                    Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var8);
                }
                //data/data/com.xxx.xxx/code_cache/secondary-dexes
                File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
                //解压apk,获得dex的zip文件列表
                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
                //校验zip文件
                if (checkValidZipFiles(files)) {
                    //安装dex文件
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    //校验失败,重新执行解压(解压失败直接抛出异常)和安装
                    Log.w("MultiDex", "Files were not valid zip files.  Forcing a reload.");
                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
                    if (!checkValidZipFiles(files)) {
                        throw new RuntimeException("Zip files were not valid.");
                    }

                    installSecondaryDexes(loader, dexDir, files);
                }
            }
        }
        /***部分代码省略***/
    }
}

Multidex获取dex文件

加载dex文件

/**
 * @param context
 * @param applicationInfo
 * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/
 * @param forceReload 是否强制重新加载
 * @return 包含dex的zip文件列表
 * @throws IOException
 *             if an error occurs when writing the file.
 */
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
    //data/data/com.xxx.xxx/
    File sourceApk = new File(applicationInfo.sourceDir);
    //apk的循环冗余校验码
    long currentCrc = getZipCrc(sourceApk);
    List files;
    //是否强制执行reload和是否已经解压过apk
    if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
        try {
            //是否已存在zip文件
            files = loadExistingExtractions(context, sourceApk, dexDir);
        } catch (IOException var9) {
            files = performExtractions(sourceApk, dexDir);
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }
    } else {
        //获取apk中的classes2.dex并且压缩为zip文件
        files = performExtractions(sourceApk, dexDir);
        //存储当前apk的信息,作为下次有效缓存的明证
        putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
    }
    return files;
}
/**
 * @param context
 * @param timeStamp apk的最后一次修改时间
 * @param crc apk的循环冗余校验码
 * @param totalDexNumber apk中一共有几个dex
 */
private static void putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber) {
    SharedPreferences prefs = getMultiDexPreferences(context);
    Editor edit = prefs.edit();
    edit.putLong("timestamp", timeStamp);
    edit.putLong("crc", crc);
    edit.putInt("dex.number", totalDexNumber);
    apply(edit);
}

获取已经存在的dex的压缩包

/**
 * @param context
 * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/
 * @param sourceApk data/app/com.xxx.xxx-1.apk
 * @return 包含dex的zip文件列表
 * @throws IOException
 *             if an error occurs when writing the file.
 */
private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir) throws IOException {
    //extractedFilePrefix = com.xxx.xxx.apk.classes
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    //totalDexNumber=apk中dex的数量
    int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
    List<File> files = new ArrayList(totalDexNumber);
    //主dex已经加载过了,加载class2.dex,class3.dex......
    for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
        //fileName = com.xxx.xxx.apk.classes2.zip
        String fileName = extractedFilePrefix + secondaryNumber + ".zip";
        //extractedFile = data/data/com.xxx.xxx/code_cache/secondary-dexes/com.wuba.bangjob-1.apk.classes2.zip
        File extractedFile = new File(dexDir, fileName);
        if (!extractedFile.isFile()) {
            throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
        }

        files.add(extractedFile);
        //校验zip文件是否完整
        if (!verifyZipFile(extractedFile)) {
            Log.i("MultiDex", "Invalid zip file: " + extractedFile);
            throw new IOException("Invalid ZIP file.");
        }
    }

    return files;
}

生成dex的压缩zip文件

/**
 * @param sourceApk data/app/com.xxx.xxx-1.apk
 * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/
 * @return 包含dex的zip文件列表
 * @throws IOException
 *             if an error occurs when writing the file.
 */
private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException {
    //extractedFilePrefix = com.xxx.xxx.apk.classes
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    //删除data/data/com.xxx.xxx/code_cache/secondary-dexes/目录下的文件
    prepareDexDir(dexDir, extractedFilePrefix);
    List<File> files = new ArrayList();
    ZipFile apk = new ZipFile(sourceApk);
    try {
        //从class2.dex开始
        int secondaryNumber = 2;
        for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
            //fileName = com.xxx.xxx.apk.classes2.zip
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            File extractedFile = new File(dexDir, fileName);
            files.add(extractedFile);
            //最多三次尝试生成dex的zip文件
            int numAttempts = 0;
            //标识是否生成dex的zip文件
            boolean isExtractionSuccessful = false;
            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix);
                //校验压缩文件是否完整,否则删除重来
                isExtractionSuccessful = verifyZipFile(extractedFile);
                if (!isExtractionSuccessful) {
                    extractedFile.delete();
                    if (extractedFile.exists()) {
                        Log.w("MultiDex", "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;
        }
    }/***部分代码省略***/
    return files;
}

将classes2.dex放入zip文件中

/**
 * @param apk apk的压缩包
 * @param dexFile apk中的classes2.dex文件
 * @param extractTo /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx.apk.classes2.zip
 * @param extractedFilePrefix  com.wuba.bangjob-1.apk.classes
 * @throws IOException
 *             if an error occurs when writing the file.
 */
private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
    InputStream in = apk.getInputStream(dexFile);
    ZipOutputStream out = null;
    ///data/data/com.wuba.bangjob/code_cache/secondary-dexes/com.xxx.xxx.apk.classes1415547735.zip
    File tmp = File.createTempFile(extractedFilePrefix, ".zip", extractTo.getParentFile());
    try {
        out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
        try {
            //将classes2.dex进行压缩内容名称为classes.dex
            ZipEntry 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);
            }

            out.closeEntry();
        } finally {
            out.close();
        }
        //重命名文件为com.xxx.xxx.apk.classes2.zip
        if (!tmp.renameTo(extractTo)) {
            throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
        }
    }/***部分代码省略***/
}

dex文件的装载

将含有加载含有dex的压缩包进行夹杂,相关知识点参考:Android类加载之PathClassLoader和DexClassLoader
为什么需要做版本的区分,就是因为版本见类加载的实现是有些差异的。

/**
 * @param loader
 * @param dexDir /data/data/xxx.xxx.xxx/code_cache/secondary-dexes/
 * @param files dex的压缩zip文件
 */
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    if (!files.isEmpty()) {
        if (VERSION.SDK_INT >= 19) {//KitKat(19)
            MultiDex.V19.install(loader, files, dexDir);
        } else if (VERSION.SDK_INT >= 14) {//IceCreamSandwich(14,15),JellyBean(16,17,18)
            MultiDex.V14.install(loader, files, dexDir);
        } else {
            MultiDex.V4.install(loader, files);
        }
    }
}

Android类加载之PathClassLoader和DexClassLoader 这篇文章将的是VERSION.SDK_INT >= 19 那么我们先看看第一个case:

private static final class V19 {
    private V19() {
    }
    /**
     * @param loader PathClassLoader
     * @param additionalClassPathEntries dex压缩包路径
     * @param optimizedDirectory opt之后的dex文件目录
     */
    private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
        //PathClassLoader的DexPathList类型变量pathList
        Field pathListField = MultiDex.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList();
        //进行dex的opt并合并dex的Element
        MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
        //处理异常
        if (suppressedExceptions.size() > 0) {
            Iterator i$ = suppressedExceptions.iterator();
            while(i$.hasNext()) {
                IOException e = (IOException)i$.next();
                Log.w("MultiDex", "Exception in makeDexElement", e);
            }
            //一直感觉Google源码这块有些问题,loader应该改为dexPathList。因为dexElementsSuppressedExceptions变量是属于DexPathList的成员
            Field suppressedExceptionsField = MultiDex.findField(loader, "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(loader));
            if (dexElementsSuppressedExceptions == null) {
                dexElementsSuppressedExceptions = (IOException[])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(loader, dexElementsSuppressedExceptions);
        }

    }
    //调用DexPathList的makeDexElements方法进行dex的opt
    private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
        return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
    }
}

19 > VERSION.SDK_INT >= 14的处理支持比VERSION.SDK_INT >= 19少了异常处理。是因为DexPathList的makeDexElements方法有了修改。

/**
 * VERSION.SDK_INT >= 19
 */
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
        ArrayList<IOException> suppressedExceptions);

/**
 * 19 > VERSION.SDK_INT >= 14
 */
private static Element[] makeDexElements(ArrayList<File> files,File optimizedDirectory);

VERSION_SDK_INT < 14 的情况和前面的都不相同是因为PathClassLoader的是实现就不相同。。。。

/**
 * VERSION.SDK_INT == 13
 */
public class PathClassLoader extends ClassLoader {

    private final String path;
    private final String libPath;

    /*
     * Parallel arrays for jar/apk files.
     *
     * (could stuff these into an object and have a single array;
     * improves clarity but adds overhead)
     */
    private final String[] mPaths;
    private final File[] mFiles;
    private final ZipFile[] mZips;
    private final DexFile[] mDexs;
    /***部分代码省略***/
}

apk解压结果预览

image.png

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦

想阅读作者的更多文章,可以查看我 个人博客 和公共号:

振兴书城