为啥现在 Android App 不用手动搞 MultiDex 了?

一、小明的 "仓库危机":MultiDex 的由来

咱们先从一个程序员小明的故事说起。

3 年前,小明刚接手一个购物 App,加了支付、地图、推送、统计等七八个第三方库后,编译时突然蹦出个报错:Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

小明懵了,查了半天才知道:早期 Android 用的 "Dalvik 虚拟机",像个死板的仓库管理员 ------ 它只能管一个 "DEX 仓库",而这个仓库的 "货架上限" 是 65536 个(2¹⁶,也就是常说的 64K) 。这里的 "货架" 就是 DEX 文件里的method_ids(方法引用),每个方法都要占一个货架位,超过 65536 就装不下了。

没办法,小明只能用上谷歌给的 "MultiDex 工具"------ 相当于给仓库管理员配了个 "副仓库钥匙",让它能去隔壁几个小仓库拿东西。但配置起来超麻烦:要改 Gradle、加依赖、改 Application,还得处理低版本兼容...

二、为啥现在小明不用折腾了?3 个核心原因

今年小明升级了项目,把minSdkVersion调到 21(对应 Android 5.0),发现之前的 MultiDex 配置全删了,App 照样跑得飞起。这不是魔法,而是 Android 底层和工具链的 "双重革命":

革命选手 核心能力 对小明的意义
ART 虚拟机 能管多个 "DEX 仓库",还能提前把所有仓库的货整理成 "优化包" 不用手动给管理员配钥匙了
AGP(Android Gradle 插件) 自动把超标的方法拆成多个 DEX,不用写配置 不用手动分仓库了
minSdkVersion 升高 现在多数 App 支持 Android 5.0+,默认用 ART 不用兼容死板的 Dalvik 了

三、深挖原理:从 "手动开锁" 到 "自动管理"

要搞懂区别,得先看Dalvik 时代的 MultiDex 是怎么 "手动干活" 的 ,再对比ART 时代是怎么 "自动躺平" 的

3.1 先搞懂:为啥单个 DEX 只能装 64K 方法?

DEX 文件格式里,method_ids(方法引用表)的每个条目用16 位整数索引(范围 0~65535)。就像仓库货架编号只有 5 位,最多编到 99999,超过就没法编号了 ------ 这是硬格式限制,不是虚拟机 "不想装",是 "没法认"。

3.2 Dalvik 时代:MultiDex 的 "手动开锁" 原理

Dalvik 虚拟机的PathClassLoader(类加载器)默认只加载 APK 里的classes.dex(主仓库),其他副 DEX(比如classes2.dexclasses3.dex)它根本看不见。

MultiDex 库的核心操作,就是用反射 "撬" 开 ClassLoader 的配置,把副 DEX 加进去

代码还原:早年的 MultiDex 配置

  1. Gradle 里开开关、加依赖
groovy 复制代码
android {
    defaultConfig {
        applicationId "com.xiaoming.shop"
        minSdk 19 // 低于21,必须手动配
        targetSdk 33
        multiDexEnabled true // 开启多DEX拆分
    }
}

dependencies {
    // 引入MultiDex库(AndroidX版本)
    implementation "androidx.multidex:multidex:2.0.1"
}
  1. Application 里 "手动开锁"
java 复制代码
public class ShopApp extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        // 关键:手动让MultiDex"安装"副DEX
        MultiDex.install(this);
    }
}

MultiDex.install () 干了啥?(简化版核心逻辑)

相当于 "偷偷改管理员的仓库列表":

java 复制代码
public class MultiDex {
    public static void install(Context context) {
        // 1. 从APK里解压出副DEX(classes2.dex等),放到手机本地目录
        List<File> secondaryDexFiles = extractSecondaryDexes(context);
        
        // 2. 拿到当前的类加载器(PathClassLoader)
        PathClassLoader classLoader = (PathClassLoader) context.getClassLoader();
        
        try {
            // 3. 反射获取ClassLoader里存DEX路径的"dexElements"数组
            Field dexElementsField = findField(classLoader, "dexElements");
            Object[] oldDexElements = (Object[]) dexElementsField.get(classLoader);
            
            // 4. 把副DEX转换成对应的"Element"对象(仓库钥匙)
            Object[] newDexElements = makeDexElements(classLoader, secondaryDexFiles);
            
            // 5. 合并新旧数组:把副DEX的钥匙加到管理员的钥匙串里
            Object[] mergedDexElements = mergeArrays(oldDexElements, newDexElements);
            
            // 6. 反射把新数组塞回ClassLoader
            dexElementsField.set(classLoader, mergedDexElements);
            
        } catch (Exception e) {
            throw new RuntimeException("MultiDex加载失败", e);
        }
    }
}

这操作很 "hack"------ 相当于绕过系统限制改配置,还可能在低版本系统出兼容问题(比如 Android 4.4 以下的 Dalvik 有 bug)。

