启动优化中的一些黑科技,了解一下~

Posted 码中之牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了启动优化中的一些黑科技,了解一下~相关的知识,希望对你有一定的参考价值。

作者:程序员江同学

前言

启动速度优化是 android 开发中的常见需求,除了一些常规的手段之外,也有一些黑科技手段,我们来看一下这些黑科技手段是否有效,以及如何实现

线程优先级设置

线程优先级设置的概念很容易理解,优先级越高的线程越容易获取 CPU 时间片,那么为了保证 app 的流畅运行,那么我们就应该将核心线程的优先级提高,而将其他线程的优先级调低

对于 app 来说,核心线程就是主线程 + RenderThread,那么我们是否有必要手动设置线程优先级呢?

我们可以通过以下命令获取当前的线程优先级

adb shell 
ps -A | grep 包名 // 根据包名找到Pid
top -H -p PID    // 查看线程优先级命令

运行结果如上图所示,要看懂上面的图我们要先了解一点背景知识

  • PR: 优先级 (priority),值越小优先级越高,会受NI的值的影响
  • NI: 即 Nice 值,我们可以通过Process.setThreadPriority设置,同样是值越小优先级越高

其实我们只需要知道它们都是值越小优先级越高就好了,可以看出主线程与 RenderThread 的优先级都挺高的,仅次于 Binder 线程

我看到一些启动优化的文章谈到线程优先级设置,但测试结果似乎是没有必要?难道是版本问题?有了解的同学可以在评论区说下

核心线程绑定大核

绑定大核是否有必要?

核心线程绑定大核的思路也很容易理解,现在的 CPU 都是多核的,大核的频率比小核要高不少,如果我们的核心线程固定运行在大核上,那么应用性能自然会有所提升

就拿我手上的小米10来说,使用的是骁龙865的 CPU,由一颗A77超大核+三颗A77大核+四颗A55小核心组成,我们可以通过/sys/devices/system/cpu/目录下的文件获取各个核的频率,如下所示

cas:/sys/devices/system/cpu $ cat cpu0/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu1/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu2/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu3/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu4/cpufreq/cpuinfo_max_freq
2419200
cas:/sys/devices/system/cpu $ cat cpu5/cpufreq/cpuinfo_max_freq
2419200
cas:/sys/devices/system/cpu $ cat cpu6/cpufreq/cpuinfo_max_freq
2419200
cas:/sys/devices/system/cpu $ cat cpu7/cpufreq/cpuinfo_max_freq
2841600

可以看出,cpu0 到 cpu3 是4个小核, cpu4 到 cpu6 是3个大核,cpu7 是超大核,它们之间的频率还是相差挺大的

同时通过 systrace 工具可以发现,主线程基本都运行在 cpu7 这个超大核上,而 RenderThread 会在 cpu4 到 cpu6 间切换,有时甚至会调度到小核上

因此可以看出还是有必要把 RenderThread 绑定到一个大核上的,绑定可以更好的利用缓存以及减少线程的上下文切换

绑定大核实现

绑定大核是通过函数sched_setaffinity实现的

extern "C" JNIEXPORT void JNICALL
Java_com_zj_android_startup_optimize_StartupNativeLib_bindCore(
        JNIEnv *env,
        jobject /* this */, jint thread_id, jint core) 
    cpu_set_t mask;     //CPU核的集合
    CPU_ZERO(&mask);     //将mask置空
    CPU_SET(core, &mask);    //将需要绑定的cpu核设置给mask,核为序列0,1,2,3……
    if (sched_setaffinity(thread_id, sizeof(mask), &mask) == -1)      //将线程绑核
        LOG("bind thread %d to core %d fail", thread_id, core);
     else 
        LOG("bind thread %d to core %d success", thread_id, core);
    

如上所示,sched_setaffinity共有 3 个参数

  • 参数 1 是线程的 id,如果为 0 则表示主线程
  • 参数 2 表示 cpu 序列掩码的长度
  • 参数 3 则表示需要绑定的 cpu 序列的掩码

以上是线程绑定大核的核心代码,可以看到我们还需要获取 RenderThread 的 id ,以及 cpu 大核的序列

