Android multidex 使用 与 实现原理

Android multidex 使用 与 实现原理

在Android中一个Dex文件最多存储65536个方法,也就是一个short类型的范围。但随着应用方法数量的不断增加,当Dex文件突破65536方法数量时,打包时就会抛出异常。

为解决该问题,Android5.0时Google推出了官方解决方案:MultiDex。

  • 打包时,把一个应用分成多个dex,例:classes.dex、classes2.dex、classes3.dex…,加载的时候把这些dex都追加到DexPathList对应的数组中,这样就解决了方法数的限制。
  • Andorid 5.0之后,ART虚拟机天然支持MultiDex。
  • Andorid 5.0之前,系统只加载一个主dex,其它的dex采用MultiDex手段来加载。

一、使用

如何使用,最好参照google官方文档,写的很详细:

配置方法数超过 64K 的应用

这里做一下简要说明:

1、minSdkVersion 为 21 或更高值

如果是android 5.0以上的设备,只需要设置为multiDexEnabled true

android {
    defaultConfig {
        ...
        minSdkVersion 21 
        targetSdkVersion 26
        multiDexEnabled true
    }
    ...
}

2、minSdkVersion 为 20 或更低值

如果需要适配android 5.0以下的设备,需使用 Dalvik 可执行文件分包支持库

android {
    defaultConfig {
        ...
        minSdkVersion 15 
        targetSdkVersion 26
        multiDexEnabled true
    }
    ...
}

dependencies {
  compile 'com.android.support:multidex:1.0.3'
}

Java代码方面,继承MultiDexApplication 或者 在Application中添加MultiDex.install(this);

// 继承 MultiDexApplication
public class MyApplication extends MultiDexApplication { ... }


// 或者 在Application中添加 MultiDex.install(this);
public class MyApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
     super.attachBaseContext(base);
     MultiDex.install(this);
  }
}

二、android 5.0 以下 MultiDex 原理

注:
源码基于的版本 com.android.support:multidex:1.0.3

通过 Dalvik可执行文件分包支持库 和 配置方法数超过64K的应用 我们了解到:

  • android 5.0 以下Dalvik虚拟机 只能加载一个主class.dex
  • android.support.multidex.MultiDex.install(this)是对android 5.0 以下Dalvik虚拟机 的兼容;

这里我们分两部分介绍,一部分是dex文件的加载;一部分是dex文件的抽取。

2.1、Dex文件的加载

下面通过跟踪 MultiDex.install(this); 源码,了解其实现原理。

MultiDex.install(this);

跟踪 MultiDex.install(this); 源码

public static void install(Context context) {
    // 如果系统版本大于android 5.0 则天然支持MultiDex
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    } 
    // 系统版本低于android 1.6 抛出异常
    else if (VERSION.SDK_INT < 4) {
        throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } 
    // android 1.6 < android < android 5.0
    else {
        try {
	        // 获取当前应用信息 应用信息不存在,则返回
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
                Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                return;
            }
            // MultiDex
            // sourceDir: /data/app/com.xiaxl.demo-2/base.apk
            // dataDir:   /data/user/0/com.xiaxl.demo
            doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
        } catch (Exception var2) {
            Log.e("MultiDex", "MultiDex installation failure", var2);
            throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
        }

        Log.i("MultiDex", "install done");
    }
}

上边代码中,对1.6 < android < android 5.0 进行判断处理,低于1.6版本抛出异常;高于5.0版本,天然支持MultiDex,所以忽略

  • 如果系统版本大于android 5.0 ART虚拟机 天然支持MultiDex
  • 系统版本低于android 1.6 抛出异常
  • doInstallation MultiDex 处理

跟踪 MultiDex.doInstallation

跟踪 MultiDex.doInstallation,查看MultiDex的实现原理

