Android MultiDex 源码分析

本文介绍了如何在Android应用中启用MultiDex解决65536方法数限制,包括API 21及以上版本的内置支持、旧版配置方法,以及MultiDex的限制、如何在主dex包含必要类,源码级的MultiDex安装过程和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");
        // 虚拟机版本号大于等 2.1 本身支持分 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 = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值