Android开发

Android MultiDex 源码分析

本文主要是介绍Android MultiDex 源码分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、启用 MultiDex
    • Android 5.0 和之后的版本
    • Android 5.0 之前的版本
  • 二、MultiDex 的限制
  • 三、在主 dex 文件中包含必要的类
  • 四、源码分析
  • BoostMultiDex


前言

App 在迭代过程中功能越来越丰富,代码量越来越多就会遇到一个构建错误表示方法数超出 65536 出现这个问题的原因是 dex 文件中对方法数的索引是 short 类型。在计算机科学领域,术语 K 表示1024(或2^10)。因为 65,536 等于 64 X 1024,所以这个限制称为 ‘64K引用限制’ 为了解决这个限制 Google 推出了 MultiDex

一、启用 MultiDex

Android 5.0 和之后的版本

Android 5.0(API 级别 21)及更高版本使用 Android Runtime (ART),它本身支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,这会扫描查找 classesN.dex 文件,并将它们编译成单个 .oat 文件以供 Android 设备执行,所以如果项目的 minSdkVersion 大于等于 21 则不需要 MultiDex 并且系统会默认启用 MultiDex

Android 5.0 之前的版本

    defaultConfig {
        multiDexEnabled true
    }

	dependencies {
		implementation 'androidx.multidex:multidex:2.0.1'
	}

通过以上配置引入和开启 MultiDex 然后根据是否扩展 Application 配置 android:name

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <application
            android:name="androidx.multidex.MultiDexApplication" >
        ...
    </application>
</manifest>

如果扩展了 Application 则把 Application 更改为 MultiDexApplication 更多的做法是在 Application 的 attachBaseContext 方法中调用 MultiDex.install(this)

class MyApplication : SomeOtherApplication() {

    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        MultiDex.install(this)
    }
}

之后再构建应用时 Android 构建工具会根据需要构建多个 dex 文件并且构建系统会将所有 DEX 文件都打包到 APK 中

二、MultiDex 的限制

  1. 在冷启动时因为需要安装 dex 文件,如果 dex 文件过大处理时间过长容易引发 ANR
  2. linearAlloc 限制,即使方法数没有超过 65536 能正常编译打包成 apk 在安装的时候也有可能会提示 INSTALL_FAILED_DEXOPT 而导致安装失败,这个一般就是因为 linearAlloc 的限制导致的。主要是因为 Dexopt 使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在 Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成 dexopt 崩溃.

针对问题一可通过在子线程或者其他进程初始化来解决
针对问题二可通过 --set-max-idx-number= 参数控制每一个dex 的最大方法个数,写小一点可以产生多个 dex 为了避免 2.3 机型 runtime 的 linearAlloclimit 最好保持每一个 dex 体积小于 4M 比如 value <= 48000 具体如下:

android.applicationVariants.all {
    variant ->
        dex.doFirst{
            dex->
            if (dex.additionalParameters == null) {
                dex.additionalParameters = []
            }
                dex.additionalParameters += '--set-max-idx-number=48000'

       }
}

代码缩减可以减少甚至有可能避开这些问题

三、在主 dex 文件中包含必要的类

如果 App 启动过程中需要的类没有包含在主 dex 文件中会发生 java.lang.NoClassDefFoundError 错误这个时候需要使用 multiDexKeepFile 或 multiDexKeepProguard 属性声明这些其他类,手动将这些类包含在主 dex 文件中

multiDexKeepFile 创建 multidex-config.txt 文件内容如下:

com/example/MyClass.class
com/example/MyOtherClass.class

然后引入该文件

android {
    buildTypes {
        release {
            multiDexKeepFile file('multidex-config.txt')
            ...
        }
    }
}

multiDexKeepProguard 文件使用与 Proguard 相同的格式,并且支持全部 Proguard 语法,创建一个名为 multidex-config.pro 的文件内容如下:

-keep class com.example.MyClass
-keep class com.example.MyClassToo

-keep class com.example.** { *; } // All classes in the com.example package

然后应用该文件

android {
    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            ...
        }
    }
}

四、源码分析

