Categories
程式開發

新弹幕引擎架构设计,提高业务开发效率


一、背景简介

优酷旧有的弹幕功能经过长期业务迭代,添加各种需求,代码耦合越来越严重,一个 View 显示里用 if else 兼容了各种各样的显示样式;为了同时使用在 iPhone 版和 iPad 版 APP 里,又加了不少机型判断。最终导致新增功能越来越困难,于是重新设计一套灵活好用的弹幕引擎框架提上日程。

本文主要分享优酷 APP 目前正在使用的新版弹幕引擎库,与播放器解耦、可独立使用,无第三方依赖,打包后大小仅有 60 多 KB。

二、定制能力

1)支持定制弹幕子 View 显示;

2)支持定制各项参数;

3)支持屏蔽某些弹幕;

4)可由业务实现新的弹幕显示形式,不局限于默认的从右向左滚动、置顶、置底。

三、架构设计简介

弹幕引擎库的主要作用是显示弹幕的滚动、或者置顶置底然后淡出,它需要知道一批数据、某条数据使用某种子 View 渲染以及使用某种排版形式,同时还需要一些必要的参数,如行高。

首先定义几个名词:

排版类型:目前支持从右向左滚动、置顶、置底,业务可自己实现其它特殊的排版;

UI 风格:指单条数据表现出来的 ui 样式,库内置一个简单的 Label 显示,业务可自己实现其它的 ui 样式,如左边一个图片右边文字。

为了提供较强的定制能力,满足各种需求,引擎库使用了插件注册方式提供各种能力,包括默认的排版类型和 UI 风格也是通过插件注册的,下面是整个库的架构设计图:

新弹幕引擎架构设计,提高业务开发效率 1

引擎库对外导出的内容包含图里上层的 DanmakuView、Data Driver,及下层的 DataPlugin、 Setting Plugin、Layout Plugin、Ui Plugin,中间的一层业务不可见。

DanmakuView 是弹幕子 View 显示的父容器,业务可把它放在任何一个业务页面,并给它提供合适的 frame,它提供了注册插件,开始暂停动画、直接塞数据、获取当前显示的弹幕 View、数据、重新布局等方法。

整个流程是业务通过左侧数据驱动或者直接通过 DanmakuView 给到数据后,数据被分类缓存在 Data Cache Manager 里,内部布局 Engine 开始启动内部定时器,从 Cache 拿数据,有了数据后通过 ViewPool 取到一个新建的或者之前用过被回收的相同类型子 View ,然后从 SettingPlugin 拿到当前需要的参数,一并交给子 View 去计算宽高,然后再尝试调用 LayoutPlugin 的方法去排版,如果此时 DanmakuView 容器里能排版,则通知子 View 去渲染更新内部元素,如果不能排版则等待下一个定时器事件、或直到有新一批数据给到后被丢弃。

四、插件简介

所有插件创建后都通过 YKDanmakuView 的 registerPlugin 方法进行注册。

所有插件的属性和方法都带有 ykdm_前缀避免与业务的属性或方法冲突。

  1. UI 风格插件

Ui Plugin: 提供渲染数据的子 View,ui 插件要实现 YKDanmakuUiStylePlugin 协议,通过 uiType 表明自身负责创建哪种数据的弹幕子 View ,同时子 View 类要实现 YKDanmakuItemViewProtocol 协议,通过 viewSize 方法告知引擎当前子 View 的宽高,引擎确认剩余空间是否够排版,如可以则调用 renderView 方法由插件负责显示子 view 内部元素, viewSize 方法内部不建议去真正的刷新内部 view 显示,因为此时可能因为排版不下而放弃,多余的刷新显示动作浪费性能。

新弹幕引擎架构设计,提高业务开发效率 2

上面截图中可以实现多个 ui 插件用来表现多种不同的风格,有带图的,有纯文本的,当然相似的也可以用一种 ui 插件,然后 view 内部根据数据显示隐藏部分子 view。

  1. 排版插件

Layout Plugin: 当 View 创建完成后,布局插件用来实现具体的排版类型,库里已经默认实现了从右向左滚动、置顶、置底,插件需实现 YKDanmakuLayoutPlugin 协议,通过 layoutType 区分是哪种排版类型。

  1. 参数设置插件

Setting Plugin: 用来提供引擎库必要的一些基本参数以及某条弹幕数据是否需要过滤掉不显示,需实现 YKDanmakuSettingPlugin 协议,基本参数通过 YKDanmakuSettingParam 对象告知引擎库,包含行高、显示几行、滚动持续时间、置顶置底的淡出动画时间等参数。

  1. 数据相关插件

显示子 View、排版、参数都有了,还差一个不可缺少的内容,数据从哪儿来?

1)数据基本格式

数据会通过数组提供给弹幕引擎,数据基类都需实现 YKDanmakuItemInfoProtocol 协议中要求的几个方法:

a)layoutType 提供布局类型:告知引擎使用哪种 LayoutPlugin 排版,内置 kYKDanmakuLayoutTypeScroll、kYKDanmakuLayoutTypeTop、kYKDanmakuLayoutTypeBottom,对应着从右向左滚动、置顶、置底;

b)uiType 提供 UI 风格类型,告知引擎使用哪种 UIPlugin 去创建 View 及计算大小、渲染;

c)forceShow 此数据是否是需要强制显示,用在 vip 用户、自己发的弹幕等高优先级的情况,当有下一批数据来更新替换上一批数据时,上一批数据中如存在 forceShow 为 True 的数据,那么即使排版不下此数据也会强制排版(可能会与之前排版的弹幕部分重叠);