应用中线程的信息记录在 /proc/pid/task 的文件中,通过解析 task 文件就可以获取当前进程的所有线程,而 cpu 大核序列也可以通过解析 /sys/devices/system/cpu 目录实现

具体代码就不在这里粘贴了,完整代码可见文末链接

GC 抑制

GC 抑制是否有必要?

我们知道 Java 的拉圾回收机制,在 Android 5.0 之后,ART 取代了 Dalvik,ART 虚拟机在垃圾回收的时候虽然没有像 Dalvik 一样 stop the world,但在启动阶段如果发生垃圾回收,GC 线程同样抢占了不少系统资源

Google 也注意到启动阶段 GC 对启动速度的影响,并在 Android 10 之后做了一定的优化,详情可见如下提交:cs.android.com/android/_/a…

可以看出,基本思路是在 2s 内提高后台 GC 的阈值,减少启动阶段的 GC 次数,根据 Google 的测试,抑制 GC 后效果如下

可以看出,GC 次数明显减少,启动速度也有一定的提升。那么我们是不是可以使用一些手段让 Android 10 以下也能实现这个效果呢?

同时我们也可以通过以下代码获取 gc 的次数与耗时,方便统计 gc 对启动耗时的影响,以评估是否有必要做 GC 抑制

Debug.getRuntimeStat("art.gc.gc-count") // gc 次数
Debug.getRuntimeStat("art.gc.gc-time")  // gc 耗时
Debug.getRuntimeStat("art.gc.blocking-gc-count") // 阻塞 gc 次数
Debug.getRuntimeStat("art.gc.blocking-gc-time") // 阻塞 gc 耗时 

GC 抑制实现

HeapTaskDaemon 执行流程

GC 主要是通过 HeapTaskDaemon 线程实现的,这是一个守护线程,在 Zygote 线程启动后这个线程也就启动了,启动后主要做了以下工作

  1. HeapTaskDaemon.runInternal()方法开始一步步调用到 native 层的 task_processor.RunAllTasks() 方法
  2. TaskProcessor中的tasks为空时,会休眠等待,否则会取出第一个HeapTask并执行其Run方法

HeapTaskRun方法是一个虚函数,需要子类来实现

class HeapTask : public SelfDeletingTask 
;

class SelfDeletingTask : public Task 
;

class Task : public Closure 
;

class Closure 
 public:
  virtual ~Closure()  
  // 定义 Run 虚函数
  virtual void Run(Thread* self) = 0;
;

HeapTask就是垃圾回收的任务,有多个子类,比如最常见的 ConcurrentGCTask 就是其子类,在 Java 内存达到阈值时就会执行这个 Task,用于执行并发 GC

方案设计

在了解了 HeapTaskDaemon 的执行流程之后,我们想到,如果启动时在ConcurrentGCTaskRun方法执行前休眠一段时间,不就可以实现 GC 抑制了吗?

Run方法正好是虚函数,虚函数与 Java 中的抽象函数类似,留给子类去扩展实现多态

虚函数和外部库函数一样都没法直接执行,需要在表中去查找函数的真实地址,那么我们是不是可以使用类似 PLT Hook的思路,使用自定义函数的地址替换原有函数地址,实现 Hook 呢?

答案是肯定的,如上图所示,一个类中如果存在虚函数,那么编译器就会为这个类生成一张虚函数表,并且将虚函数表的地址放在对象实例的首地址的内存中。同一个类的不同实例,共用一张虚函数表的。

因此我们的主要思路如下:

  1. 启动时将虚函数表中的 Run 函数地址替换为自定义函数地址
  2. 在自定义函数内部休眠一段时间,抑制 GC
  3. 休眠完成后将虚函数表中的函数地址替换回来,避免影响后续执行

具体实现

很显然要想实现 Hook,我们首先需要获取ConcurrentGCTask对象地址与其Run方法地址

那么我们可以如何获取方法地址呢?

dlopen 函数和 dlsym 可以用于打开动态链接库中的函数,通过函数的符号返回函数地址

因此我们需要做下面两件事

  1. 获取函数符号
  2. 根据函数符号获取函数地址
adb pull /system/lib64/libart.so // Android 10 以前系统,Android 10 之后换了位置
aarch64-linux-android-readelf -s --wide libart.so

