一、MultiDex是什么,解决了什么问题
MultiDex 顾名思义就是对分包的Dex文件进行读取加载到ClassLoader的库
android 早期的版本中,Dex文件(android对java中字节码 class文件 进行了调整,把所有的class打包到一的Dex文件中)的方法数量是有上限的65536,当一个app中的方法数量超过这个值,在app安装的时候就会抛出错误,无法安装
为什么Dex的方法数量上限是65536呢?
android在安装一个应用的时候其中会有一步工作是使用DexOpt工具对Dex文件进行优化,会将Dex文件转成ODex文件(Optimised Dex 优化了的Dex文件)因为ODex速度更快,DexOpt处理Dex文件的时候会把Dex中的所有方法提取id索引,然后存在一个链表中,链表的长度用short来保存,这个就导致了这个长度不能超过65536
在android5.0以前是有这个问题的,在android5.0以后android系统解决了这个问题,所以MultiDex是针对android5.0以前的这个问题起作用的
解决这个问题要分成2步
1、将Dex分包,分成若干个不同的包
2、将这些不同的Dex加载进入app的ClassLoader中,从而正常加载
第一步:这一步其实用的android studio中的插件来解决的,
multiDexEnabled true
这个是在工程中的app下面的build.gradle文件中 配置的,就是告诉gradle 在编译打包app的时候将app打包成多个Dex的模式
第二步:在app工程中配置MultiDex库,来加载多个Dex文件,这个配置是在app的application中的
protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); }
或者直接让自己的application继承MultiDexApplication 就可以了
public class MultiDexApplication extends Application { public MultiDexApplication() { } protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } }
这个就是接入了MultiDex库的 打包app的资源目录图了
可以看到很多的Dex文件
二、MutiDex实现原理
接下来我们来分析下MutiDex如何实现加载多个Dex文件到app的ClassLoader中的
1、ClassLoader分析
classloader是java的类加载器,是用来把java类加载java虚拟器中的作用
在android中的虚拟器是Dalvik把java的虚拟器进行了调整修改,来更加在适配android的轻量化需求
但是这个是在android5.0以前,
android5.0之后变成了AndroidRuntime简称ART,简而言之将android的Dex一次基础性预编译,提前将字节码转为机器码,减少了Dalvik的运行时转机器码的工作量,速度更快了,
我们来看下ClassLoader的继承结构
-
ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。BootClassLoader是它的内部类。
-
SecureClassLoader类和JDK8中的SecureClassLoader类的代码是一样的,它继承了抽象类
-
ClassLoader。SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
-
URLClassLoader类和JDK8中的URLClassLoader类的代码是一样的,它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。
-
InMemoryDexClassLoader是Android8.0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。
-
BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader和DexClassLoader都继承它。
2、MultiDex流程
我们用MultiDex2.0.0为例子
1、MultiDex.install(this);
这个是MultiDex的调用的其实位置,这个是在aplication的attachBaseContext中调用的,所以我们的代码不要在attachBaseContext的MultiDex.install(this) 之前调用,因为这个时候有些Dex还没有加载,会跑出来ClassNotFoundException
2、MultiDex.install内部逻辑
public static void install(Context context) { Log.i("MultiDex", "Installing application"); 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("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + "."); } 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; } 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、IS_VM_MULTIDEX_CAPABLE 这个是当前的虚拟器是否支持MutliDex的标志
private static final boolean IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
static boolean isVMMultidexCapable(String versionString) { boolean isMultidexCapable = false; if (versionString != null) { Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString); if (matcher.matches()) { try { int major = Integer.parseInt(matcher.group(1)); int minor = Integer.parseInt(matcher.group(2)); isMultidexCapable = major > 2 || major == 2 && minor >= 1; } catch (NumberFormatException var5) { } } } Log.i("MultiDex", "VM with version " + versionString + (isMultidexCapable ? " has multidex support" : " does not have multidex support")); return isMultidexCapable; }
版本是通过System.getProperty("java.vm.version") 来获取虚拟器的版本
然后根据版本号判断条件来识别,通过代码可以看出 要大于等2.1.0的版本才具备MutilDex的能力,这个是系统就具备了这个能力 不需要Mutildex这个库来实现了,所以低版本的android 是不具备这个能力的
如果是>=2.1.0 这个库就什么都不做 让系统来完成
2、else if (VERSION.SDK_INT < 4)
这个表示android4一下的不支持 会直接抛异常
3、获取application 获取dex 然后加载
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; } 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() + ")."); }
ApplicationInfo 的获取没什么好说的,就是获取的app的相关资源和路径等信息,比如apk的安装路径 比如dex路径等
3、doInstallation
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 { //installedApk 这个是判断apk是否已经安装,避免重复加载 节省时间 synchronized(installedApk) { if (!installedApk.contains(sourceApk)) { installedApk.add(sourceApk); if (VERSION.SDK_INT > 20) { //如果sdk版本高于20,MultiDex 不能保证可行,就是说sdk版本高于android5.0 这个库依然可以执行 但是不保证没问题 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") + "\""); } ClassLoader loader; try { //获取app的ClassLoader 这个ClassLoader到底是什么呢?这个下面详解 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; } if (loader == null) { Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching."); } else { try { //清理Dex的缓存目录?难道每次都清楚缓存,那每次进app都要加载 ? //下面详解 clearOldDexDir(mainContext); } catch (Throwable var24) { Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24); } //这个就是获取Dex的缓存目录, File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName); //这个类的作用是提取apk中的dex 然后压缩成zip格式的文件 最后返回zip的list MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir); IOException closeException = null; try { //根据apk里的dex文件 压缩成zip格式 然后返回 List files = extractor.load(mainContext, prefsKeyPrefix, false); try { //将zip的dex文件 添加到系统的app的ClassLoader的Dex列表前面 //为了能够提前加载 //具体过程稍后解析 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; } } } } }
上面的已经写了相关注释,比较复杂的过程会有详细解释
1、loader = mainContext.getClassLoader();
这个到底是来自哪里
public abstract ClassLoader getClassLoader(); 首先这个调用会走到这里 这个是Context里的抽象方法,他的实现是在ContextImpl
ContextImpl: @Override 275 public ClassLoader getClassLoader() { 276 return mPackageInfo != null ? 277 mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader(); 278 } 如果mPackageInfo==null 就调用ClassLoader.getSystemClassLoader() mPackageInfo缓存作用
public abstract class ClassLoader { static private class SystemClassLoader { public static ClassLoader loader = ClassLoader.createSystemClassLoader(); }
private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); // String[] paths = classPath.split(":"); // URL[] urls = new URL[paths.length]; // for (int i = 0; i < paths.length; i++) { // try { // urls[i] = new URL("file://" + paths[i]); // } // catch (Exception ex) { // ex.printStackTrace(); // } // } // // return new java.net.URLClassLoader(urls, null); // TODO Make this a java.net.URLClassLoader once we have those? return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); }
我们可以清楚的看到 Context 调用的ClassLoader是PathClassLoader,这个是加载我们自己写的类的加载器
同时我们可以看到PathClassLoader的父加载器是BootClassLoader,之前我们提过BootClassLoader是ClassLoader的内部类,这个BootClassLoader 是在虚拟器开始的时候就初始化 然后加载系统的一些类库。
app自定义的类 都是PathClassLoader来完成加载的
我们来看下ClassLoader的loadClass方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded //通过虚拟器中查找已经加载过的类信息 Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { //从父类加载 加载class c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. //上面都都没有了 找不到了 就从当前的类加载器 加载类信息 c = findClass(name); } } return c; }
双亲委托机制:
我们可以看到ClassLoader的加载是双亲委托机制的,先查找父加载器是否已经加载过,如果加载过了 就直接用父类的,如果没有就从自己的类加载中加载
优点:
1、提高加载性能
这个很容易理解,类加载的过程是比较耗时的,如果避免了重复加载 性能肯定会提高
2、安全性
防止内核的核心类被串改,比如String在系统一开始的时候就被加载了,如果用户自定一个String类,如果不先去parent去查询是否已经加载了,就会直接用用的String的自定义类,那么这个系统毫无安全可言了,核心api可以随意被替换,相反如果加载用户的自定义String类,就先去查询父ClassLoader,始终得到是系统String类,用户自定义的没有起作用了
2、clearOldDexDir
清除旧的dex文件,这里不是清除上次加载的dex文件缓存。 获取dex缓存目录是,会优先获取/data/data/${packageName}/code-cache作为缓存目录。 如果获取失败,则使用/data/data/${packageName}/files/code-cache目录。 这里清除的是第二个目录。
3、MultiDexExtractor#load 方法
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException { Log.i("MultiDex", "MultiDexExtractor.load(" + this.sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")"); //文件锁,防止多进程操作 MultiDex.lock 是锁的文件 if (!this.cacheLock.isValid()) { throw new IllegalStateException("MultiDexExtractor was closed"); } else { List files; //是否强制重新解压,第一次优先使用已经加载过的dex //根据crc和修改时间来判断是否需要重新解压,覆盖安装app后crc和时间会修改 if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) { try { 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."); } files = this.performExtractions(); putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files); } Log.i("MultiDex", "load found " + files.size() + " secondary dex files"); return files; } }
这个方法的作用就是把apk中的dex文件打包压缩成zip的文件然后返回
下面来看下如果重新解压要走的方法
performExtractions()
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException { String extractedFilePrefix = this.sourceApk.getName() + ".classes"; this.clearDexDir(); //返回结果集合 实质是file文件集合 List<MultiDexExtractor.ExtractedDex> files = new ArrayList();、 //要抽取的apk文件 ZipFile apk = new ZipFile(this.sourceApk); try { //dex的序号名字一般都是classes2.dex classes3.dex //第一个dex已经被系统加载了,所以是从第二个dex开始, int secondaryNumber = 2; for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) { String fileName = extractedFilePrefix + secondaryNumber + ".zip"; //这个就是要添加进去的zip文件 这个zip里面后面会从apk里面提取对应的dex放进去 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; //numAttempts :这个是重复次数,因为提取apk 有可能不成功, //isExtractionSuccessful 通过这个标志来判断是否提取成功 while(numAttempts < 3 && !isExtractionSuccessful) { ++numAttempts; //这个方法的核心,从apk里面提取出dex文件 然后放进去extractedFile的zip文件中 //这个其实是一些io操作 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; }
4、installSecondaryDexes
这个方法里面就是这个框架的核心了,顾名思义加载dex
这个方法里面会根据sdk的版本 使用不同的加载方案 以此来达到兼容的目的
rivate 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); } } } 不同版本的sdk使用不同的加载方案
我们用V19的方案解读,其他版本都类似
private static final class V19 { private V19() { } static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { /* *MultiDex.findField(loader, "pathList"); *loader:这个就是之前的Context.getClassLoader获取的PathClassLoader *pathList 这个是PathClassLoader的父类BaseDexClassLoader的局部属性,一个存储了dex的集 *合类DexPathList */ Field pathListField = MultiDex.findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList(); //expandFieldArray 这个是把两个数组合并到一起 //把dexPathList(DexPathList)的dexElements的集合和classes2.dex 等的合并 //classes2.dex 会把这个追加到系统的classloader的dexlist的后面 //然后把最后的结果设置到DexPathList中 //下面会说下makeDexElements // MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); 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; } } private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions)); } }
1、BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader { 30 private final DexPathList pathList; 31 32 /** 33 * Constructs an instance. 34 * 35 * @param dexPath the list of jar/apk files containing classes and 36 * resources, delimited by {@code File.pathSeparator}, which 37 * defaults to {@code ":"} on Android 38 * @param optimizedDirectory directory where optimized dex files 39 * should be written; may be {@code null} 40 * @param librarySearchPath the list of directories containing native 41 * libraries, delimited by {@code File.pathSeparator}; may be 42 * {@code null} 43 * @param parent the parent class loader 44 */ 45 public BaseDexClassLoader(String dexPath, File optimizedDirectory, 46 String librarySearchPath, ClassLoader parent) { 47 super(parent); 48 this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory); 49 }
BaseDexClassLoader 里面有个pathList属性类,里面存储了已经加载了的dex列表
2、makeDexElements
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions)); }
这个方法其实就是反射调用DexPathList的makeDexElements 生成Element[] 数组集合,
那我们看下Element
static class Element { //这个element对应的file 494 private final File dir; //这个file是否是目录 495 private final boolean isDirectory; //如果这个file是以zip结尾,那么这个文件会被初始化为DexFile文件,zip会被初始化 496 private final File zip; //如果这个文件是以dex结尾 那么这个文件会被初始化为DexFile文件,zip=null 497 private final DexFile dexFile; 498 499 private ClassPathURLStreamHandler urlHandler; 500 private boolean initialized; 501 502 public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) { 503 this.dir = dir; 504 this.isDirectory = isDirectory; 505 this.zip = zip; 506 this.dexFile = dexFile; 507 } 508
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader, 134 DexPathList.Element[] elements) throws IOException { 135 if (outputName != null) { 136 try { 137 String parent = new File(outputName).getParent(); 138 if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) { 139 throw new IllegalArgumentException("Optimized data directory " + parent 140 + " is not owned by the current user. Shared storage cannot protect" 141 + " your application from code injection attacks."); 142 } 143 } catch (ErrnoException ignored) { 144 // assume we'll fail with a more contextual error later 145 } 146 } 147 //这个方法最终会调用到native的方法 并返回dex的native索引, //native中其实这个主要是验证dex的结构合理行,初始化是否正确 148 mCookie = openDexFile(sourceName, outputName, flags, loader, elements); 149 mFileName = sourceName; 150 //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName); 151 }
private static native Object openDexFileNative(String sourceName, String outputName, int flags, 397 ClassLoader loader, DexPathList.Element[] elements);
三、总结
MutliDex框架的核心思想是把其他的dex文件添加进了app的Classloader(PathCloassLoader)中的pathlist(可以理解为dex列表中,其实是DexFile的数组)其实是一种提前往系统注入dex的方法策略 来实分多次加载dex文件的目的
市面上很多的热修复框架都是基于这个思想来做的