Multidex记录三:源码解析

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容