通过以上方式可以导出 so 中的符号,查找到结果如下

_ZTVN3art2gc4Heap16ConcurrentGCTaskE   // ConcurrentGCTask
_ZN3art2gc4Heap16ConcurrentGCTask3RunEPNS_6ThreadE // Run 方法

可以看出,符号是原来的名字做了一定的变换,这是 c++ 的 name mangling 机制,mangling 的目的就是为了给重载的函数不同的签名,具体的规则可以自行查阅,这里就不赘述了

还有需要注意的一点是,Android 7.0 以上对 dlsym 的调用有限制,同时从 aarch64-linux-android-readelf 的结果可以看出, ConcurrentGCTask 在 .dynsym 段,而 Run 方法在 .symtab 段,而 dlsym 只能搜索 .dynsym 段,而无法搜索 .symtab 段,因此我们这里使用enhanced_dlsym开源库,既支持 Android 7.0 以上调用,也可以查找 .stymtab 段

好了,前置知识讲完了,下面来看下代码

void delayGC() 
    //以RTLD_NOW模式打开动态库libart.so,拿到句柄,RTLD_NOW即解析出每个未定义变量的地址
    void *handle = enhanced_dlopen("/system/lib64/libart.so", RTLD_NOW);
    //通过符号拿到ConcurrentGCTask对象地址
    void *taskAddress = enhanced_dlsym(handle, "_ZTVN3art2gc4Heap16ConcurrentGCTaskE");
    //通过符号拿到run方法
    void *runAddress = enhanced_dlsym(handle, "_ZN3art2gc4Heap16ConcurrentGCTask3RunEPNS_6ThreadE");
    //由于 ConcurrentGCTask 只有五个虚函数,所以我们只需要查询前五个地址即可。
    for (size_t i = 0; i < 5; i++) 
        //对象头地址中的内容存放的就是是虚函数表的地址,所以这里是指针的指针,即是虚函数表地址,拿到虚函数表地址后,转换成数组,并遍历获取值
        void *vfunc = ((void **) taskAddress)[i];
        // 如果虚函数表中的值是前面拿到的 Run 函数的地址,那么就找到了Run函数在虚函数表中的地址
        if (vfunc == runAddress) 
            //这里需要注意的是,这里 +i 操作拿到的是地址,而不是值,因为这里的值是 Run 函数的真实地址
            mSlot = (void **) taskAddress + i;
        
    
    // 保存原有函数
    originFun = *mSlot;
    // 将虚函数表中的值替换成我们hook函数的地址
    replaceFunc(mSlot, (void *) &hookRun);


//我们的 hook 函数
void hookRun(void *thread) 
    //休眠3秒
    sleep(3);
    //将虚函数表中的值还原成原函数,避免每次执行run函数时都会执行hook的方法
    replaceFunc(mSlot, originFun);
    //执行原来的Run方法
    ((void (*)(void *)) originFun)(thread);

核心代码就是上面这些,主要做了这么几件事:

  1. 通过符号获取Run方法地址
  2. 遍历虚函数表,找到虚函数表中存放Run方法真实地址的位置
  3. 保存原函数地址,并将虚函数表中的值替换成我们 hook 的函数地址
  4. 在 hook 函数中休眠一段时间,休眠结束后还原虚函数表,避免影响后续任务

总结

在性能优化中除了一些常规手段外,也经常有一些黑科技手段,本文主要介绍了启动优化中的线程优先级设置,核心线程绑定大核,GC 抑制等手段, 讲解了一下这些黑科技手段是否有效,以及具体是怎么实现的,希望对你有所帮助。

我们在学习一些新型的优化手段或方案的前提时,你必须要对其原始的优化手段要有所了解,这样你才能达到事半功倍的效果,如果什么都不懂来学习这些,就像一个还没学会走路,就要开始跑的小孩,最后换来的就是不断踩坑。这里为了帮助一些对性能优化掌握的还不够的小伙伴们整理了《Android 性能优化核心笔记》,将启动优化、内存优化、存储优化、网络优化、卡顿优化……等其中原理分析的比较透彻,敬请参考!!!

带你了解S12直播中的“黑科技”