Dalvik 虚拟机将 App 限制为每个 APK 只能使用一个 classes.dex 字节码文件,Android 4.4 及以下采用的是 Dalvik 虚拟机,在通常情况下,Dalvik 虚拟机只能执行做过 OPT 优化的 DEX 文件,也就是我们常说的 ODEX 文件。一个 APK 在安装的时候,其中的 classes.dex 会自动做 ODEX 优化,并在启动的时候由系统默认直接加载到 APP 的 PathClassLoader 里面,因此classes.dex 中的类肯定能直接访问,不需要我们操心。除它之外的 DEX 文件,也就是classes2.dex、classes3.dex、classes4.dex 等 DEX 文件(次 dex 文件),这些文件都需要靠我们自己进行 ODEX 优化,并加载到 ClassLoader 里,才能正常使用其中的类。否则在访问这些类的时候,就会抛出 ClassNotFound 异常从而引起崩溃,因此 Android 官方推出了 MultiDex 只需要在 APP 程序执行最早的入口,也就是Application.attachBaseContext 里面直接调 MultiDex.install 它会解开 APK 包,对第二个以后的 DEX 文件做 ODEX 优化并加载。这样,带有多个 DEX 文件的 APK 就可以顺利执行下去了。这个操作会在 APP 安装或者更新后首次冷启动的时候发生,正是由于这个过程耗时漫长,才导致了我们上面提到的 ANR 问题。下面开始分析源码:

    public static void install(Context context) {
        Log.i(TAG, "Installing application");
        // 高版本(5.0 及以上)本身支持分 dex 不需要 MultiDex
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
            return;
        }

		// 如果 API 版本小于 4 则抛出异常
        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
            throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
                    + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
        }

        try {
      		// 获取 ApplicationInfo
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
              Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
                  + " MultiDex support library is disabled.");
              return;
            }

            doInstallation(context,
            		// /data/app/[包名]-mbddlJ7I2IuVAyb6lj-Afg==/base.apk
                    new File(applicationInfo.sourceDir),
                    // /data/user/0/[包名]
                    new File(applicationInfo.dataDir),
                    CODE_CACHE_SECONDARY_FOLDER_NAME,
                    NO_KEY_PREFIX,
                    true);

        } catch (Exception e) {
            Log.e(TAG, "MultiDex installation failure", e);
            throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
        }
        Log.i(TAG, "install done");
    }

做了一些校验工作调用 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 {
        synchronized (installedApk) {
            if (installedApk.contains(sourceApk)) {
                return;
            }
            installedApk.add(sourceApk);

			// 如果 API 版本大于 20 输出警告日志
            if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
                Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
                        + Build.VERSION.SDK_INT + ": SDK version higher than "
                        + MAX_SUPPORTED_SDK_VERSION + " 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") + "\"");
            }

            // dalvik.system.PathClassLoader
            ClassLoader loader = getDexClassloader(mainContext);
            if (loader == null) {
                return;
            }

            try {
              // 清除 /data/user/0/[包名]/files/secondary-dexes 目录 
              clearOldDexDir(mainContext);
            } catch (Throwable t) {
              Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                  + "continuing without cleaning.", t);
            }

			// dex 文件目录 /data/user/0/[包名]/code_cache/secondary-dexes
            File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
            // MultiDexExtractor is taking the file lock and keeping it until it is closed.
            // Keep it open during installSecondaryDexes and through forced extraction to ensure no
            // extraction or optimizing dexopt is running in parallel.
            MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
            IOException closeException = null;
            try {
            	// 从 apk 文件中提取 dex 文件
                List<? extends File> files =
                        extractor.load(mainContext, prefsKeyPrefix, false);
                try {
                	// 安装 dex 文件让 classloader 可以访问到 class
                    installSecondaryDexes(loader, dexDir, files);
                // Some IOException causes may be fixed by a clean extraction.
                } catch (IOException e) {
                    if (!reinstallOnPatchRecoverableException) {
                        throw e;
                    }
                    Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
                            + "forced extraction", e);
                    // 发生异常强制重新提取
                    files = extractor.load(mainContext, prefsKeyPrefix, true);
                    // 安装 dex 文件让 classloader 可以访问到 class
                    installSecondaryDexes(loader, dexDir, files);
                }
            } finally {
                try {
                    extractor.close();
                } catch (IOException e) {
                    // Delay throw of close exception to ensure we don't override some exception
                    // thrown during the try block.
                    closeException = e;
                }
            }
            if (closeException != null) {
                throw closeException;
            }
        }
    }

先看一下提取 dex 文件的过程

    List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
            throws IOException {
        Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
                prefsKeyPrefix + ")");

        if (!cacheLock.isValid()) {
            throw new IllegalStateException("MultiDexExtractor was closed");
        }

        List<ExtractedDex> files;
        // 如果不是强制重新提取并且没有修改(覆盖安装)
        if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
            try {
            	// 加载已经存在的 dex 文件
                files = loadExistingExtractions(context, prefsKeyPrefix);
            } catch (IOException ioe) {
                Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
                        + " falling back to fresh extraction", ioe);
                // 发生异常重新提取
                files = performExtractions();
                // 保存提取后的信息下次使用
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                        files);
            }
        } else {
            if (forceReload) {
                Log.i(TAG, "Forced extraction must be performed.");
            } else {
                Log.i(TAG, "Detected that extraction must be performed.");
            }
            // 否则重新提取
            files = performExtractions();
            // 保存提取后的信息下次使用
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                    files);
        }

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