3.3 ART 时代:"自动管理" 的底层逻辑

Android 5.0(API 21)后,ART 虚拟机彻底取代 Dalvik,它解决问题的思路不是 "手动加钥匙",而是 "重构仓库系统":

核心变化 1:安装时 "提前整理货物"(AOT 编译)

当你安装 App 时,系统会启动一个叫dex2oat的工具,把 APK 里所有的 DEX 文件(classes.dexclasses2.dex...)编译成一个 OAT 文件 (优化后的二进制文件,存放在/data/dalvik-cache/)。

OAT 文件里包含:

  • 所有 DEX 的方法、类的机器码(不用运行时再解释);
  • 原始 DEX 信息(兼容极少数需要动态加载的场景)。

相当于 "仓库管理员" 在你用之前,就把所有小仓库的货搬到一个 "超级大仓库"(OAT)里,还按使用频率整理好了 ------ 运行时直接从这个大仓库拿东西,根本不用管原来有多少个 DEX。

核心变化 2:ClassLoader "天然认多 DEX"

ART 的PathClassLoader不再只认classes.dex,而是能直接读取 OAT 文件里的所有 DEX 信息。就算没编译成 OAT(比如动态下载的 DEX),ART 也支持直接加载多个 DEX 文件,不用反射修改配置。

代码对比:现在的配置(minSdk ≥21)

groovy 复制代码
android {
    defaultConfig {
        applicationId "com.xiaoming.shop"
        minSdk 21 // 关键:≥21默认用ART
        targetSdk 33
        // 不用写multiDexEnabled true!AGP自动拆DEX
    }
}

// Application正常写,不用继承MultiDexApplication,不用调用install()
public class ShopApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 该干嘛干嘛,不用管DEX
    }
}

四、时序图:两种时代的 DEX 加载流程

用时序图更直观地看区别,我们用mermaid语法画出来(小白也能看懂):

4.1 Dalvik 时代 + 手动 MultiDex

rust 复制代码
sequenceDiagram
    participant 系统 as 安卓系统(Dalvik)
    participant 应用 as 小明的购物App
    participant MultiDex库 as MultiDex工具库
    participant ClassLoader as PathClassLoader(类加载器)
    
    系统->>应用: 启动Application,调用attachBaseContext()
    应用->>MultiDex库: 调用MultiDex.install(this)
    MultiDex库->>应用: 从APK解压副DEX到本地目录
    MultiDex库->>ClassLoader: 反射获取dexElements数组(旧)
    MultiDex库->>MultiDex库: 把副DEX转换成Element对象
    MultiDex库->>MultiDex库: 合并新旧dexElements数组
    MultiDex库->>ClassLoader: 反射更新dexElements数组
    系统->>应用: 调用Application.onCreate()
    应用->>ClassLoader: 加载类(主+副DEX都能找到)

4.2 ART 时代 + AGP 自动处理

rust 复制代码
sequenceDiagram
    participant 系统 as 安卓系统(ART)
    participant dex2oat as 系统编译工具
    participant 应用 as 小明的购物App(AGP已拆DEX)
    participant ClassLoader as PathClassLoader(类加载器)
    
    系统->>dex2oat: 用户安装APK,触发编译
    dex2oat->>dex2oat: 把所有DEX(classes1~n.dex)编译成OAT文件
    dex2oat->>系统: 保存OAT文件到/data/dalvik-cache/
    系统->>应用: 启动Application
    系统->>ClassLoader: 初始化ClassLoader,指向OAT文件
    ClassLoader->>系统: 读取OAT文件中的所有DEX信息
    系统->>应用: 调用Application.onCreate()
    应用->>ClassLoader: 加载类(直接从OAT读取,无需额外处理)

五、总结:不用 MultiDex 的本质是 "工具替你干活了"

现在不用手动配置 MultiDex,不是因为 App 的方法数变少了(反而现在 App 更复杂,方法数动辄上百万),而是:

  1. 底层虚拟机升级:ART 从 "只能管一个仓库" 变成 "能提前整理所有仓库";
  2. 构建工具优化:AGP 自动拆分 DEX,不用开发者写配置;
  3. 系统版本迭代 :多数 App 的minSdkVersion≥21,不用兼容 Dalvik。

就像小明从 "手动分货、递钥匙",变成 "雇了个超级管家(ART+AGP),自动分货、整理仓库"------ 开发者终于能专注于写业务,不用再跟底层 DEX 较劲了~

相关推荐
踢球的打工仔3 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人4 小时前
安卓socket
android
安卓理事人9 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学11 小时前
Android M3U8视频播放器
android·音视频
q***577411 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober11 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿12 小时前
关于ObjectAnimator
android
zhangphil13 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我14 小时前
从头写一个自己的app
android·前端·flutter
lichong95115 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端