学习MultiDex库如何动态加载dex或apk

记得还是很多年以前,某个新增功能需要使用一个第三方jar包,一顿操作猛入虎准备交活,编译报错了,大致是 dex文件方法数超过了65536 ,自然而然认识了MultiDex,虽然最终是通过proguard代码缩减解决的。

知识回顾:

Android 程序一般使用 Java 语言开发,但是 Dalvik 等虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行额外处理,最终生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式,但是一个dex文件最多只能有65536个方法,也就是我们常说的64K方法数限制,随着app的使用场景越来越复杂,即使进行了代码缩减,也很难在一个dex 文件中包含全部功能。所以我们需要把app编译成包含多个dex文件,就不会有方法数超过65536的编译错误了。

Android 4.4 及以下采用的Dalvik 虚拟机,只能执行做过 OPT 优化的 DEX 文件,也就是 ODEX 文件。一个 APK 在安装的时候,其中的classes.dex会自动做 ODEX 优化,并在启动的时候由系统默认直接加载到 APP 的PathClassLoader里面,因此classes.dex中的类肯定能直接访问,不需要我们操心。

除它之外的 DEX 文件,也就是classes2.dex、classes3.dex、classes4.dex等 DEX 文件(这里我们统称为 Secondary DEX 文件),这些文件都需要靠我们自己进行 ODEX 优化,并加载到 PathClassLoader 里,才能正常使用其中的类。否则在访问这些类的时候,就会抛出ClassNotFound异常从而引起崩溃。

dex怎么加载到PathClassLoader?app运行时怎么从dex加载类?

文章内容基于Android 34 sdk sources, multidex 2.0.1,当前版本的PathClassLoader类图简化如下,包含了涉及到的类、属性和方法。Dex经过makeDexElements加载后赋值给DexPathList的dexElements,应用的类也是从这个列表按顺序查找加载。

应用启动时系统会创建PathClassLoader,在父类构造函数里初始化了一个DexPathList对象,参数dexPath是可以包含classes和资源的jar/apk/zip/dex文件路径,多个路径用冒号分开, 一般默认就只有 APP的安装包路径,比如:

dexPath= /data/app/~~fmvqN9xs10CVaMI2ZRR4Q==/com.bestjoy.app.tv-KhBnl6h3u9URR4Wo0rdXMQ==/base.apk

DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();

    // save dexPath for BaseDexClassLoader
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions, definingContext, isTrusted);

}

makeDexElements方法就是关键,会将dexPath中的每个file调用loadDexFile加载成DexFile,最终形成Element[]赋值给dexElements。

系统创建的PathClassLoader默认optimizedDirectory是null的, 优化后的dex会存放在系统指定的位置,比如dalvik-cache目录,  所以这里直接new DexFile。

openDexFile方法是一个native方法,就不继续分析了,我们就把mCookie当做是打开的dex文件的引用理解好了。

经过上面的步骤,DexPathList.dexElements已经初始化好了,后续运行加载需要的类的链路可以用下图表示:

DexPathList.findClass中dexElements循环调用element.findClass

实际上就是调用的DexFile.loadClassBinaryName

上面这一堆就是要明确一件事,一个类在加载前,其所在的dex文件必须先加载到dexPathList.dexElements中,否则运行时就会报ClassNotFound异常

为此,Android 官方推出了 MultiDex 方案,它的实现原理就是帮我们解开 APK 包,对第二个以后的 DEX 文件做 ODEX 优化并加载到dexPathList.dexElements里,这样带有多个 DEX 文件的 APK 就能正常运行了。

使用这个库只需要在 APP 程序执行最早的入口,也就是Application.attachBaseContext里面直接调MultiDex.install就够了。

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
        MultiDex.install(base)

}

看上去很简单,实现原理却值得学习,可以借鉴来实现动态加载外部库。

既然apk只有一个classes.dex的时候可以调用系统提供的方法加载到PathClassLoader,那我们就把其他的dex每个单独保存成一个只有单个classes.dexzipapk实际上也是zip),然后也调用相同的方法加载并追加到PathClassLoader中去就可以了。

doInstallation实际上也是这样做的,分为MultiDexExtractor.load和installSecondaryDexes两个步骤。

第一步,MultiDexExtractor.load

解压APK里classes2.dex、classes3.dex、classes4.dex等DEX 文件,分别进行 ZIP 压缩,保存为只有单个classes.dex文件条目的classesN.zip, 全部转换后记录下此时sourceApk的crc和时间戳。

比如此时我们可以看到 APP 的 code_cache 目录下会有类似的文件:

 /data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes2.zip

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes3.zip

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes4.zip

生成zips是一个耗时操作,load不会每次都做一遍,会先比较此时sourceApk的crc值和时间戳是否和上次记录的一致,只有不一致才重新生成zips。

生成zips是在performExtractions()方法,从classes2.dex开始, 之后数字每次递增1,查询apk中是否存在该文件条目dexFile,不存在则说明没有dex文件了,转换完成;否则对存在的dexFile调用extract(apk, dexFile, extractedFile, extractedFilePrefix)

经过extract后,APP中的classesN.dex已经保存为base.apk.classesN.zip了。

这些Zip随后会交给下一步处理。

第二步,installSecondaryDexes

知识点回顾里有提到如何调用系统方法加载apk中的classes.dex,这里也一样,只不过我们需要反射调用系统方法。

以V19.install(loader, files, dexDir)为例,我们先再看一下类图, 目标是把第一步那些zip加载追加到DexPathList的dexElements里。

先反射获得当前PathClassLoader对象的pathList值(DexPathList对象), 然后makeDexElements方法会反射调用DexPathListde的makeDexElements方法加载zip。

比如此时我们可以看到 APP 的 code_cache 目录下会有多出来的.dex文件:

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes2.zip

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes2.dex

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes3.zip

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes3.dex

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes4.zip

/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes4.dex

接着在expandFieldArray方法里将新加载的dexElements和原本pathList中已有的dexElements合并,合集重新设置给pathList.dexElements。

 2024.04.09更新

Multidex在应用安装或更新后首次启动,存在严重的性能问题,用户需要等待相当长的时间才能正常使用APP,所以推荐字节跳动开源的BoostMultidex框架,带来了显著的性能提升,这里是一个对比结果: 有兴趣的可以前往【传送门】。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值