开播多进程演进(内存优化500+MB)

背景

直播的内存水位一直都处于高位,尤其是开播侧,平均pss在 2000mb+,内存过高会带来卡顿、OOM,ANR、系统强杀等严重稳定性问题。过去我们在内存上的优化主要是以解决内存泄漏方式进行优化,内存收益非常微小。

简单摸底了下内存的归属,发现我们的内存很大一部分都是来自其他无关业务的加载。

使用线上包进行线下测试得到以下数据:开播自身的内存仅占43%左右,基础内存占比28%(系统+抖音app初始化占用的内存,开播运行需要依赖),与开播完全无关的内存占比29%。

业务 内存 测试方法
app初始化 520mb 从抖音跳转到一个子进程空页面,不加载MainActivity,统计子进程的内存。空页面所占内存较少,可以忽略。
app初始化+MainActivity 1035mb 正常打开抖音,进入短视频页面+主播中心,然后跳转到子进程的空页面,统计主进程内存。
app初始+MainActivity+开播 1830mb 正常打开抖音,进入短视频页面+主播中心,然后开播(超清)

不加载开播无关业务只有两个方法,一种是让业务方在加载直播的时候清理干净,另外一种是使用独立进程,第一种方法实现起来会比较困难,首先业务只要加载过,就一定会留下一些code内存和静态内存,其次很多业务明确是有预加载的需求,不可能因为直播就不去加载。

通过对多进程的探索,可以将用户的内存水位大幅降低500mb+。接下来开始讲多进程的具体演进思路。

方案演进

最开始有大佬调研了将所有音画链路放入子进程 以及将主播所有逻辑放入子进程 (也就是方案一跟二,详细可看这里主播开播多进程OnePage),最终因为要适配的业务方太多以及总内存提升太多而放弃。

后来我在他的基础上微调,提出了主播所有逻辑放入子进程,杀掉主进程(方案三),以此来降低总的内存,后来在线下测试过程中发现,直播很多业务都依赖主进程,经常被非预期拉起,同时还有很多广播跟provider都是注册在主进程,最严重的是很多sp跟keva使用了非进程安全的写操作,偶尔会将文件写坏,最终会导致应用启动必崩的问题。故该方案潜在问题很多。

基于方案三遇到的问题,于是我想如果把子进程改成主进程不就没问题了,由于主播需要大部分的基建能力,重新初始化一遍子进程跟主进程的成本不会相差太多,只需要把子进程改成主进程即可。

于是就有了方案四,开播独立主进程, 开播的时候拉起一个轻量子进程(几乎为空进程),将房间等数据通过跨进程传递给子进程,然后再通过子进程杀掉主进程,最后再拉起开播Activity(运行在主进程),这样就避免了加载无关业务的内存。

方案简单说明 内存 稳定性 风险 通信性能 可行性
方案一 音画链路整个放子进程 采集特效编码推流等逻辑放入子进程,通过Surface 可以跨进程渲染的特性实现流画面的共享。 主进程内存降低总内存增加 提升,开播一半的崩溃都源自采集->推流。 牵扯到大量的跨进程通信,性能降低 可行性低,影响范围太大,需要将音画链路上的所有能力都改到子进程,然后所有通信改成跨进程通信,很多数据都无法传递,需要加适配层。牵扯到的业务方也是海量的。
方案二 主播所有逻辑放入子进程 将主播的Activity放入到子进程中。 主进程内存降低总内存增加 不变 不变 可行性中。影响范围主要是一些依赖主端能力的业务,从目前的分析上看,核心业务功能对主端依赖都不高。依赖比较严重的主要是埋点、性能监控、异常监控、setting等逻辑。im服务。
方案三 开播所有逻辑放入子进程+杀掉主进程。 在方案二的基础上杀掉主进程,降低总的内存水位 总内存降低 不变 不变 同上
方案四(最终方案) 开播独立主进程 主进程不加载短视频Activity,不跑跟开播无关的业务,只加载直播的Activity 主进程内存降低总内存降低 不变 不变 可行性高

最终方案

最终方案的简单流程可以看下面这张图,红色区域的内存都可以被优化,紫色区域的部分内存可以优化。

线下性能测试

基于最终的方案先简单的实现了一个版本进行性能测试(用户体验较差,但可以大约测出最终的内存收益)。用两款手机测试都有500mb+的收益。最终证明这个方案值得在线上一试。

(以下数据使用diggo进行测试,安装包是在MR中打的非localtest线上包)

详细方案

多进程的核心原理相对比较好理解,但要想真正在线上跑,需要考虑的事情就比较多了,比如各个页面的平滑过度、最后一帧的跨进程展示跨进程通信等问题要解。

页面平滑过渡

正常情况如果跳转子进程不处理的话,会有一段时间白屏显示(经排查,这里的白屏属于Activity的默认背景),体验相当不好,需要给Activity设置主题背景,这里不能将背景设置放到onCreate中,时机太晚,所以必须设置在清单文件中设置。