// 相关入口参数
// sourceDir: /data/app/com.xiaxl.demo-2/base.apk
// dataDir:   /data/user/0/com.xiaxl.demo
// secondaryFolderName: "secondary-dexes"
// prefsKeyPrefix: ""
// reinstallOnPatchRecoverableException: true
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 {
	// 已安装Apk
    Set var6 = installedApk;
    // 同步
    synchronized(installedApk) {
	    // 如果 /data/app/com.xiaxl.demo-2/base.apk 未安装
        if (!installedApk.contains(sourceApk)) {
	        // 添加到 installedApk 这个集合中
            installedApk.add(sourceApk);
            // Android 系统版本大约5.0("java.vm.version"的版本号错误),天然支持MultiDex
            if (VERSION.SDK_INT > 20) {
                Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " 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") + "\"");
            }
            // 根据context 获取 ClassLoader
            ClassLoader loader;
            try {
	            // 获取ClassLoader,实际上是PathClassLoader
                loader = mainContext.getClassLoader();
            } catch (RuntimeException var25) {
                Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
                return;
            }
            // ClassLoader 获取失败
            if (loader == null) {
                Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
            }
            //  
			else {
				// 清除老的缓存的Dex目录,来源的缓存目录是"/data/user/0/${packageName}/files/secondary-dexes"
				// 清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes
                try {
                    clearOldDexDir(mainContext);
                } catch (Throwable var24) {
                    Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
                }
                
                // 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件
                // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
                File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                
                // 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
	            // 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快
                // sourceApk: /data/app/com.xiaxl.demo-2/base.apk
                // dexDir: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                IOException closeException = null;

                try {
		            // prefsKeyPrefix: ""
		            // 返回dex文件列表
                    List files = extractor.load(mainContext, prefsKeyPrefix, false);
                    try {
	                    // 安装secondaryDex
	                    // /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                        installSecondaryDexes(loader, dexDir, files);
                    } catch (IOException var26) {
                        if (!reinstallOnPatchRecoverableException) {
                            throw var26;
                        }

                        Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
                        files = extractor.load(mainContext, prefsKeyPrefix, true);
                        installSecondaryDexes(loader, dexDir, files);
                    }
                } finally {
                    try {
                        extractor.close();
                    } catch (IOException var23) {
                        closeException = var23;
                    }

                }

                if (closeException != null) {
                    throw closeException;
                }
            }
        }
    }
}

忽略dex文件抽取逻辑和校验逻辑,以上代码中主要做了以下三件事:

  • 清空缓存目录"/data/user/0/${packageName}/files/secondary-dexes"
  • 使用MultiDexExtractor这个工具把APK中的dex抽取到"/data/user/0/${packageName}/code_cache/secondary-dexes"目录
  • 加载"/data/user/0/${packageName}/code_cache/secondary-dexes"目录下的dex

下边查看MultiDex.installSecondaryDexes方法,了解MultiDex的具体实现

MultiDex.V4.install(loader, files);

// dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
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 (VERSION.SDK_INT >= 19) {
            MultiDex.V19.install(loader, files, dexDir);
        } else if (VERSION.SDK_INT >= 14) {
            MultiDex.V14.install(loader, files);
        } else {
            MultiDex.V4.install(loader, files);
        }
    }
}

不同版本的Android系统,类加载机制有一些不同,所以分为了V19、V14和V4三种情况下的安装。

这里我们看下一V19的源码

private static final class V19 {
    private V19() {
    }
    // additionalClassPathEntries: dex列表
    // optimizedDirectory: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
    static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
	    // 传递的loader是PathClassLoader,findFidld()方法找到父类BaseClassLoader中pathList属性
	    // 获取BaseDexClassLoader中pathList属性
	    // this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        Field pathListField = MultiDex.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        // 将dex文件添加到DexPathList中的dexElements 数组的末尾
        ArrayList<IOException> suppressedExceptions = new ArrayList();
        MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
        // 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况
        if (suppressedExceptions.size() > 0) {
            Iterator var6 = suppressedExceptions.iterator();

            while(var6.hasNext()) {
                IOException e = (IOException)var6.next();
                Log.w("MultiDex", "Exception in makeDexElement", e);
            }

            Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
            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(dexPathList, dexElementsSuppressedExceptions);
            IOException exception = new IOException("I/O exception during makeDexElement");
            exception.initCause((Throwable)suppressedExceptions.get(0));
            throw exception;
        }
    }
	
	// 通过反射的方式调用DexPathList#makeDexElements()方法
	// dexPathList: DexPathList
	// files: dex文件列表
    private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
	    // 通过DexPathList的makeDexElements方法加载 “dex文件”
        Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
        return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
    }
}

通过V19的install()方法,关于MultiDex如何加载Dex文件的问题已经清晰:

  • 将APK文件中除主dex文件之外的dex文件追加到PathClassLoader(也就是BaseClassLoader)DexPathListde Element[]数组中。这样在加载一个类的时候就会遍历所有的dex文件,保证了打包的类都能够正常加载。

Dex的加载到此完成,下边查看Dex的抽取逻辑~~~~~

2.2、Dex文件的抽取

前边说过:
MultiDexExtractor这个工具类的作用是把APK中的dex文件抽取到/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes目录中

MultiDexExtractor 构造方法

