MultiDex源码解析

1.产生背景

65535问题是一个应用开发到一定阶段后必定会遇到的一个问题,主要是因为在开始设计的Dex文件格式中将method的引用限制为short进行存储,导致超过数目后编译失败,后来google推出了一个MultiDex来解决这一个问题。

2.源码分析

2.1MultiApplication

这个类是需要我们去继承的,当然也可以不用继承,我们只需要实现以下的方法就行

MultiDex.install(this);
2.2MultiDex

首先这个类的static静态块中初始化了一些数据,

static {   
  SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes"; 
  installedApk = new HashSet();
  IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
}

第一个参数是分dex的文件存放路径,第二个是一个hashset,第三个调用的一个判断是否multidex已经支持的一个方法,传入的参数则是虚拟机的版本信息。

Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if(matcher.matches()) {
...
int e = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = e > 2 || e == 2 && minor >= 1;
...

如上所示,是通过一个正则来进行判断的,根据对多个手机版本的测试,在4.4.4的机型上版本为1.6.0,在5.1和6.0的机型上均为2.1.0,推断在5.0以下的机型返回false,5.0及以上的返回true。
接下来就是install部分的代码了,在贴代码前先提出几个问题,app在安装中做了什么事情,安装后存放的路径在哪
install时分为几个步骤

3.1判断是否进入MultiDex主流程
if(IS_VM_MULTIDEX_CAPABLE) {  
  Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if(VERSION.SDK_INT < 4) {  
  throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {

这里调用的就是上面的虚拟机版本并且不支持小于4的情况

3.2判断是否安装过
Set var2 = installedApk;
    synchronized(installedApk) {  
    String apkPath = e.sourceDir; 
    if(installedApk.contains(apkPath)) {      
        return; 
    }
    installedApk.add(apkPath);

如果安装过就直接返回,这里installedAPK是静态块中直接初始化的,默认就是空,而且没有赋值的地方,肯定会跳过这个过程,所以这里还不了解实际的意义是什么。

3.3清除老的插件列表

清除老的Dex文件只是为了防止重新加载,这里只是传入了一个dex的文件目录然后进行递归删除文件,最后删除整个文件夹,代码比较简单就不贴出来了。

3.4MultiDex文件提取
File dexDir = new File(e.dataDir, SECONDARY_FOLDER_NAME);
List files = MultiDexExtractor.load(context, e, dexDir, false);

这里首先拼接一个完整的dex的路径
dataDir是安装后存放数据的地方 也就是data/data/packageName
SECONDARY_FOLDER_NAME则是安装完dex存在的地方,拼接出来的完整路径dexDir就是
data/data/packageName/code_cache/secondary-dexes
然后将这个路径以及applicationInfo,是否强制重新加载传递给MultiDexExtractor,这个是提取dex的核心代码

static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
File sourceApk = new File(applicationInfo.sourceDir);
    long currentCrc = getZipCrc(sourceApk);
    List files;
    if(!forceReload && !isModified(context, sourceApk, currentCrc)) {
        try {
            files = loadExistingExtractions(context, sourceApk, dexDir);
        } catch (IOException var9) {
            files = performExtractions(sourceApk, dexDir);
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }
    } else {
        files = performExtractions(sourceApk, dexDir);
        putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
    }
    return files;
}

逐行分析,isModified根据CRC校验apk是否和安装时的一样,forceReload默认传进来为false,当在加载失败的时候会走到MultiDex的catch方法中,然后传入进来的就是true,一般就是不重新提取,所以是直接走到loadExistingExtractions方法

private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir) throws IOException {
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
    ArrayList files = new ArrayList(totalDexNumber);
    for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
        String fileName = extractedFilePrefix + secondaryNumber + ".zip";
        File extractedFile = new File(dexDir, fileName);
        if(!extractedFile.isFile()) {
            throw new IOException("Missing extracted secondary dex file \'" + extractedFile.getPath() + "\'");
        }
        files.add(extractedFile);
        if(!verifyZipFile(extractedFile)) {
            throw new IOException("Invalid ZIP file.");
        }
    }
    return files;
}

sourceApk是外部传进来的,初始值是applicationInfo的sourceDir,getName后得到的就是apkName.apk
然后在for循环中根据分dex的count进行遍历,经过fileName,dexDir文件拼接最后产生的files列表的全称就是data/data/packageName/code_cache/secondary-dexes/data/data/apkName.apk.classesN.zip
但是如果提取失败,或者文件校验不成功,便会强制进行performExtractions。

private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException { 
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    prepareDexDir(dexDir, extractedFilePrefix);
    ArrayList files = new ArrayList();
    ZipFile apk = new ZipFile(sourceApk);
    try {
            int e = 2;
            for(ZipEntry dexFile = apk.getEntry("classes" + e + ".dex");
            dexFile != null;
            dexFile = apk.getEntry("classes" + e + ".dex")) {
            String fileName = extractedFilePrefix + e + ".zip";
            File extractedFile = new File(dexDir, fileName);
            files.add(extractedFile);
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;
            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix);
                isExtractionSuccessful = verifyZipFile(extractedFile);
                if(!isExtractionSuccessful) {
                    extractedFile.delete();
                    if(extractedFile.exists()) {
                    }
               }
            }
            if(!isExtractionSuccessful) {
                throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + e + ")");
            }
            ++e;
        }
    } finally {
        try {
            apk.close();
        } catch (IOException var16) {
            Log.w("MultiDex", "Failed to close resource", var16);
        }
    }
    return files;
}