理想情况的实现,在开直播页面重启直播的时候,对当前页面截屏,同时将截屏内容盖在最上层,子进程Activity页面设置主题背景为透明,在子进程activity加载成功前,我们看到的就是重启直播前的最后一帧画面。

子进程Activity加载成功后,将最后一帧画面再显示到子进程的imageview中,同时拉起主进程的service,此时我们看到的还是最后一帧画面。

主进程service在加载完成前,会初始化主进程,耗时较长,故也是透明界面,此时看到是子进程的界面,也就是最后一帧画面。

整个过程的 UI 展示可以看下面的图。

显示最后一帧

主播中心最后一帧由三部分组成,顶部的状态栏,内容区域还有底部的导航栏(第一张图)。

由于主播Activity未使用沉浸模式,故重启直播场景只包含了中间这一部分(第二张图)。

截屏实现

对于普通view直接获取根view,创建一个bitmap初始化canvas,然后将根view渲染到canvas即可。但是对于Surfaceview而言,截屏就比较麻烦。目前业界对于surfaceview截屏的主要方式是通过PixelCopy。截取到surfaceview的内容之后,再将其与根view的截屏合并即可。

进程通信

重启直播场景进程间通信有3个操作,第一个是通知进程自杀,第二个是开播相关数据,第三个是截屏的最后一帧。由于后面两个场景的数据大小都有可能超过1mb,无法通过intent&aidl传递,最终选择使用 aidl+共享内存 实现。

共享内存

共享内存使用MemoryFile+ParcelFileDescriptor实现。由于getFileDescriptor被隐藏了起来,需要通过反射调用。截止目前的android15为止,该方法都可以使用。

ini 复制代码
val size = byteData.size
val memoryFile = MemoryFile("xxx", size+4)
val outputStream = memoryFile.outputStream
// 写入共享数据的大小
outputStream.write(intToBytesBigEndian(size))
outputStream.write(byteData)
outputStream.flush()
outputStream.close()
val clazz = memoryFile.javaClass
val fdsMethod = clazz.getDeclaredMethod("getFileDescriptor")
dsMethod.isAccessible = true
val descriptor = fdsMethod.invoke(memoryFile)
if (descriptor is FileDescriptor) {
    val pfd = ParcelFileDescriptor.dup(descriptor)
    return pfd
}

ParcelFileDescriptor是跨进程文件传递的描述符,通过它就可以找到对应的内存,该类虽然是Parcelable对象,但是在intent传递中是被禁止的,ParcelFileDescriptor 只能通过 aidl 方法传递

由于共享内存对应的流无法被感知到大小,为了提高读取效率,这里针对共享内存的格式做了一些要求。前四个字节存储数据大小,后面存储共享的数据。

任务裁剪

这里主要是为了优化

(app加载流程)

从子进程拉起主进程的时候,会先进行落地页判断,同时会判断是否为我们多进程场景,如果是多进程场景,就不加载跟开播无关的任务。

跳转前设置一个标志,这个标志使用data实现。之所以不放在intent的extra中是因为放在extra中在落地页取的时候会发生crash。

less 复制代码
intent.setData(Uri.parse("live://multiprocess"))

总结

通过以上方案可以实现内存上的大幅优化,但由于重启进程等同于重启app,耗时平均增加了3s+,所以目前优先在对耗时不怎么重要的重启开播场景使用并拿到相对不错的收益。

整体内存降低500mb+,java 内存降低90mb+,native 内存降低 150mb+,java 内存降低90mb+。详细数据请看下图。Lowmemory Kill 降低48%,ANR降低38%。

同时,基于多进程的演进,我们也上线了 直播Crash 的自动恢复。当用户Crash的时候,我们用了同样的手段跨进程恢复最后的直播页面,用户虽然崩溃了,但是还能继续开看播,同时内存等压力也大大降低,可谓浴火重生。后续有机会再单独分享。

相关推荐
lph0094 小时前
Android compose Room Sqlite 应用 (注入式)
android·数据库·sqlite
用户2018792831674 小时前
用 “快递站” 故事读懂 Binder 驱动:公开 / 匿名 Binder 打开全解析
android
相与还5 小时前
【2D横版游戏开发】godot实现tileMap地图
android·游戏引擎·godot
游戏开发爱好者85 小时前
App 上架平台全解析,iOS 应用发布流程、苹果 App Store 审核步骤
android·ios·小程序·https·uni-app·iphone·webview
2501_916007475 小时前
iOS 上架 App 费用详解 苹果应用发布成本、App Store 上架收费标准、开发者账号与审核实战经验
android·ios·小程序·https·uni-app·iphone·webview
ndzj9814796735 小时前
Android target35适配之窗口边衬区变更
android
用户2018792831675 小时前
匿名Binder的奥秘之“特工潜伏行动”
android
潘潘潘5 小时前
Android JNI中Java&Kotlin与C语言的相互调用
android
用户095 小时前
Kotlin 将会成为跨平台开发的终极选择么?
android·面试·kotlin