d)contentText 弹幕的文本内容,当未提供 UIPlugin 时,默认使用此属性显示一个文本 View。

2)数据提供形式

数据的提供形式可以分为两大类,一种是一次性的简单给予,一种是持续不断的给予或者有定制需求。

a)如果业务是在图文如漫画业务上把用户评论作为弹幕显示,数据一般是一次性给的,那么可以直接通过 DanmakuView 的 setDataArray 方法将弹幕数据提供给引擎,引擎按序用完数据结束显示;

b)除上面这种简单情况,如果数据需要一批批的给,比如像视频播放器一样每秒更新数据,或者数据更新不是替换而是追加,或者数据要循环使用,都可以用下面这种用法:

对于第二种稍复杂的情况,需创建一个 Data Driver(继承自:YKDanmakuBaseDataDriver)用来驱动数据更新,配合 DataPlugin 提供数据,DataPlugin 需实现协议 YKDanmakuDataPlugin。

Data Driver 可以接受播放器的通知(比如每秒一次播放进度更新)、或者内置定时器、或从实时通道接收原始数据、或一次性驱动,总之当需要更新数据时调用基类的 triggerFetchDataWithParams 方法带上业务自己规则的参数,此时 DataPlugin 的 triggerFetchDataWithParams 实现会被调用,参数也会传递过来, DataPlugin 根据参数的不同通过不同的方式取得解析好的数据,如访问后端 api、读取离线缓存、使用参数中提供的原始 json 数据等等,然后通过 callback 同步或者异步返回给弹幕引擎。

DataPlugin 其中几个方法的作用:

a)dataDriver: 引擎内部关联 DataEngine 使用,通过这个方法返回 Data Driver 实例即可;

b) dataRefreshMode: 可选,表示此数据更新是清除旧数据再添加(YKDmDataRefreshMode_Replace)、或是追加(YKDmDataRefreshMode_Append),默认 Replace;

c)recyclableData:可选,表示提供的数据使用完成后是否继续从头开始使用,比如漫画或者图文型弹幕可能提供一批数据后,后续反复使用。默认 NO。

五、交互需求建议

新弹幕引擎架构设计,提高业务开发效率 3

如上图点击后被点击的弹幕暂停,同时显示一个小面板(小面板业务创建并添加到父 View 容器中)。

1)如果弹幕子 View 需要点击交互,那么此子 View 需要重载 hitTest:withEvent:方法,不然无法响应点击;

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   point = [self convertPoint:point toView:self.superview];
   if ([self.layer.presentationLayer hitTest:point] != nil) {
      return self;
   }
   return nil;
}

2)业务可在 UI Plugin 里 constructView 方法或者 View 创建 init 代码里注册各种点击事件,当用户点击后进行需要的操作,如唤起输入框回复此弹幕,或者显示一个小面板;

3)YKDanmakuItemViewProtocol 里 renderView 方法里 YKDanmakuLayoutParam 参数的有属性表明是否是重新使用此子 view 显示一个新数据,当更新参数或者父 View frame 变化导致当前在屏幕上的子 View 重新刷新时此参数是 false,此参数可用来做曝光埋点等业务。同时可选方法 notifyShowComplete 表示此子 view 显示完成即将被回收,可以用来清除 text、image 或者配合计算某种 text 显示了多少次,做些彩蛋之类的需求。

六、性能优化建议

1)Setting Plugin 的 settingParamByLayoutParam 方法调用很频繁,实现需尽可能高效,比如计算一次后缓存起来供下次使用;

2)当 YKDanmakuView 的 frame 改变后,会自动对当前已经显示的弹幕子 View 重新排版和刷新,比如一些业务横竖屏不同状态下子 View 文字大小、滚动速度会有不同,此时业务无需再调用 YKDanmakuView 里的 reloadSetting 方法重复刷新;

3)参数设置中的滚动时间变化后后续排版会自动使用新参数,无需调用 YKDanmakuView 里的 reloadSetting 方法;

4)在 frame 不变的情况下,如要单独更新文字颜色、字体大小、显示行数、过滤弹幕等可调用 reloadSetting 方法,为提搞性能,刷新标记参数可选择以下其一:

a)kYKDanmakuLayoutFlagColor 只重绘颜色,不影响大小,此模式跳过大小计算逻辑,提高性能;

b)kYKDanmakuLayoutFlagSize 更新了字体大小,一般要影响子 view 的整体布局,但不更新颜色;

c)kYKDanmakuLayoutFlagFilter 通过 settingPlugin 的 needFilter 方法控制整个 view 显示或隐藏、或者调整了整体显示行数,只隐藏或者显示子 View,不重新计算大小及刷新显示;

d)kYKDanmakuLayoutFlagAll 需要完整计算位置及渲染逻辑。

5)与上述对应,子 View 的重新渲染代码建议这样写:

- (void)ykdm_renderViewWithLayoutParam:layoutParam...... {
   if ([layoutParam needChangeAll]) {
      //对内容修改,如label 赋值,对imageView 设置图片等等
   }

   if ([layoutParam needChangeColor]) {
      //需要更新颜色,可设置文字颜色,各种背景色等等
   }

   if ([layoutParam needChangeSize]) {
      //需要调整view 各项大小,如字号更新,图片大小更新
   }

   if (layoutParam.firstLayout) {
      //用新数据进行渲染,埋点等
   }
}

作者 | 阿里文娱高级无线开发工程师 趋势