摘要:让精彩更流畅、让较量更清晰、让参与更沉浸、让体验更有趣,幕后的舞台,从来都是技术的战场,S12背后的名场面同样场场高能。

本文分享自华为云社区《用硬核方式打开S12名场面》,作者:华为公众号。

让精彩更流畅、让较量更清晰、让参与更沉浸、让体验更有趣,幕后的舞台,从来都是技术的战场,S12背后的名场面同样场场高能。

经过35天的鏖战,2022英雄联盟全球总决赛(S12)终于在11月6日落下帷幕。在这一个多月里,各方战队一路披荆斩棘,有命悬一线,也有高光瞬间,荣耀的背后始终跟随着挑战。而对于拥有S12直播平台独家版权的哔哩哔哩(以下简称“B站”)来说,亦是如此。

据悉,整个赛事期间,B站共完成了91场比赛的实时直播,其中最后一场的S12全球总决赛播出后,哔哩哔哩英雄联盟赛事直播间的实时人气一度突破3.1亿

纹理更加清晰的画质、“纵享丝滑”的转播效果、沉浸感十足的互动特效、更贴心的无障碍直播间......S12直播可以看做是音视频领域“黑科技”的一次前沿试水。对于需承载亿万级并发在线观看人次的直播平台来说,每一处升级都错综复杂,每一点进步都得来不易。

高光时刻,和战队即时共享

在短视频、直播、社交等并进的互动时代,超高清已经成为游戏玩家的硬核需求。“得画质者得人心”,不管是音视频内容的生产商还是服务商,极致的画质体验已经成为获得用户注意力资源的武器。

而在B站上看过今年英雄联盟全球总决赛的观众,大都会明显感觉到:今年的赛事直播画面更清晰了,互动也更多了。

据【哔哩哔哩技术】官方公众号公布的信息显示,针对 S12的赛事直播,B站研发了一种实时画面超分算法来提升画面质量,该算法能够在源流的基础上使得画面细节更加清晰、纹理更加丰富。

▲低质图像/超分图像/高质图像/残差图像

图像超分算法在业内已经不是新词,目前主流超分算法分为非实时和实时处理两种类型。在过往,已有大量非实时超分算法的实践案例,比较典型的是视频网站将低分辨率、低清晰度的老旧视频转换成符合主流观看需求的720p、1080p、2k的高分辨率视频。但这种非实时超分算法的计算量很大,只适用于对视频实时性没有要求的场景。

在B站S12赛事直播这类对视频实时性要求很高的游戏直播场景中,实时处理的超分算法可大幅度缩减计算量,在提升画面质量的同时,也能提高视频处理效率。

在此前测试时,B站的实时画面超分算法将视频从1080p超分至4k分辨率,单卡处理速度可以达到75fps(画面每秒传输帧数)。这个处理效率对于60fps的游戏直播源流来说,在速度上还有较大富余。

此次B站推出的单卡4k 75fps的实时超分速度拓宽了超分技术在直播领域的应用。尽管目前该算法仅在S12官方赛事直播间应用,但据B站透露,未来其也将服务于更多需要较高实时性的游戏直播业务中,开拓更为广阔的应用空间。

不过,当前的互动时代,远不止画质等信息的“通信”传输,更强调的是共享时空,实时互动。

“看比赛时候的氛围有非常强的实时性和感染力,大家会想要把自己比较饱满的情绪及时表达出来,所以我们希望能通过除了弹幕或者是常规的点赞动作,让大家能把自己兴奋的一面更好地展现在直播间里,让所有在线的观众能共庆此时,分享快乐。”今年B站的赛事直播比较明显的变化是赛事房间使用了新版直播间,支持上下滑切换直播间、全新背景图、表情包弹幕等功能。据B站直播互动技术保障负责人CrystalDan介绍,为了增强互动性和趣味性,B站在直播间加入了不少新玩法。

例如在比赛到了团战、推高地等比较激烈的时刻,直播间会发起“点赞热力特效”,引导大家积极点赞为选手打call,在极短的时间内打满进度条后会出现酷炫的动画特效。一方面是让用户通过点赞的方式表达自己对这场比赛的热情,另一方面也让没有点赞的用户可以通过进度条和动画特效感受到紧张的比赛氛围。

