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);

// 继承 MultiDexApplicationpublic 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: trueprivate 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-dexesprivate 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 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 files, File optimizedDirectory, ArrayList 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-dexesMultiDexExtracto(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: falseList<? 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 performExtractions() throws IOException {    //  抽取出的dex文件名前缀是"base.apk.classes"    String extractedFilePrefix = this.sourceApk.getName() + ".classes";    this.clearDexDir();    // 返回的dex列表    List 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原理解析

更多相关文章

  1. 转:教程:实现Android的不同精度的定位(基于网络和GPS)
  2. Android(安卓)GPRS的自动打开与关闭。
  3. Android(安卓)开发艺术探索笔记之八 -- 理解 Window 和 WindowMa
  4. Cordova自定义插件实战
  5. Android(安卓)中Dialog点击空白处會消失问题
  6. ANDROID 后台服务 service
  7. 【Android】音乐播放器边播边缓存(二)AndroidVideoCache的后台播放
  8. Android(安卓)Studio中获取sha1证书指纹数据的方法
  9. Android学习笔记-保存文件(Saving Files)

随机推荐

  1. Android中Service(服务)详解
  2. Android属性动画完全解析
  3. android利用GPS和高德地图获取定位案例
  4. android的EditText控件,内容右对齐
  5. Android中的文件存储数据方式
  6. android:layout_marginStart和android:pa
  7. linearlayout属性
  8. jsonformatter json格式化 Android
  9. android 名称
  10. 安卓开发到底是什么