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 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 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 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 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加载的就是这个列表。
整个加载过程到这里就分析完了,其实还有很多细节没有理清,以后深入理解后可能能产生一些新的想法。

你可能感兴趣的:(MultiDex源码解析)