[关闭]
@946898963 2018-08-16T17:11:51.000000Z 字数 14023 阅读 950

MultiDex源码分析

Android源码分析


本文并非原创,转载自:Android MultiDex实现原理解析

MultiDex.install是整个MultiDex的入口点,我们以此为切入点开始分析:

  1. public static void install(Context context) {
  2. Log.i(TAG, "install");
  3. // 检查当前系统是否支持multidex
  4. if (IS_VM_MULTIDEX_CAPABLE) {
  5. Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
  6. try {
  7. clearOldDexDir(context);
  8. } catch (Throwable t) {
  9. Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
  10. + "continuing without cleaning.", t);
  11. }
  12. return;
  13. }
  14. // MultiDex最低只支持到1.6
  15. if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
  16. throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
  17. + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
  18. }
  19. try {
  20. ApplicationInfo applicationInfo = getApplicationInfo(context);
  21. if (applicationInfo == null) {
  22. // Looks like running on a test Context, so just return without patching.
  23. return;
  24. }
  25. synchronized (installedApk) {
  26. // sourceDir对应于/data/app/<package-name>.apk
  27. String apkPath = applicationInfo.sourceDir;
  28. // 若给定apk已经install过,直接退出
  29. if (installedApk.contains(apkPath)) {
  30. return;
  31. }
  32. installedApk.add(apkPath);
  33. // MultiDex 最高只支持到20(Android 4.4W),更高的版本不能保证正常工作
  34. if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
  35. Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
  36. + Build.VERSION.SDK_INT + ": SDK version higher than "
  37. + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
  38. + "runtime with built-in multidex capabilty but it's not the "
  39. + "case here: java.vm.version=\""
  40. + System.getProperty("java.vm.version") + "\"");
  41. }
  42. /*
  43. * 待Patch的class loader应该是BaseDexClassLoaderd的子类,
  44. * MultiDex主要通过修改pathList字段来添加更多的dex
  45. */
  46. ClassLoader loader;
  47. try {
  48. loader = context.getClassLoader();
  49. } catch (RuntimeException e) {
  50. /* Ignore those exceptions so that we don't break tests relying on Context like
  51. * a android.test.mock.MockContext or a android.content.ContextWrapper with a
  52. * null base Context.
  53. */
  54. Log.w(TAG, "Failure while trying to obtain Context class loader. " +
  55. "Must be running in test mode. Skip patching.", e);
  56. return;
  57. }
  58. if (loader == null) {
  59. // Note, the context class loader is null when running Robolectric tests.
  60. Log.e(TAG,
  61. "Context class loader is null. Must be running in test mode. "
  62. + "Skip patching.");
  63. return;
  64. }
  65. // MultiDex的二级dex文件将存放在 /data/data/<package-name>/secondary-dexes 下
  66. File dexDir = new File(context.getFilesDir(), SECONDARY_FOLDER_NAME);
  67. // 从apk中查找并解压二级dex文件到/data/data/<package-name>/secondary-dexes
  68. List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
  69. // 检查dex压缩文件的完整性
  70. if (checkValidZipFiles(files)) {
  71. // 开始安装dex
  72. installSecondaryDexes(loader, dexDir, files);
  73. } else {
  74. Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
  75. // 第一次检查失败,MultiDex会尽责的再检查一次
  76. files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
  77. if (checkValidZipFiles(files)) {
  78. // 开始安装dex
  79. installSecondaryDexes(loader, dexDir, files);
  80. } else {
  81. // Second time didn't work, give up
  82. throw new RuntimeException("Zip files were not valid.");
  83. }
  84. }
  85. }
  86. } catch (Exception e) {
  87. Log.e(TAG, "Multidex installation failure", e);
  88. throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
  89. }
  90. Log.i(TAG, "install done");
  91. }

这个方法涵盖了MultiDex安装的整个流程:

1. 检查虚拟机版本判断是否需要MultiDex

在ART虚拟机中(部分4.4机器及5.0以上的机器),采用了Ahead-of-time(AOT)compilation技术,系统在apk的安装过程中,会使用自带的dex2oat工具对apk中可用的dex文件进行编译,并生成一个可在本地机器上运行的odex(optimized dex)文件,这样做会提高应用的启动速度。(但是安装速度降低了)

若不需要使用MultiDex,将使用clearOldDexDir清除/data/data/pkgName/code-cache/secondary-dexes目录下下所有文件

2. 根据applicationInfo.sourceDir的值获取安装的apk路径

