抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减少80%

APP 收到 SIGSTKFLT 信号崩溃了,同时还输出了这样的日志:

06-25 15:10:53.821 7449 7450 E dalvikvm: threadid=2: stuck on threadid=135, giving up

06-25 15:10:53.821 7449 7450 D dalvikvm: threadid=2: sending two SIGSTKFLTs to threadid=135 (tid=8021) to cause debuggerd dump

SIGSTKFLT 是 Dalvik 虚拟机特有的一个信号。当虚拟机发生了 ANR 或者需要做 GC 的时候,就需要挂起所有 RUNNING 状态的线程,如果此时 Dalvik 虚拟机等待了足够长时间,线程仍旧无法被挂起,就会调用dvmNukeThread函数发送 SIGSTKFLT 信号给相应线程,从而杀死 APP。

具体代码如下:

static void waitForThreadSuspend(Thread* self, Thread* thread)

{

constint kMaxRetries = 10;

… …

while (thread->status == THREAD_RUNNING) {

… …

if (retryCount++ == kMaxRetries) {

ALOGE(“Fatal spin-on-suspend, dumping threads”);

dvmDumpAllThreads(false);

/* log this after – long traces will scroll off log */

=> ALOGE(“threadid=%d: stuck on threadid=%d, giving up”,

self->threadId, thread->threadId);

/* try to get a debuggerd dump from the spinning thread */

=> dvmNukeThread(thread);

/* abort the VM */

dvmAbort();

… …

}

而从堆栈我们看出,杀死进程的时候,我们正调用DexFile.loadDex,这个方法最后会调用到dvmRawDexFileOpen里面,执行 write 操作。而这个 write 涉及 I/O 操作,是比较耗时的。所以,当线程在做 dexopt,长时间无法响应虚拟机的挂起请求时,就会触发这个问题。

一般来说,虚拟机在执行 Java 代码的时候,都会是 RUNNING 状态。而只要调用了 JNI 方法,在执行到 C/C++ 代码的时候,就会切换为 NATIVE 状态。而虚拟机只会在 RUNNING 状态下会挂起线程,如果是在 NATIVE 状态下,虚拟机是不会要求线程必须挂起的。

不过,这里有一个特殊之处。虽然DexFile.loadDex方法最终也走到了 JNI 里面调用dvmRawDexFileOpen函数,但由于DexFile类是虚拟机的内部类,Dalvik 虚拟机不会在内部类执行 JNI 方法的时候将线程切换为 NATIVE 状态,仍然会保持原来的 RUNNING 状态。于是,在 RUNNING 状态下,做 OPT 的线程就会被要求挂起。而此时由于正在执行耗时的 write 操作,无法响应挂起请求,便出现了如上的崩溃。

当然,可能有人会想到在 Native 代码中,用CallStaticObjectMethod来触发DexFile.loadDex,不过这种方式是不可行的。因为CallStaticObjectMethod调用 Java 方法DexFile.loadDex时,会使得状态再次切换为 RUNNING。

具体来看下 CallStatciXXXMethod 方法的定义处:

static _ctype CallStatic##_jname##Method(JNIEnv* env, jclass jclazz, \

jmethodID methodID, …) \

{ \

UNUSED_PARAMETER(jclazz); \

ScopedJniThreadState ts(env); \

JValue result; \

va_list args; \

va_start(args, methodID); \

dvmCallMethodV(ts.self(), (Method*)methodID, NULL, true, &result, args);\

va_end(args); \

if (_isref && !dvmCheckException(ts.self())) \

result.l = (Object*)addLocalReference(ts.self(), result.l); \

return _retok; \

}

关键在于 ScopedJniThreadState:

explicit ScopedJniThreadState(JNIEnv* env) {

mSelf = ((JNIEnvExt*) env)->self;

… …

CHECK_STACK_SUM(mSelf);

dvmChangeStatus(mSelf, THREAD_RUNNING);

}

~ScopedJniThreadState() {

dvmChangeStatus(mSelf, THREAD_NATIVE);

COMPUTE_STACK_SUM(mSelf);

}

在使用dvmCallMethodV调用 Java 方法前,会先切换状态为THREAD_RUNNING,执行完毕后,ScopedJniThreadState析构,再切换回THREAD_NATIVE。这样,JNI 执行DexFile.loadDex就和直接执行 Java 代码一样,状态会有问题。不只是CallStaticXXXMethod,所有使用CallXXXMethod函数在 Native 下调用 Java 方法的情况都是如此。

好在,我们想到了另一个办法:既然 Dalvik 不会对内部类的 JNI 调用做切换,我们就自己写一个 JNI 调用,使其走到 Native 代码中,这样线程就会变为 Native 状态,然后 直接调用虚拟机内部函数 做 dexopt 即可。这样在做 dexopt 的时候,始终会处于 NATIVE 的状态,不会切为 RUNNING,也不会被要求挂起,也就能避免这个问题。

这个虚拟机内部函数就是dvmRawDexFileOpen,我们先来看下它的代码说明:

/*

  • Open a raw “.dex” file, optimize it, and load it.

  • On success, returns 0 and sets “*ppDexFile” to a newly-allocated DexFile.

  • On failure, returns a meaningful error code [currently just -1].

*/

int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName,

RawDexFile** ppDexFile, bool isBootstrap);