首先调用的是prepareDexDir方法

File cache = dexDir.getParentFile();
mkdirChecked(cache);
mkdirChecked(dexDir);
FileFilter filter = new FileFilter() {
    public boolean accept(File pathname) {
        return !pathname.getName().startsWith(extractedFilePrefix); 
   }};

在里面初始化了两级文件夹 code_cache和secondary-dexes,然后过滤掉不是extractedFilePrefix开头的文件并将其删除,通过ZipFile的构造函数中传入源文件apk

int e = 2;
for(ZipEntry dexFile = apk.getEntry("classes" + e + ".dex");dexFile != null;dexFile = apk.getEntry("classes" + e + ".dex")) {
    String fileName = extractedFilePrefix + e + ".zip";
    File extractedFile = new File(dexDir, fileName);
    files.add(extractedFile);
    int numAttempts = 0;
    boolean isExtractionSuccessful = false;
    while(numAttempts < 3 && !isExtractionSuccessful) {
        ++numAttempts;
        extract(apk, dexFile, extractedFile, extractedFilePrefix);
        isExtractionSuccessful = verifyZipFile(extractedFile);
        if(!isExtractionSuccessful) {
            extractedFile.delete();
            if(extractedFile.exists()) {
            }
        }
    }
    if(!isExtractionSuccessful) {
        throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + e + ")");
    }
    ++e;

for(1;2;3){4},for循环的顺序就是1-2-4-3这样的,所以首先是从ZipFile的getEntry中取出classes2.dex这个dexFile,经过熟悉的两步拼接成一个data/data/packageName/code_cache/secondary-dexes/data/data/apkName.apk.classesN.zip,然后调用extract这个方法。

private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
    InputStream in = apk.getInputStream(dexFile);
    ZipOutputStream out = null;
    File tmp = File.createTempFile(extractedFilePrefix, ".zip", extractTo.getParentFile());
    try {
        out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
        try {
            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();
        }
        if(!tmp.renameTo(extractTo)) {
            throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
        }
    } finally {
        closeQuietly(in);
        tmp.delete();
    }}

第一步拿到dexFile的流,创建一个tmp的zip临时文件,将dexFile的数据写入到临时的zip文件中,并存入一份时间戳,这里有三次重试机会,每次调用一次便将numAttempts++,如果仍然不成功,就将这个文件删除。
到此阶段,无论是直接加载dex还是重新提取都走完了自己的阶段,然后就是最终MultiDex的installSecondaryDexes方法,分为三个版本的加载,分别是19,14和14以下,其中14和19版本的代码大同小异,在19上只是多了一个suppressedExceptions,这个在stackoverflow上有人给了一个定义

Java 7 has a new feature called "suppressed exceptions", because of "the addition of ARM" (support for ARM CPUs?).

主要是用于兼容ARM平台的cpu,做一些特定的事情

An exception can be thrown from the block of code associated with the try-with-resources statement. In the example writeToFileZipFileContents, an exception can be thrown from the try block, and up to two exceptions can be thrown from the try-with-resources statement when it tries to close the ZipFile and BufferedWriter objects. If an exception is thrown from the try block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception thrown by the block is the one that is thrown by the writeToFileZipFileContents method. You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed method from the exception thrown by the try block.

真正的install只有三行代码

Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));

BaseDexClassLoader.java
通过反射拿到上面这个类中定义的DexPathList的pathList这个实例

private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class});
    return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory}));
}

然后再次通过反射调用DexPathList的MakeDexElements方法,这里实际上是产生了一个Elements数组,包含.jar.zip.apk等文件。最终实际上我们通过classloader加载的就是这个列表。
整个加载过程到这里就分析完了,其实还有很多细节没有理清,以后深入理解后可能能产生一些新的想法。

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

推荐阅读更多精彩内容