再比如通过“小黄车”卖赛事装扮。当某一个战队获胜了,用户可能会由此产生购买该战队装扮或周边的欲望,以获得归属感和荣誉感。而直播间里的“小黄车”提供淘宝、京东、会员购、个性装扮等商品,能够满足用户边看边买的需求。此外,还有一键预约、礼物特效、弹幕表情等更便捷、强互动的功能,大大提升了用户的观赛和参与体验。

更好的画质,更多的玩法,说起来简单,要实现起来却并不容易。作为承载千万级乃至亿量级的赛事直播平台,任何细小的问题都会对用户体验造成巨大的伤害,而新的玩法也会带来新的场景计算和流量并发。

最强辅助,为直播保驾护航

简单来讲,本次S12赛事直播的流程可以理解为:

腾讯官方提供赛事的原地址;

B站的云源站拿到直播信号后,会进行画面的预处理,包括增强或锐化等;

进行转码和4k的一个超分;

将处理好的视频流通过CDN内容分发网络,下发至观众的设备中进行播放。

为了保障直播各个环节的稳定性,早在今年8月,B站的技术团队就制定出了周密的直播保障方案。不仅在技术上,对直播老网关做了迁移和容器化,并通过Go/PHP去cpuset化、内核升级推进了直播合池,提升了PaaS资源弹性,对核心功能做多活保证直播体验,还通过全链路压测、混沌工程、设定预案/SOP等技术手段,去提高系统的可靠性。

同时,在业务上,B站也与上游业务方做了合理的流量预估和资源预算,并同往年一样,选择依托华为云过硬的技术服务,以确保直播万无一失。

“从外面来看,我们整个直播系统是一个比较简单的结构,但其实整个直播系统后面涉及到的微服务有几百个。这对于直播的保障工作来说,是一件很有挑战的事情。”B站S12技术总负责人小卫在采访中说道。

要知道,赛事直播是一项比较复杂的工程,需要统一的管理和调度。因此,华为云容器的引擎为B站提供了混合集群的统一管理服务。基于华为云CCE容器的托管服务,B站搭建起导购台和中转台,配合华为高性能的GPU云主机进行赛事的流转码,让每场比赛都能够顺利地在亿万的终端上流畅、顺滑地调度播放。

“在今年新增的这些玩法中,高并发的读和写是同时存在的,我们需要在高并发的场景下,保证用户的互动体验。” 小卫表示。

本次赛事华为云还为B站提供了边缘分布式直播方案,帮助B站降低计算成本,消除了之前统一转码源站的单点故障,增强了直播过程中的稳定性、安全性。同时打造海量弹性资源池,按需调度,更好地提升了用户的互动体验。

例如基于华为云智能边缘站点,可实现视频访问的冷热分流:

  • 如果用户访问冷视频,就通过边缘站点解析域名,经由华为云内部DCI高速网络访问直读存储OBS;
  • 如果用户访问热视频,则通过热域名访问华为云CDN,通过CDN的就近内容分发,保证热度视频的用户播放体验。

通过这样冷、热不同视频的不同调度逻辑,细粒度管理,能够更精细化地管理与控制成本。

同时,华为云独创的高性价比直播方案——边缘G-EIP的网络算力分离,也是本次赛事技术支持中的一大亮点。

众所周知,华为云在很多城市都有大量优质稳定的计算资源,能够确保B站的收流、转码、录制等计算密集型业务的持续稳定运行。只不过,一线城市的带宽资源相对成本较高,直播又是带宽强依赖型业务,这样势必会导致直播建设成本上浮,形成成本与稳定不可兼得的局面。

因此,华为云针对B站的实际场景需求,创新性地将网络出口拉远至一线城市附近的二三线城市pop点,利用当地廉价的带宽资源与华为云主力region的网络服务绑定结合,做到了可靠性与成本兼顾。同时,也得益于华为云分布在全国各区域的边缘站点,终端用户得以就近接入,保障了流畅的观赛体验。

此外,除了对直播间的互动功能进行升级,在今年的S12期间,B站的无障碍直播间也使用了华为的高性能GPU云主机,在自动生成实时字幕上比去年更为快速、准确。