安装完成的apk路径为/data/app/.apk

3. 检查apk是否执行过MultiDex.install,若已经安装直接退出

4. 使用MultiDexExtractor.load获取apk中可用的二级dex列表

  1. static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
  2. Log.i("MultiDex", "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
  3. File sourceApk = new File(applicationInfo.sourceDir);
  4. long currentCrc = getZipCrc(sourceApk);
  5. List files;
  6. if(!forceReload && !isModified(context, sourceApk, currentCrc)) {
  7. try {
  8. files = loadExistingExtractions(context, sourceApk, dexDir);
  9. } catch (IOException var9) {
  10. Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var9);
  11. files = performExtractions(sourceApk, dexDir);
  12. putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
  13. }
  14. } else {
  15. Log.i("MultiDex", "Detected that extraction must be performed.");
  16. files = performExtractions(sourceApk, dexDir);
  17. putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
  18. }
  19. Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
  20. return files;
  21. }

MultiDexExtractor.load会先判断是否需要从apk中解压dex文件,主要判断依据是:上次保存的apk(zip文件)的CRC校验码和last modify日期与dex的总数量是否与当前apk相同。此外,forceReload也会决定是否需要重新解压,这个参数后文会提到。

如果需要解压dex文件,将会使用performExtractions将.dex从apk中解压出来,解压路径为

  1. /data/data/<package-name>/code_cache/secondary-dexes/<package-name>.apk.classes2.zip
  2. /data/data/<package-name>/code_cache/secondary-dexes/<package-name>.apk.classes3.zip
  3. ...
  1. private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException {
  2. // extractedFilePrefix值为<package-name>.apk.classes
  3. String extractedFilePrefix = sourceApk.getName() + ".classes";
  4. prepareDexDir(dexDir, extractedFilePrefix);
  5. ArrayList files = new ArrayList();
  6. ZipFile apk = new ZipFile(sourceApk);
  7. try {
  8. int e = 2;
  9. // 扫描apk内所有classes2.dex、classes3.dex...文件
  10. for(ZipEntry dexFile = apk.getEntry("classes" + e + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + e + ".dex")) {
  11. // 解压路径为 /data/data/<package-name>/secondary-dexes/<package-name>.classes2.dex.zip 、/data/data/<package-name>/secondary-dexes/<package-name>.classes3.dex.zip ...
  12. String fileName = extractedFilePrefix + e + ".zip";
  13. File extractedFile = new File(dexDir, fileName);
  14. files.add(extractedFile);
  15. Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
  16. int numAttempts = 0;
  17. boolean isExtractionSuccessful = false;
  18. // 每个dex文件都会尝试3次解压
  19. while(numAttempts < 3 && !isExtractionSuccessful) {
  20. ++numAttempts;
  21. extract(apk, dexFile, extractedFile, extractedFilePrefix);
  22. isExtractionSuccessful = verifyZipFile(extractedFile);
  23. Log.i("MultiDex", "Extraction " + (isExtractionSuccessful?"success":"failed") + " - length " + extractedFile.getAbsolutePath() + ": " + extractedFile.length());
  24. if(!isExtractionSuccessful) {
  25. extractedFile.delete();
  26. if(extractedFile.exists()) {
  27. Log.w("MultiDex", "Failed to delete corrupted secondary dex \'" + extractedFile.getPath() + "\'");
  28. }
  29. }
  30. }
  31. if(!isExtractionSuccessful) {
  32. throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + e + ")");
  33. }
  34. ++e;
  35. }
  36. } finally {
  37. try {
  38. apk.close();
  39. } catch (IOException var16) {
  40. Log.w("MultiDex", "Failed to close resource", var16);
  41. }
  42. }
  43. return files;
  44. }

解压成功后,会保存本次解压所使用的apk信息,用于下次调用MultiDexExtractor.load时判断是否需要重新解压:

  1. private static void putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber) {
  2. SharedPreferences prefs = getMultiDexPreferences(context);
  3. Editor edit = prefs.edit();
  4. // apk最后修改时间戳
  5. edit.putLong("timestamp", timeStamp);
  6. // apk的CRC校验码
  7. edit.putLong("crc", crc);
  8. // dex的总数量
  9. edit.putInt("dex.number", totalDexNumber);
  10. apply(edit);
  11. }