先看一下第一次提取的过程

    private List<ExtractedDex> performExtractions() throws IOException {
		// base.apk.classes
        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;

		// 清空上面提到的 dex 文件目录 /data/user/0/[包名]/code_cache/secondary-dexes
		// 除了 MultiDex.lock 文件
        clearDexDir();

        List<ExtractedDex> files = new ArrayList<ExtractedDex>();
		
		// apk 文件
        final ZipFile apk = new ZipFile(sourceApk);
        try {

            int secondaryNumber = 2;
			// 获取 apk 文件中的 classes2.dex 文件
            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            while (dexFile != null) {
                // 文件名 base.apk.classes2.zip
                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
                // /data/user/0/[包名]/code_cache/secondary-dexes/base.apk.classes2.zip
                ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
                // 将 dex 文件加入文件列表
                files.add(extractedFile);

                Log.i(TAG, "Extraction is needed for file " + extractedFile);
                // 提取 dex 文件的次数
                int numAttempts = 0;
                // 是否提取成功标记
                boolean isExtractionSuccessful = false;
                // 一个 dex 文件最多提取 3 次
                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                    numAttempts++;

                    // 真正的提取工作在这里
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);

                    // Read zip crc of extracted dex
                    try {
                        extractedFile.crc = getZipCrc(extractedFile);
                        isExtractionSuccessful = true;
                    } catch (IOException e) {
                        isExtractionSuccessful = false;
                        Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
                    }

                    // Log size and crc of the extracted zip file
                    Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
                            + " '" + extractedFile.getAbsolutePath() + "': length "
                            + extractedFile.length() + " - crc: " + extractedFile.crc);
                    if (!isExtractionSuccessful) {
                        // Delete the extracted file
                        extractedFile.delete();
                        if (extractedFile.exists()) {
                            Log.w(TAG, "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++;
                // 提取下一个 dex 文件
                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            }
        } finally {
            try {
                apk.close();
            } catch (IOException e) {
                Log.w(TAG, "Failed to close resource", e);
            }
        }

        return files;
    }

看一下真正的做提取工作的方法

    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
            String extractedFilePrefix) throws IOException, FileNotFoundException {

        InputStream in = apk.getInputStream(dexFile);
        ZipOutputStream out = null;
        // 创建临时文件 /data/user/0/[包名]/code_cache/secondary-dexes/tmp-base.apk.classes.zip
        File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
                extractTo.getParentFile());
        Log.i(TAG, "Extracting " + tmp.getPath());
        try {
            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
            try {
                ZipEntry classesDex = new ZipEntry("classes.dex");
                // keep zip entry time since it is the criteria used by Dalvik
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);

                byte[] buffer = new byte[BUFFER_SIZE];
                int length = in.read(buffer);
                while (length != -1) {
                    out.write(buffer, 0, length);
                    length = in.read(buffer);
                }
                out.closeEntry();
            } finally {
                out.close();
            }
            if (!tmp.setReadOnly()) {
                throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
                        "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
            }
            Log.i(TAG, "Renaming to " + extractTo.getPath());
            // 重命名 /data/user/0/[包名]/code_cache/secondary-dexes/base.apk.classes2.zip
            if (!tmp.renameTo(extractTo)) {
                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
                        "\" to \"" + extractTo.getAbsolutePath() + "\"");
            }
        } finally {
            closeQuietly(in);
            tmp.delete(); // return status ignored
        }
    }

就是将 dex 文件压缩为 zip 文件,将所有的 dex 文件都压缩为 zip 文件后返回保存信息到 multidex.version 文件中(sp)下一次直接加载存在的 zip 文件

    private List<ExtractedDex> loadExistingExtractions(
            Context context,
            String prefsKeyPrefix)
            throws IOException {
        Log.i(TAG, "loading existing secondary dex files");

		// base.apk.classes
        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
        // 刚保存了信息的 multidex.version
        SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
        // dex 文件数量
        int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1);
        // 个数去掉主 dex 文件(classes.dex)
        final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
		// 从 classes2.dex 文件开始
        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
        	// 文件名是上一步提取后保存的 base.apk.classes2.zip
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            // 创建文件添加到文件列表
            ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
            if (extractedFile.isFile()) {
                extractedFile.crc = getZipCrc(extractedFile);
                long expectedCrc = multiDexPreferences.getLong(
                        prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE);
                long expectedModTime = multiDexPreferences.getLong(
                        prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE);
                long lastModified = extractedFile.lastModified();
                if ((expectedModTime != lastModified)
                        || (expectedCrc != extractedFile.crc)) {
                    throw new IOException("Invalid extracted dex: " + extractedFile +
                            " (key \"" + prefsKeyPrefix + "\"), expected modification time: "
                            + expectedModTime + ", modification time: "
                            + lastModified + ", expected crc: "
                            + expectedCrc + ", file crc: " + extractedFile.crc);
                }
                files.add(extractedFile);
            } else {
                throw new IOException("Missing extracted secondary dex file '" +
                        extractedFile.getPath() + "'");
            }
        }

        return files;
    }

