我们知道,Android 低版本(4.X 及以下,SDK < 21)的设备,采用的 Java 运行环境是 Dalvik 虚拟机。它相比于高版本,最大的问题就是在安装或者升级更新之后,首次冷启动的耗时漫长。这常常需要花费几十秒甚至几分钟,用户不得不面对一片黑屏,熬过这段时间才能正常使用 APP。
这是非常影响用户的使用体验的。我们从线上数据也可以发现,Android 4.X 及以下机型,其新增用户也占了一定的比例,但留存用户数相比新增则要少非常多。尤其在海外,像东南亚以及拉美等地区,还存有着很大量的低端机。4.X 以下低版本用户虽然比较少,但对于抖音及 TikTok 这样有着亿级规模的用户的 APP,即使占比 10%,数目也有上千万。因此如果想要打通下沉市场,这部分用户的使用和升级体验是绝对无法忽视的。
这个问题的根本原因就在于,安装或者升级后首次 MultiDex 花费的时间过于漫长。为了解决这个问题,我们挖掘了 Dalvik 虚拟机的底层系统机制,对 DEX 相关处理逻辑进行了重新设计,最终推出了 BoostMultiDex 方案,它能够减少 80%以上的黑屏等待时间,挽救低版本 Android 用户的升级安装体验。
我们先来简单看一个安装后首次冷启动加载 DEX 时间的对比数据:
可以看到原始 MultiDex 方案竟然花了半分钟以上才能完成 DEX 加载,而 BoostMultiDex 方案的时间仅需要 5 秒以内。优化效果极为显著!
接下来,我们就来详细讲解整个 BoostMultiDex 方案的研发过程与解决思路。
起因
我们先来看下导致这个问题的根本原因。这里面是有多个原因共同引起的。
首先需要清楚的是,在 Java 里面想要访问一个类,必然是需要通过 ClassLoader 来加载它们才能访问到。在 Android 上,APP 里面的类都是由PathClassLoader负责加载的。而类都是依附于 DEX 文件而存在的,只有加载了相应的 DEX,才能对其中的类进行使用。
Android 早期对于 DEX 的指令格式设计并不完善,单个 DEX 文件中引用的 Java 方法总数不能超过 65536 个。
对于现在的 APP 而言,只要功能逻辑多一些,很容易就会触达这个界限。
这样,如果一个 APP 的 Java 代码的方法数超过了 65536 个,这个 APP 的代码就无法被一个 DEX 文件完全装下,那么,我们在编译期间就不得不生成多个 DEX 文件。我们解开抖音的 APK 就可以看到,里面确实包含了很多个 DEX 文件:
8035972 00-00-1980 00:00 classes.dex
8476188 00-00-1980 00:00 classes2.dex
7882916 00-00-1980 00:00 classes3.dex
9041240 00-00-1980 00:00 classes4.dex
8646596 00-00-1980 00:00 classes5.dex
8644640 00-00-1980 00:00 classes6.dex
5888368 00-00-1980 00:00 classes7.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 文件(这里我们统称为 Secondary DEX 文件),这些文件都需要靠我们自己进行 ODEX 优化,并加载到 ClassLoader 里,才能正常使用其中的类。否则在访问这些类的时候,就会抛出ClassNotFound异常从而引起崩溃。
因此,Android 官方推出了 MultiDex 方案。只需要在 APP 程序执行最早的入口,也就是Application.attachBaseContext里面直接调MultiDex.install,它会解开 APK 包,对第二个以后的 DEX 文件做 ODEX 优化并加载。这样,带有多个 DEX 文件的 APK 就可以顺利执行下去了。
这个操作会在 APP 安装或者更新后首次冷启动的时候发生,正是由于这个过程耗时漫长,才导致了我们最开始提到的耗时黑屏问题。
原始实现
了解了这个背景之后,我们再来看 MultiDex 的实现,逻辑就比较清晰了。
首先,APK 里面的所有classes2.dex、classes3.dex、classes4.dex等 DEX 文件都会被解压出来。
然后,对每个 dex 进行 ZIP 压缩。生成 classesN.zip 文件。
接着,对每个 ZIP 文件做 ODEX 优化,生成 classesN.zip.odex 文件。
具体而言,我们可以看到 APP 的 code_cache 目录下有这些文件:
com.bytedance.app.boost_multidex-1.apk.classes2.dex
com.bytedance.app.boost_multidex-1.apk.classes2.zip
com.bytedance.app.boost_multidex-1.apk.classes3.dex
com.bytedance.app.boost_multidex-1.apk.classes3.zip
com.bytedance.app.boost_multidex-1.apk.classes4.dex
com.bytedance.app.boost_multidex-1.apk.classes4.zip
这一步是通过DexFile.loadDex方法实现的,只需要指定原始 ZIP 文件和 ODEX 文件的路径,就能够根据 ZIP 中的 DEX 生成相应的 ODEX 产物,这个方法会最终返回一个DexFile对象。
最后,APP 把这些DexFile对象都添加到PathClassLo