如果apk未被修改,将会调用loadExistingExtractions方法,直接加载上一次解压出来的文件:

  1. private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir) throws IOException {
  2. Log.i("MultiDex", "loading existing secondary dex files");
  3. String extractedFilePrefix = sourceApk.getName() + ".classes";
  4. int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
  5. ArrayList files = new ArrayList(totalDexNumber);
  6. for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
  7. String fileName = extractedFilePrefix + secondaryNumber + ".zip";
  8. File extractedFile = new File(dexDir, fileName);
  9. if(!extractedFile.isFile()) {
  10. throw new IOException("Missing extracted secondary dex file \'" + extractedFile.getPath() + "\'");
  11. }
  12. files.add(extractedFile);
  13. if(!verifyZipFile(extractedFile)) {
  14. Log.i("MultiDex", "Invalid zip file: " + extractedFile);
  15. throw new IOException("Invalid ZIP file.");
  16. }
  17. }
  18. return files;
  19. }

不管是调用了loadExistingExtractions还是performExtractions,都会返回一个解压后的.apk.classes2.zip、.apk.classes3.zip…File列表,供下一步使用。

5. 两次校验dex压缩包的完整性

通过上一步得到解压后的dex File列表后,在MultiDex中会两次检查zip文件的完整性:

  1. public static void install(Context context) {
  2. ...
  3. try {
  4. ...
  5. synchronized (installedApk) {
  6. ...
  7. // 从apk中查找并解压二级dex文件到/data/data/<package-name>/secondary-dexes
  8. List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
  9. // 检查dex压缩文件的完整性
  10. if (checkValidZipFiles(files)) {
  11. // 开始安装dex
  12. installSecondaryDexes(loader, dexDir, files);
  13. } else {
  14. Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
  15. // 第一次检查失败,MultiDex会尽责的再检查一次
  16. files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
  17. if (checkValidZipFiles(files)) {
  18. // 开始安装dex
  19. installSecondaryDexes(loader, dexDir, files);
  20. } else {
  21. // Second time didn't work, give up
  22. throw new RuntimeException("Zip files were not valid.");
  23. }
  24. }
  25. }
  26. } catch (Exception e) {
  27. Log.e(TAG, "Multidex installation failure", e);
  28. throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
  29. }
  30. Log.i(TAG, "install done");
  31. }

若第一次校验失败(dex文件损坏等),MultiDex会重新调用MultiDexExtractor.load方法重查找加载二级dex文件列表,值得注意的是此时forceReload的值为true,会强制重新从apk中解压dex文件。

6. 开始dex的安装

在阅读这一章节前,建议阅读:Android BaseDexClassLoader源码阅读

经过上面的重重检验和解压,终于到了最关键的一步:将二级dex添加到我们classLoader中

  1. private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
  2. if(!files.isEmpty()) {
  3. if(VERSION.SDK_INT >= 19) {
  4. MultiDex.V19.install(loader, files, dexDir);
  5. } else if(VERSION.SDK_INT >= 14) {
  6. MultiDex.V14.install(loader, files, dexDir);
  7. } else {
  8. MultiDex.V4.install(loader, files);
  9. }
  10. }
  11. }

由于SDK版本不同,ClassLoader中的实现存在差异,所以使用了三个分支去执行dex的安装。这里我们选择MultiDex.V14.install进行分析,其他两个大同小异:
入参 含义
ClassLoader loader 通过context.getClassLoader获取到的默认类加载器
List additionalClassPathEntries 二级dex文件解压后的路径(通过步骤4获得)
optimizedDirectory 对应/data/data//code_cache/secondary-dexes/目录

  1. private static Field findField(Object instance, String name) throws NoSuchFieldException {
  2. Class clazz = instance.getClass();
  3. while(clazz != null) {
  4. try {
  5. Field e = clazz.getDeclaredField(name);
  6. if(!e.isAccessible()) {
  7. e.setAccessible(true);
  8. }
  9. return e;
  10. } catch (NoSuchFieldException var4) {
  11. clazz = clazz.getSuperclass();
  12. }
  13. }
  14. throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
  15. }
  16. private static Method findMethod(Object instance, String name, Class... parameterTypes) throws NoSuchMethodException {
  17. Class clazz = instance.getClass();
  18. while(clazz != null) {
  19. try {
  20. Method e = clazz.getDeclaredMethod(name, parameterTypes);
  21. if(!e.isAccessible()) {
  22. e.setAccessible(true);
  23. }
  24. return e;
  25. } catch (NoSuchMethodException var5) {
  26. clazz = clazz.getSuperclass();
  27. }
  28. }
  29. throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
  30. }
  31. private static final class V14 {
  32. private V14() {
  33. }
  34. private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
  35. // 通过反射获取 ClassLoader中的pathList
  36. Field pathListField = MultiDex.findField(loader, "pathList");
  37. Object dexPathList = pathListField.get(loader);
  38. // 先调用pathList的makeDexElements,然后将生成的Element[]传入expandFieldArray中
  39. MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
  40. }
  41. private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  42. Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class});
  43. return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory}));
  44. }
  45. }