“每当我们有一些资源不足、备量不足的问题时,就会找到我们的合作伙伴一起去看能不能通过一些方案或者从内部协调一些机器,帮我们去cover这些问题。”据B站S12技术总负责人小卫介绍,在赛事期间,华为云会专门指派一些工程师到现场做驻场保障,在比赛的后端也会有团队远程在线互动,以确保不管是前方还是后方出现问题,都能及时响应,从而保证整个赛事过程的平稳顺利。

共进共赢,音视频产业迎来黄金时代

“希望成为中国年轻人的文化生活方式,陪这一代人一起成长。”成立13年,以技术创新为源动力的B站已发展成为了国内音视频行业的领军企业,而华为也在借助底层技术架构的不断创新,释放音视频技术生产力。

从1988年华为通话业务成就国内一流的音频引擎,到2004年华为视频业务拥有国内一流的直播CDN能力,再到2017年华为云上线CDN海量节点,以及今年和B站的合作进一步突破了视频直播这一复杂工程,华为始终立足于整个音视频大生态,探索技术在其中的创新价值。

而得益于底座能力与关键技术的持续突破,音视频服务体系正逐步从点播、直播到实时音视频深化发展。实时性与互动性的长足需求与优化,逐步激活了更多场景下的音视频互动模式,驱动用户的音视频消费习惯向更加还原真实、更加沉浸式的实时音视频服务迁移。

“我曾经看过一个电影叫做《头号玩家》。在我自己的设想中,未来一定是往这个方向发展的,是通过脑机接口或者类似AR、VR的技术来实现的。我觉得这种沉浸式的体验给用户带来的感受会非常好。”B站技术委员会负责人毛剑在采访中说道。

实时的音视频互动与社交娱乐场景具有天然契合度,不仅可以直接提升现有产品的基础属性,还可以融合空间音频、VR、AR等技术,给予用户沉浸式的实时互动体验。也正因如此,国内实时音视频的市场规模一直在不断上升,场景效能也在不断深化。

根据艾瑞咨询发布的《2022年中国实时音视频行业研究报告》显示,2021年中国实时音视频(RTC)PaaS市场规模为16亿元,同比增长10.3%。受社交娱乐头部应用的高度渗透及“双减”政策的持续影响,预计未来三年的复合增长率为28.4%,2024年实时音视频(RTC)PaaS市场规模将达到30亿元。

而目前来看,华为云的SparkRTC技术已经十分成熟,可提供高质量、低成本的实时音视频服务,实现了跨大洲端到端时延200ms以内,SLA服务可用性高达99.99%。

“我们设想的未来,它包含着元宇宙、数字孪生,也包括VR和AR在内的很多技术。实现这些技术的核心在于,在应用的过程中解决高带宽和低延时问题。在那之后,视频这个市场就会像星辰大海一样宽广。它能带来海量的视频,人人都会去享受视频。”华为云上海总经理张英梗在采访中说道。

可以预见,未来实时音视频将有望在越来越多的产业互联网领域实现落地,同时元宇宙相关应用的出现也将为消费互联网创造更大的想象空间。

据张英梗总经理介绍,无论是to B还是to C,华为云都具有丰富的服务经验,未来也将持续加强数字内容生产的生态体系构建和技术研究实践,助力客户搭建无限玩法或场景,共建实时互动的世界。

“我们希望客户和伙伴在整个大视频生态上,都能体验到未来技术带给他们的创新价值,让客户能更好地服务于他的客户以及他自己。同时,也希望我们的伙伴在整个华为云生态上,都能享受到‘一切皆服务’带给他们价值体验。” 张英梗说道。

S12虽已落幕,但属于B站与华为云的音视频“黄金时代”才刚刚开始。

点击关注,第一时间了解华为云新鲜技术~

以上是关于启动优化中的一些黑科技,了解一下~的主要内容,如果未能解决你的问题,请参考以下文章

android黑科技系列——自动注入代码工具icodetools

android黑科技系列——破解游戏之修改金币数

拯救OOM 字节自研 Android 虚拟机内存管理优化黑科技 mSponge

android黑科技系列——Xposed框架实现拦截系统方法详解

android黑科技系列——应用市场省流量更新(增量升级)原理解析

进博会上的人气 AI 黑科技 了解一下?