// sourceApk:  /data/app/com.xiaxl.demo-2/base.apk
// dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
MultiDexExtracto(File sourceApk, File dexDir) throws IOException {
    this.sourceApk = sourceApk;
    this.dexDir = dexDir;
    // 循环冗余校验码(CRC)
    this.sourceCrc = getZipCrc(sourceApk);
    // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock
    File lockFile = new File(dexDir, "MultiDex.lock");
    // 对文件内容的访问,既可以读文件也可以写文件,可以访问文件的任意位置适用于由大小已知的记录组成的文件
    // 对/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock 进行读写
    this.lockRaf = new RandomAccessFile(lockFile, "rw");

    try {
	    // 返回文件通道
        this.lockChannel = this.lockRaf.getChannel();

        try {
            Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
            this.cacheLock = this.lockChannel.lock();
        } catch (RuntimeException | Error | IOException var5) {
            closeQuietly(this.lockChannel);
            throw var5;
        }

        Log.i("MultiDex", lockFile.getPath() + " locked");
    } catch (RuntimeException | Error | IOException var6) {
        closeQuietly(this.lockRaf);
        throw var6;
    }
}

MultiDexExtractor.load

APK中的dex文件的抽取

// 返回dex文件列表
// prefsKeyPrefix: ""
// forceReload: false
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
    // MultiDexExtractor 不可用
    if (!this.cacheLock.isValid()) {
        throw new IllegalStateException("MultiDexExtractor was closed");
    } else {
        List files;
        // forceReload ==false;
        // isModified == true;
        // 如果不需要重新加载并且文件没有被修改过
	    // isModified()方法是根据SharedPreference中存放的APK文件上一次修改的时间戳和currentCrc来判断是否修改过文件
        if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
            try {
	            // 从缓存目录中加载已经抽取过的文件,并返回dex文件列表
                files = this.loadExistingExtractions(context, prefsKeyPrefix);
            } catch (IOException var6) {
                Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                files = this.performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }
        } else {
            if (forceReload) {
                Log.i("MultiDex", "Forced extraction must be performed.");
            } else {
                Log.i("MultiDex", "Detected that extraction must be performed.");
            }
            // 如果强制加载或者APK文件已经修改过就重新抽取dex文件
            files = this.performExtractions();
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
        }

        Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
        return files;
    }
}

MultiDexExtractor.performExtractions()

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
	//  抽取出的dex文件名前缀是"base.apk.classes"
    String extractedFilePrefix = this.sourceApk.getName() + ".classes";
    this.clearDexDir();
    // 返回的dex列表
    List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
    // apk压缩包
    ZipFile apk = new ZipFile(this.sourceApk);

    try {
        int secondaryNumber = 2;

        for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
	        // base.apk.classes2.zip
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            // 创建文件/data/app/com.xiaxl.demo-2/base.apk.classes2.zip
            MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
            // 添加到文件列表
            files.add(extractedFile);
            Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;
            // 抽取dex
            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix);

                try {
                    extractedFile.crc = getZipCrc(extractedFile);
                    isExtractionSuccessful = true;
                } catch (IOException var18) {
                    isExtractionSuccessful = false;
                    Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
                }

                Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
                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;
        }
    } finally {
        try {
            apk.close();
        } catch (IOException var17) {
            Log.w("MultiDex", "Failed to close resource", var17);
        }

    }

    return files;
}

2.3、其他相关代码

clearOldDexDir(Context context)

private static void clearOldDexDir(Context context) throws Exception {
	// /data/user/0/com.xiaxl.demo/files/secondary-dexes
    File dexDir = new File(context.getFilesDir(), "secondary-dexes");
    if (dexDir.isDirectory()) {
        Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
        // 获取文件列表
        File[] files = dexDir.listFiles();
        // 文件为空
        if (files == null) {
            Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
            return;
        }
        // 文件不为空
        File[] var3 = files;
        int var4 = files.length;
        // 循环清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes 下全部文件
        for(int var5 = 0; var5 < var4; ++var5) {
            File oldFile = var3[var5];
            Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
            if (!oldFile.delete()) {
                Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
            } else {
                Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
            }
        }
        // 删除 /data/user/0/com.xiaxl.demo/files/secondary-dexes 文件夹
        if (!dexDir.delete()) {
            Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
        } else {
            Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
        }
    }
}
private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {
	// 创建 /data/user/0/com.xiaxl.demo/code_cache 目录
    File cache = new File(dataDir, "code_cache");
    try {
        mkdirChecked(cache);
    } catch (IOException var5) {
        cache = new File(context.getFilesDir(), "code_cache");
        mkdirChecked(cache);
    }
    // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
    File dexDir = new File(cache, secondaryFolderName);
    mkdirChecked(dexDir);
    return dexDir;
}

三、总结

到这里,MultiDex安装多个dex的原理已经清楚了。

  • 通过一定的方式把dex文件抽取出来;
  • 把这些dex文件追加到DexPathList的Element[]数组的后面
    这个过程要尽可能的早,所以一般是在Application的attachBaseContext()方法中。

另外,hotfix热修复技术,就是通过一定的方式把修复后的dex插入到DexPathList的Element[]数组前面实现修复后的class抢先加载。

参考:

Android源代码

类加载机制系列3——MultiDex原理解析

你可能感兴趣的:(Android源码)