这个函数可以用来打开原始 DEX 文件,并且对它做优化和加载。对应到 libdvm.so 中的符号是_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb,我们只需要用 dlsym 在 libdvm.so 里面找到它,就可以直接调用了,完整代码如下:

using func = int ()(constchar fileName, constchar* odexOutputName, void* ppRawDexFile, bool isBootstrap);

void* handler = dlopen(“libdvm.so”, RTLD_NOW);

dvmRawDexFileOpen = (func) dlsym(handler, “_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb”);

dvmRawDexFileOpen(file_path, opt_file_path, &arg, false);

这样,我们自己写一个 JNI 调用,在 Native 状态下执行上述代码,就能达到完成 ODEX 的目的,从而根本上杜绝这个异常了。

另外,我们把 dexopt 操作放到了单独进程执行,由此可以避免 ODEX 操作对主进程造成其他性能影响。此外,由于设备情况多种多样,运行环境十分复杂,还可能会有一些厂商魔改,导致的 dlsym 找不到_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb符号,虽然这种情况极为罕见,但理论上仍有可能发生。单独进程里面由于环境比较纯粹,基本很少发生 ANR 和 GC 事件,挂起的情况就很少,也能最大程度规避这个问题。

多级加载


我们发现,相比于官方 MultiDex 加载 ZIP 形态的 DEX 文件,非 ZIP 方式的 DEX(也就是直接对 DEX 文件做 ODEX,而不用先把 DEX 压缩进 ZIP 里面)对于整体时间也有一定程度的优化,因为这种非 ZIP 方式避免了原先的两个耗时:

  1. 把原始 DEX 压缩为 ZIP 格式的时间;

  2. ODEX 优化的时候从 ZIP 中解压出原始 DEX 的时间。

非 ZIP 的方式相比于 ZIP 方式,整体耗时会减少 40% 左右,但是 DEX 文件磁盘占用空间比原先 ZIP 文件的方式增加一倍多。因此我们可以只在磁盘空间充裕的时候,优先使用非 ZIP 方式加载。

而我们openDexFile_bytearray加载 DEX 的方式,需要的只是原始 DEX 文件的字节数组(byte[])。这个字节数组我们在首次冷启动的时候是直接从 APK 里面解压提取得到的。我们可以在这次启动提取完成后,先把这些字节数组落地为 DEX 文件。这样如果再次启动 APP 的时候,ODEX 没做完,就可以直接使用前面保存的 DEX 文件来得到字节数组了,从而避免了从 APK 解压的时间。

总体来看,我们整套方案中一共存在四种形态的 DEX:

  1. 从 APK 文件里面解压得到的 DEX 字节数组;

  2. 从落地的 DEX 文件里面得到的 DEX 字节数组;

  3. 从 DEX 文件优化得到的 ODEX 文件;

  4. 从 ZIP 文件优化得到的 ODEX 文件。

生成各个产物的时序图如下所示:

我们依次说明每一步:

  • A. 从 APK 里面直接解压得到 DEX 字节数组;

  • B. 将 DEX 数组保存为文件;

  • C. 用 DEX 文件生成 ODEX 文件;

  • D. 用 DEX 数组生成 ZIP 文件以及它对应的 ODEX 文件。

正常情况下,我们会依次按 A -> B -> C 的时序依次产生各个文件,如果中间有中断的情况,我们下次启动后会继续按照当前已有产物做对应操作。我们仅在磁盘空间不够,且所在系统不支持直接加载字节数组的情况下才会走 ZIP&ODEX 方式的 D 路径。这里不支持的情况主要是一些特殊机型,比如 4.4 却采用了 ART 虚拟机的机型、阿里 Yun OS 机型等。

接下来我们继续看下加载流程图:

  • 当 APP 首次启动的时候,如果会从 APK 里面解压 DEX 数组,因此会按照 a -> b 的路径执行;

  • 当 APP 发现只有 DEX 文件,没有 ODEX 文件时,会把从 DEX 文件中取得 DEX 数组,按照 c -> b 路径执行;

  • 当 APP 发现 DEX 文件和 ODEX 文件都存在的时候,会按照 ODEX 方式加载,按照 d 路径执行;

  • 当 APP 发现有 ZIP 文件以及它所对应的 ODEX 的时候,会按照 e 路径执行。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

Android学习PDF+架构视频+面试文档+源码笔记

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

Android学习PDF+架构视频+面试文档+源码笔记

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-vCI2i3M4-1710703726321)]

【算法合集】

[外链图片转存中…(img-eGV9VYzZ-1710703726321)]

【延伸Android必备知识点】

[外链图片转存中…(img-hmPRd3Ce-1710703726322)]

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值