/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

  1. public class BaseDexClassLoader extends ClassLoader {
  2. ...
  3. /** structured lists of path elements */
  4. private final DexPathList pathList;
  5. ...
  6. public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
  7. super(parent);
  8. this.originalPath = dexPath;
  9. this.originalLibraryPath = libraryPath;
  10. this.pathList =
  11. new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
  12. }
  13. }

/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

  1. /*package*/ final class DexPathList {
  2. ....
  3. /**
  4. * Makes an array of dex/resource path elements, one per element of
  5. * the given array.
  6. */
  7. private static Element[] makeDexElements(ArrayList<File> files,
  8. File optimizedDirectory) {
  9. ArrayList<Element> elements = new ArrayList<Element>();
  10. /*
  11. * Open all files and load the (direct or contained) dex files
  12. * up front.
  13. */
  14. for (File file : files) {
  15. File zip = null;
  16. DexFile dex = null;
  17. String name = file.getName();
  18. if (name.endsWith(DEX_SUFFIX)) {
  19. // Raw dex file (not inside a zip/jar).
  20. try {
  21. dex = loadDexFile(file, optimizedDirectory);
  22. } catch (IOException ex) {
  23. System.logE("Unable to load dex file: " + file, ex);
  24. }
  25. } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
  26. || name.endsWith(ZIP_SUFFIX)) {
  27. zip = file;
  28. try {
  29. dex = loadDexFile(file, optimizedDirectory);
  30. } catch (IOException ignored) {
  31. /*
  32. * IOException might get thrown "legitimately" by
  33. * the DexFile constructor if the zip file turns
  34. * out to be resource-only (that is, no
  35. * classes.dex file in it). Safe to just ignore
  36. * the exception here, and let dex == null.
  37. */
  38. }
  39. } else {
  40. System.logW("Unknown file type for: " + file);
  41. }
  42. if ((zip != null) || (dex != null)) {
  43. elements.add(new Element(file, zip, dex));
  44. }
  45. }
  46. return elements.toArray(new Element[elements.size()]);
  47. }
  48. ...
  49. }

所以MultiDex在安装开始时,会先通过反射调用BaseDexClassLoader里
DexPathList类型的pathList字段,接着通过pathList调用DexPathList的makeDexElements方法,将上面解压得到的additionalClassPathEntries(二级dex文件列表)封装成Element数组。

需要注意的是,makeDexElements最终会去进行dex2opt操作,这是一个比较耗时的过程,如果全部放在main线程去处理的话,比较影响用户体验,甚至可能引起ANR。

dex2opt后,/data/data//code_cache/secondary-dexes/下的会出现优化后的文件:.apk.classes2.dex等

最后调用MultiDex.expandFieldArray:

  1. private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
  2. Field jlrField = findField(instance, fieldName);
  3. Object[] original = (Object[])((Object[])jlrField.get(instance));
  4. Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
  5. System.arraycopy(original, 0, combined, 0, original.length);
  6. System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
  7. jlrField.set(instance, combined);
  8. }

expandFieldArray同样是通过反射调用,找到pathList中的dexElements字段,并将上一步生成的封装了二级dex的Element数组添加到dexElements之后,完成整个安装流程

总结

通过上面的分析,我们可以总结出来MultiDex的原理如下:

apk在Applicaion实例化之后,会检查系统版本是否支持MultiDex,判断二级dex是否需要安装;
如果需要安装则会从apk中解压出classes2.dex并将其拷贝到应用的/data/data//code_cache/secondary-dexes/目录下;
通过反射将classes2.dex等注入到当前的ClassLoader的pathList中,完成整体安装流程。

MultiDex安装过程源码分析

MultiDex 编译过程

ART虚拟机是如何内建支持MultiDex的?

Android MultiDex实现原理解析

Android分包MultiDex源码分析

MultiDex工作原理分析和优化方案

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注