现在两种路径都拿到了 zip 文件列表之后就是将这些文件加入到 classloader 了

    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 (Build.VERSION.SDK_INT >= 19) {
                V19.install(loader, files, dexDir);
            } else if (Build.VERSION.SDK_INT >= 14) {
                V14.install(loader, files);
            } else {
                V4.install(loader, files);
            }
        }
    }

区分了版本这里只看一下 Android API 19 的处理

    private static final class V19 {

		// loader:dalvik.system.PathClassLoader
		// additionalClassPathEntries:所有的 dex 文件压缩后的 zip 文件
		// optimizedDirectory:所有的 dex 文件压缩后的 zip 文件目录
        static void install(ClassLoader loader,
                List<? extends File> additionalClassPathEntries,
                File optimizedDirectory)
                        throws IllegalArgumentException, IllegalAccessException,
                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                        IOException {
			// 反射获取父类的 dalvik.system.DexPathList pathList 字段
            Field pathListField = findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            // 先通过反射调用 makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, suppressedExceptions) 拿到一个 Element[] 
            // 然后再通过反射将 element 数组添加到 dexPathList 的 dexElements 字段中,这样 classloader 就可以找到其他 dex 文件中的类了
            // 这里与 QQ 空间的热修复方案很相似 QQ 空间的做法是把修复后的类放到修复前的类前面保证优先加载修复后的字节码文件
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makeDexElement", e);
                }
                Field suppressedExceptionsField =
                        findField(dexPathList, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions =
                        (IOException[]) suppressedExceptionsField.get(dexPathList);

                if (dexElementsSuppressedExceptions == null) {
                    dexElementsSuppressedExceptions =
                            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(suppressedExceptions.get(0));
                throw exception;
            }
        }

        /**
         * A wrapper around
         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
         */
        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                        throws IllegalAccessException, InvocationTargetException,
                        NoSuchMethodException {
            Method makeDexElements =
                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                            ArrayList.class);

            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }

在 App 安装过程中会通过 dexopt 工具(函数)对 classes.dex 做 odex 优化工作所以不需要对主 dex 文件做处理但是其他次 dex 文件的 odex 优化工作就需要 mutlidex 来做了,具体的工作在 dalvik.system.DexPathList#makeDexElements 方法里

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
        return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false);
    }


    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

上面是 9.0 的源码(查看老版本是没有最后一个 ClassLoader 参数的,但后续流程都是差不多的)

    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }
    }

不管 optimizedDirectory 参数是否为 null 最后都会调用 DexFile 的构造方法,在构造方法里调用 openDexFile 方法再调用 openDexFileNative 方法在这个方法里做真正的 odex 的优化工作

最后一点为什么把 odex 文件加入到 dalvik.system.DexPathList#dexElements 总就可以了涉及到了 Android 的类加载

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

PathClassLoader 继承自 BaseDexClassLoader

上面是 BaseDexClassLoader 的加载 class 的方法可以看到直接调用了 dalvik.system.DexPathList#findClass 看一下这个方法

	// dalvik.system.DexPathList#findClass
    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
    
    // dalvik.system.DexPathList.Element#findClass
        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

这个方法就是遍历的上面操作的 Element 数组在每一个 dex 文件里查找对应的 class 所以通过反射将 dex 文件加入到 elements 数组中就可以找到所有的 class 了

BoostMultiDex

以上就是 MultiDex 的基本流程了,但是 MultiDex 在第一次冷启动时做了大量的 io 操作导致启动缓慢,其中会先压缩 dex 文件为 zip 文件再解压 zip 文件进行 odex 优化最后加载优化后的 odex 启动,抖音针对这种情况发布了新的开源解决方案 BoostMultiDex 它做的事情主要是在首次启动时直接加载未优化的 dex 文件完成启动然后再开启一个后台线程去做 odex 的优化工作,之后再次启动的时候如果已经做完 odex 优化的工作则加载优化后的 dex 文件去运行,这样既可以保证第一次启动时的时长又可以在再次启动的时候加载优化后的 dex 保证性能

这篇关于Android MultiDex 源码分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!