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