背景介绍
作业帮的直播间以信令为基础做教师端,学生端的同步渲染。整个直播间包括推拉流,流媒体渲染,聊天区,课件区,连麦, 通用互动等功能,其中课件区包括 H5 课件、Cocos 课件、白板、互动题、小游戏等。整体课件区内容的呈现是在 Webview 里,Webview 运行时聚合了多个不同种类的内容。下图为作业帮直播间功能简图。
技术难点
终端碎片化带来多种内容渲染的稳定性;
直播间性能保证,整体内存 /CPU 控制,子功能模块内存 /CPU 分配;
信令到达保证及各个环节的有序执行;
容错,任一内容环节出问题,如何保证不影响后续运行;
不同技术栈内容的持续性接入。
现状
前面介绍了整个直播间多背景知识以及前端的一些技术挑战,前端的主要功能区在课件区,可以看下优化之前的老架构,对整体有个粗略的认识。
所有的业务层代码聚合在称之为 zb 的模块,从图中可以粗略看出 zb 模块是一个比较庞大的模块,承载了众多不同技术体系的内容渲染和调度,也是反馈问题的重灾区,运行在一个常驻的浏览器之上。
随着用户规模的不断增大,不同的用户机型设备的增多以及内容的丰富度和效果的提升,我们的 zb 稳定性以及性能遇到了很大的挑战,线上的工单反馈也有上升趋势。来自用户侧反馈的问题大概为以下几类:
课件区黑屏,白屏,花屏;
卡在某一个具体的页面,中断了同步;
卡顿;
互动题做不了;
看不到老师笔记。
问题分析
通过大量的测试以及问题排查列举出我们的架构存在以下问题:
Webview 内存占用持续上升,无法及时 GC,内存不足是导致黑白屏的根本原因,Webview 的回收机制对我们来说是黑盒,尤其是基于 DOM 方式的渲染,研发无法通过代码触发 GC;
卡顿的原因来源于一些动画造成 CPU, 内存瞬间增长;
信令全链路到达有丢失,服务 -> NA -> Webview ,NA -> Webview 通信没有机制保障,兜底保证通信代码较多,即使信令到达,执行的时机以及完成时机都有一定程度业务的耦合;
渲染不同内容的调度逻辑耦合较多业务逻辑,逻辑链路较深,排查问题较难;
任一内容的逻辑出错或者报错有可能影响后续信令处理和调度逻辑;
统一运行在同一个 Webview,不同内容的消耗很难通过指标进行量化;
课件区内部分层不清晰,耦合较重;
排查问题与不同角色的技术,边界不清晰,沟通成本高,排查效率低。
设计目标
根据直播间的面临的技术难点以及结合我们目前架构所面临的问题,我们希望新架构能够实现以下几个目标:
底层保证内存及时回收;
底层渲染 Runtime 可切换;
通信可靠性保证(NA/FE);
内容隔离;
调度逻辑与不同内容解藕;
单个内容针对性降低内存 /CPU 并且可量化
实践 & 探索
作业帮直播间的前端架构
容器及 SDK 设计
容器设计
容器的定义
容器是直播间任何内容的载体,容器可以是 Native 容器,里面运行着 Native 的组件,可以是一个 Webview 容器,可以是一个 Cocos 容器。容器有以下几个特点:
载体;
独立无依赖;
生命周期;
独立开发,独立部署。
有了容器之后,我们可以把调度逻辑独立出来,下沉到端,这一层可以更专注任务的调度以及信令的处理,与内容解耦。
容器的状态
容器 SDK 设计
有了容器的抽象,各个内容如何集成到容器内以及如何感知容器的不同的状态切换,需要一个标准 SDK 去帮助封装所有的底层细节,对业务无感。SDK 需要具备以下能力:
生命周期钩子;
支持自定义调试;
支持后续的扩展;
基础工具库。
可以通过下面这张图,简单了解 SDK 的容器周期和实现逻辑:
内容播放器
有了容器以及调度,作为内容的业务开发,可以更专注于某一个内容本身,为了保证后续内容的可持续规范性接入,我们针对不同的内容做了一层抽象,定义为内容播放器。我们的主要播放器有 H5 PPT 播放器,H5 互动题播放器,Cocos 课件及互动播放器,画版播放器等,下面会重点介绍下 H5 PPT 播放器以及详细介绍下 Cocos 播放器架构层面的一些优化方案。
H5 PPT 播放器
这里我们以 H5 PPT 为例来介绍基于新架构播放器的分层,H5 PPT 是由编辑器生产出一份数据 + 资源去驱动内容及动画的展示,参考下图,自底往上分别为:
1. 容器层,负责播放器运行时;
2. 前端的基础库;
3. 适配层, 播放器纯渲染层和直播间的适配,包括容器生命周期的桥接,信令的注册与处理 ;
4. 播放器的渲染层。
Cocos 内容播放器设计与优化
Cocos 内容播放器是比较复杂的一个播放器,代码量非常庞大,接下来会重点介绍下我们针对 Cocos 播放器结合新架构做的一系列优化。
当前问题
信令恢复不稳定容易出问题;
加载和恢复性能差,耗时久;
线上问题排查困难,不易锁定。
问题分析
依赖过程的同步同步逻辑,如果出现信令丢失或者信令顺序错误,则会造成逻辑错误,并且后续的所有操作和渲染逻辑都会受到影响;
每次加载 Cocos 引擎、Framework、互动内容都会消耗大量的加载时间,同时依赖过程的恢复逻辑类似于帧同步,决定了断线恢复的时候要恢复之前的所有逻辑,耗时较长,CPU 占用较高;
同步逻辑是同步的 Touch 事件派发,所以数据中的事件表示不出业务逻辑,只有客户端响应后才知道触发了何种逻辑。
解决方案及技术难点
重构信令恢复机制,改为状态同步,状态同步可以解决数据错乱和丢包造成的结果不正确问题,以及恢复时耗时的性能问题,同时排查线上问题也能有明确数据内容来锁定问题。但状态同步有两个问题需要解决:与业务解耦和同步的数据量;
加载性能和内存问题,一个通过采用 Runtime 提前初始化,另一个采用生产时对内容进行标准化限制。
状态同步设计
设计思路:底层捕获输入事件以及渲染数据变化,避免业务感知同步逻辑,状态数据的同步与业务逻辑解耦,降低研发成本,同时增加增量状态和全量状态两种数据,来解决数据量大的问题。
下面这张图是节点状态变化的捕获和分发流程,这个机制在多个端之间也可以正常运作,比如从 A 端捕获的状态派发给 B 端,这就满足的主讲的操作可以同步到学生端的诉求。
下面是状态数据量的优化策略,虽然全部数据已经通过上面的逻辑控制的比较低了,但是为了更高效,并不是每帧都同步全部数据,是在关键帧把数据同步,其它帧只同步增量,这样既保证了数据量也保证了准确性,即使有丢失错乱,但是在下次全量数据是也一定能够恢复正确状态。
内存和 cpu 优化
上一部分容器的抽象,容器可以被调度,可以被销毁,在一定程度可以保证渲染内容的及时回收,接下来这部分针对不同内容做的一些优化手段。内存 /CPU 优化在我们场景中有很多的优化动作,包括客户端独立进程,Cocos 方向等,下面的环节重点介绍基于 Webview 运行时的内容的优化。
浏览器渲染过程
上面是浏览器渲染相关比较核心的一张图,如果你深入理解了浏览器整个渲染流程,就很清楚浏览器会在哪些阶段会有 CPU 以及内存的消耗,我们可以大概列举如下:
JS 代码复杂度及运行频率,会导致 CPU 消耗,操作 DOM 也会消耗内存;
JS 代码内存泄漏;
DOM 会被解析 LayoutObject,所以复杂大量的 DOM 嵌套,会导致 CPU 和内存的消耗;
CSS 同样会被解析 Object,复杂的 CSS 规则会导致 CPU,CSS 规则也可能导致 GPU 内存的消耗;
图片资源,解码 CPU 消耗,渲染是高内存消耗;
回流、重绘会导致 CPU 内存增长;
巧用合成层会减少 GPU 内存消耗,要避免层爆炸。
图片渲染过程
IO;
解码(CPU 解码,位图 -> GPU);
渲染管线 (顶点计算 / 光栅化 / 片元着色 / 帧缓冲渲染到屏幕)。
图片内存
位深 (color depth (32 /24 …)。
尺寸 (图片分辨率)。
体积 (磁盘存储所需大小)。
图片占用显存的计算方式如下:
width * height * (colorDepth/8) /(1024 * 1024)= X (M)
1024 * 1024 * (32/8)/(1024 * 1024) = 4M
可以看出图片尺寸对于内存的影响较大。
优化手段
图片压缩 / 图片裁剪;
GIF 抽帧,GIF 按照 12fps 抽帧;
动画降帧,Lottie 按照设计帧率运行,并降低至 12fps;
互动题题版,运行时按需加载;
GIF 作为合成层渲染,GPU 降低较明显;
信令发送频率及数据量降低;
白板相关优化涉及内容较多,不在这里阐述;
Cocos 框架优化,在上一部分内容播放器优化里有介绍。
总结
黑白屏降至千分位;
追查问题高效;
复杂度降低,迭代成本降低;
单一内容问题,不影响后续播放。
作者介绍:
张士标,作帮直播课堂前端团队负责人,致力于课件、互动系统的持续性能优化和探索。
评论