指南AI
V:gogoh6

抖音无人直播软件安卓版抖音直播抽奖刷屏软件

幕言助手 2024-07-05 02:53:52 幕言直播助手 1426 ℃ 阿比整蛊源头|厂商微信:gogoh6
正文

前情回忆:抖音BoostMultiDex:Android低版本上初次启动时间削减80%(一)

抖音自研的 BoostMultiDex 计划,能够大幅改善 Android 低版本(4.4 及其以下)手机更新或安拆后初次冷启动时间。而且,差别于目前业界所有优化计划,我们是从 Android Dalvik 虚拟机底层机造动手,从底子上处理了安拆后初次施行 MultiDex 耗时过长问题。

我们上一篇文章中已经介绍了 BoostMultiDex 的核心优化思绪,即若何制止 ODEX,间接加载原始 DEX 完成启动。然而用那个办法加载 DEX 文件,比拟于 ODEX 优化后的体例,其 Java 代码施行性能上仍是有所丧失的。我们也能够畴前面办法的正文里面看出,虚拟机关于间接加载原始 DEX 的情况只是做了些根本优化:

The system will only perform "essential" optimizations on the given file.

所以,固然第一次启动我们是加载了原始 DEX 来施行的,但从久远的角度考虑,后续的启动,仍是应该尽量接纳 ODEX 的体例来施行。因而,我们还需要在第一次启动完成后,在后台恰当的时候做好 ODEX 优化。

一起头我们是做法也比力简单,在顺利加载 DEX 字节数组,完成启动之后,在后台开拓零丁的线程施行DexFile.loadDex就能够了。如许当后台做完 ODEX 后,APP 第二次启动时,就能够间接加载之前做好的 ODEX,得到较好的施行性能。那种做法在线下测试的时候也很一般,然而在上线之后,我们碰到了如许一个问题……

SIGSTKFLT 问题

线上报上来一个 Native Crash,它的仓库如下所示:

Signal 16(SIGSTKFLT), Code -6(SI_TKILL)#00 pc 00016db4 /system/lib/libc.so (write+12) [armeabi-v7a]#01 pc 000884a5 /system/lib/libdvm.so (sysWriteFully(int, void const*, unsigned int, char const*)+28) [armeabi-v7a]#02 pc 00088587 /system/lib/libdvm.so (sysCopyFileToFile(int, int, unsigned int)+114) [armeabi-v7a]#03 pc 00050d41 /system/lib/libdvm.so (dvmRawDexFileOpen(char const*, char const*, RawDexFile**, bool)+392) [armeabi-v7a]#04 pc 00064a41 /system/lib/libdvm.so [armeabi-v7a]#05 pc 000276e0 /system/lib/libdvm.so [armeabi-v7a]#06 pc 0002b5c4 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184) [armeabi-v7a]#07 pc 0005fc79 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272) [armeabi-v7a]#08 pc 0005fca3 /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20) [armeabi-v7a]#09 pc 0005481f /system/lib/libdvm.so [armeabi-v7a]#10 pc 0000e3e8 /system/lib/libc.so (__thread_entry+72) [armeabi-v7a]#11 pc 0000dad4 /system/lib/libc.so (pthread_create+160) [armeabi-v7a]

APP 收到 SIGSTKFLT 信号瓦解了,同时还输出了如许的日记:

06-25 15:10:53.821 7449 7450 E dalvikvm: threadid=2: stuck on threadid=135, giving up06-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){ const int 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 (*)(const char* fileName, const char* 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 体例制止了原先的两个耗时:

把原始 DEX 压缩为 ZIP 格局的时间抖音无人曲播软件安卓版;ODEX 优化的时候从 ZIP 中解压出原始 DEX 的时间。

非 ZIP 的体例比拟于 ZIP 体例,整体耗时会削减 40%摆布,但是 DEX 文件磁盘占用空间比原先 ZIP 文件的体例增加一倍多。因而我们能够只在磁盘空间丰裕的时候,优先利用非 ZIP 体例加载。

而我们openDexFile_bytearray加载 DEX 的体例,需要的只是原始 DEX 文件的字节数组(byte[])。那个字节数组我们在初次冷启动的时候是间接从 APK 里面解压提获得到的。我们能够在此次启动提取完成后,先把那些字节数组落地为 DEX 文件。如许若是再次启动 APP 的时候,ODEX 没做完,就能够间接利用前面保留的 DEX 文件来得到字节数组了,从而制止了从 APK 解压的时间。

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

从 APK 文件里面解压得到的 DEX 字节数组;从落地的 DEX 文件里面得到的 DEX 字节数组;从 DEX 文件优化得到的 ODEX 文件;从 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 途径施行。

那么一来,APP 就能够按照当前情况,选择最适宜的体例施行加载 DEX 了。从而包管了肆意时刻的更优性能。

历程锁优化

前面提到,OPT 优化是在零丁的历程里面施行的。零丁历程除了能够削减前面的 SIGSTKFLT 问题,还能在做完 OPT 后及时末行后台历程,制止过多的资本占用。

然而,在零丁历程处置 OPT 和其他历程施行 install 的时候,都涉及到 DEX 和 ODEX 文件的拜候和生成,因而在那些历程之间涉及到文件拜候和 OPT 时,都是加文件锁互斥施行的。如许能够制止加载的同时,另一个历程在操做 DEX 和 ODEX 文件招致的文件损坏。在官方的 MultiDex 中也是接纳那种文件锁的体例来停止互斥拜候的。

但那带来了另一个问题,若是 OPT 历程在长时间做 dexopt,而此时主历程(或者其他后台历程)需要再次启动,便会因为 OPT 历程持有互斥文件锁,而招致那些历程被阻塞住无法继续启动。能够看流程图来理解那一过程:

抖音无人曲播软件安卓版

正如图中描画的场景,用户第一次翻开了 APP,然后运行一会之后因为一些情况杀死了 APP,那时,后台历程已经启动并正在做 OPT。若是此时用户想要再次翻开,就会因为 OPT 历程互斥锁招致阻塞而黑屏。那显然是不成承受的。

因而,我们就需要采纳更好的战略,使得在主历程可以一般地继续往下施行,而不至于被阻塞住。

那个问题的关键在于,主历程需要依赖 OPT 历程的产品,才气继续往下施行,而 OPT 历程此时正在操做 DEX 文件,那个过程中的产品肯定无法被主历程间接利用。

所以,若是想要主历程不再因 OPT 操做阻塞,我们很容易想到能够无视 OPT 历程,不利用 DEX 文件,只从 APK 里面获取内存形式的 DEX 字节码就能够了。不外那种体例的次要问题在于,若是 OPT 时间十分长,在那段时间内就不能不不断利用内存体例的 DEX 启动 APP,如许性能就会处于比力差的程度。

因而我们接纳的是另一种计划。在主历程退出而再次启动的时候,先中行 OPT 历程,间接获得现有 DEX 产品停止加载,然后再唤起 OPT 历程。

如下图所示:

抖音无人曲播软件安卓版

那里关键点在于若何中行历程。当然,我们能够间接在主历程发信号杀死 OPT 历程,不外那种体例过于粗暴,很可能招致 DEX 文件损坏。并且 kill 信号的体例没有回调,我们无法得知能否历程确实地退出了。

因而,我们采纳的体例是用两个文件锁来做同步,包管历程启动和退出的信息能够在多个历程之间传达。

第一个文件锁就是单纯用来做为互斥锁,包管处置 DEX 和加载 DEX 的过程是互斥发作的。第二个文件锁用来暗示历程即将获取互斥锁,我们称之为筹办锁,它能够用来通知 OPT 历程:此时有其他历程正需要加载 DEX 产品。

关于 OPT 历程而言,获取文件锁的步调如下:

获取互斥锁;施行 OPT;非阻塞地测验考试获取筹办锁;若是没有获取到筹办锁,暗示此时有其他历程已经持有筹办锁,则释放互斥锁,并退出 OPT 历程;若是获取到了筹办锁,暗示此时没有其他历程一般持有筹办锁,则再次施行第 2 步,做下个文件的 OPT;完成所有 DEX 文件的 OPT 操做,释放互斥锁,退出。

关于主历程(或其他非 OPT 历程)而言,获取文件锁的步调如下:

阻塞期待获取筹办锁;阻塞期待获取互斥锁;释放筹办锁;完成 DEX 加载;释放互斥锁;继续往下施行营业代码。

详细情形见下图:

抖音无人曲播软件安卓版

起首,OPT 历程起头施行,会获取到互斥锁,然后做 DEX 处置。OPT 历程在处置完第一个 DEX 文件后,因为没有其他历程持有筹办锁,因而 OPT 历程获取筹办锁胜利,然后释放筹办锁,继续做下一个 DEX 优化。

那时候,主历程(或其他非 OPT 历程)启动,先胜利地获取筹办锁。然后继续阻塞地获取互斥锁,此时因为 OPT 历程已经在前一步获取到了互斥锁,因而只能期待其释放。

OPT 历程在处置完第二个 DEX 后,检测到筹办锁已经被其他历程持有了,因而获取失败,从而停行继续做 OPT,释放互斥锁并退出。

此时主历程就能够胜利地获取到互斥锁,而且立即释放筹办锁,以便其他历程能够获取。接着,在完成 DEX 加载后,释放互斥锁,继续施行后续营业流程。最初再唤起 OPT 历程接着做完原先的 DEX 处置。

总体看来,在那种形式下,OPT 历程能够主动发现有其他历程需要加载 DEX,从而中断 DEX 处置,并释放互斥锁。主历程便不需要期待整个 DEX 处置完成,只需要等 OPT 历程完成比来一个 DEX 文件的处置就能够继续施行了。

实测数据

我们当地拔取了几台 4.4 及以下的设备,对它们初次启动的 DEX 加载时间停止了比照:

Android版本厂商机型原始MultiDex耗时(s)BoostMultiDex耗时(s)4.4.2LGLGMS32333.5455.0144.4.4MOTOG45.6916.7194.3SamsungGT-N710024.1863.6604.3.0SamsungSGH-T99930.3313.7914.2.2HUAWEIHol-T00瓦解3.7244.2.1HUAWEIG610-U0036.4654.9814.1.2SamsungI910030.9625.345

以上是在抖音上测得的现实数据,APK 中共有 6 个 Secondary DEX,显而易见,BoostMultiDex 计划比拟官方 MultiDex 计划,其耗时有着素质上的优化,根本都只到原先的 11%~17%之间。也就是说 BoostMultiDex 削减了原先过程 80%以上的耗时。 别的我们看到,此中有一个机型,在官方 MultiDex 下是间接瓦解,无法启动的。利用 BoostMultiDex 也将使得那些机型能够焕发重生。 别的,我们在线上采纳了对半分的体例,也就是 BoostMultiDex 和原始 MultiDex 随机各自拔取一半线上设备,比照二者的耗时。

我们先以设备维度来看,那里随机拔取了 15 分钟的线上数据,图中横轴为每个 Android 版本 4.4 及以下的设备,纵轴为初次启动加载 DEX 的耗时,按耗时升序摆列,单元为纳秒。

BoostMultiDex 下的设备耗时:

抖音无人曲播软件安卓版

MultiDex 下的设备耗时:

抖音无人曲播软件安卓版

两张图更大的区别在于纵轴的时间刻度。能够看到,绝大大都设备的 BoostMultiDex 耗时在 5s 摆布,最多耗时也不会超越 35s。而反不雅 MultiDex,大大都都需要耗时 30 多 s,最长的耗时以至到达了将近 200s。

上面的图可能不同不敷明显,我们拔取一段时间,每半小时取所有设备耗时的中位数,能够得到下面的比照曲线:

抖音无人曲播软件安卓版

此中,下方橙色线为 BoostMultiDex,上方蓝色线为原始 MultiDex,能够明显看出,耗时下降的幅度十分庞大。

耗时的大幅削减会带来如何的效果呢?我们统计了 4.4 及以下机型中,两者进入到抖音播放页的设备数占比,时间范畴为一周,此中右边橙色为 BoostMultiDex,右边蓝色为原始 MultiDex。

抖音无人曲播软件安卓版

因为我们所有设备关于两种计划的拔取是对半开的,所以理论上二者的设备数应该接近于 1 比 1,不外从图中我们能够看到,BoostMultiDex 的设备数已经大幅超越 MultiDex 的设备数,两者比例接近于 2 比 1。

从中能够看出,MultiDex 耗时的削减关于设备活泼数的提拔,效果非常显著!

总结

最初,我们再梳理一下整个计划的实现要点:

接纳openDexFile_bytearray函数,能够间接加载原始 DEX 字节码;提早注入dex_object对象,以处理 4.4 机型上加载原始 DEX 字节码时,getDex的瓦解问题;接纳dvmRawDexFileOpen函数做 ODEX,以处理 SIGSTKFLT 问题;多级加载,在 DEX 字节码、DEX 文件、ODEX 文件中拔取最适宜的产品启动 APP;零丁历程做 OPT,并实现合理的中断及恢复机造。

关于国内偏僻地域,尤其关于海外许多开展中国度,Android 低版本机型仍然占比力高。目前 BoostMultiDex 计划在抖音和 TikTok 已经全量上线,那会使得那部门低版本 Android 用户间接受益,极大优化晋级和安拆启动体验。

我们后续将开源 BoostMultiDex 计划,以协助其他 APP 在低版本 Android 手机上改良性能体验。

此后,各家对下沉市场有需要的 APP,都能间接利用 BoostMultiDex 计划,立即获得飞一般的晋级安拆体验!那也是我们为改善 Android 生态奉献的一小份力,后续很快就会发布开源地址,敬请等待!

最初的最初,仍然再提一句,抖音/TikTok Android 根底手艺团队正在北上深杭四地寻求优良 Android 开发人才,目前疫情期间我们也撑持完全长途无接触面试。只要你的手艺功力深挚或者潜力庞大,都能够通过 字节跳动雇用官网查询抖音 Android 相关职位「链接」 或者联络 xiaolin.gan@bytedance.com 来送达简历,我们非常等待你的参加!

欢送存眷字节跳动手艺团队

本文TAG:

V:gogoh6