本期作者
余洋
哔哩哔哩资深开发工程师
Feed流
Feed 流可以分为两部分来看,即 Feed+ 流,首先看第一部分 Feed,在英文中 Feed 是投喂的意思,也就是你喜欢什么样的内容,就给你什么样的内容,而流则是给用户呈现信息的形式。结合起来就是 Feed 流是一种持续更新并呈现给用户内容的信息流。
维基百科对 Feed 的定义如下:On the World Wide Web, a web feed (or news feed) is a data format used for providing users with frequently updated content.  (来源 https://en.wikipedia.org/wiki/Web_feed
Feed 流将用户主动订阅的若干消息源组合在一起形成内容聚合器,帮助用户持续地获取最新的订阅源内容,Feed 流即持续更新并呈现给用户内容的信息流。
Inline播放
文中该词指 Feed 流里的视频播放。
视频现状
目前,视频内容已经占据了互联网数据总量的80%,并且有越来越多的APP开始提供加载视频功能。即便没有任何技术与应用突破,预计到2022年视频内容的数据总量也将达到82%。这都表明视频在促进人与人交互中的作用得到了广泛的认可。随着视频成为人们休闲娱乐和信息传递的主要方式,越来越多的视频消费被前置,列表内播放视频成为 Feed 流里的一个重要组成部分。
近年来,我们可以看到:越来越多的 APP 在列表内插入可 inline 播放的卡片,如西瓜视频、爱奇艺、新浪微博等,大家都非常默契的将视频消费前置化。但是如何找到符合自己产品的视频化道路,就需要大量的线上试错、数据验证,这些都是产品打磨的重要方式。
所以对于我们开发者来说,如何快速响应业务的试错及数据的回归验证,且降低维护成本成为了迫在眉睫的事项。
场景/业务
目前,B站的首页推荐、动态、搜索等业务都有inline播放的能力,业务的差异带来了不同的交互诉求。如首页推荐在 Banner存在时会优先起播 Banner 的卡片,当用户手动点击下方手动起播卡片时,一次播放周期内,会优先播放该卡,同时首页推荐的 inline 卡片分别有弹幕、点击播放、点赞、拖拽进度等差异化功能。动态列表内,会根据一定条件会延迟起播 inline 卡片,时长可配置。
现有方案
各自为战-散落在各类列表的相似逻辑
对于 inline 播放,大致的流程为:
列表停止滚动/滚动到顶/VC didAppear 时触发检索事件;
判断当前页面是否可播;
检索 tableView/collectionView visibleCells;
计算露出比例,找到符合播放条件的卡片;
对卡片数据源鉴权。
调用播放VC 内精简后的伪代码如下:
目前,只要有 inline 播放诉求的页面,这样的代码就会出现在相应的 ViewController 中。
业务差异带来的“巧妙”解决方案
起初,inline 播放的功能相对简单,只需在列表停止滚动后,找到一张播放卡片起播即可,面板上也只有简单的音量开关按钮,所以最开始的设计都是基于 ViewController 里的checkInlinePlayCard方法,这样做在当时的背景下是“够用”的。但是随着业务的发展,Banner 也需要支持 inline 播放,并且它与列表其它卡片的播放行为存在差异性,于是我们在 ViewController 里记录了 Banner 卡片,checkInlinePlayCard方法内对这部分记录的卡片进行差异化处理,这个需求看起来被“完美”的解决掉了。
再后来,为了验证不同面板对播放时长及卡片点击的影响,产品大大提出了要将普通的 inline 卡片分成三组:只有音量按钮的卡片、可手动拖拽进度的卡片、展示弹幕的卡片。接到需求后,我们将卡片分为三种不同类型,每个卡片关于播放的一些属性是不变的,变化的是面板上的 UI 内容,需求快速上线了,但是我们产生了大量的重复代码,在后续一个内部技改需求中,我们相同的逻辑修改了三遍。
经过后来几个版本的迭代,inline 相关的功能不断丰富,checkInlinePlayCard方法处理了各式各样的的逻辑分支。以首页推荐为例,由于 Banner 及直播、广告卡等业务存在,它的找卡逻辑大概为:先检索一次 visibleCells,判断是否是 Banner 卡,如果是则去走相关的播放逻辑,如果不是则筛选出广告卡、直播卡,分别加入相应的数组中。然后筛选出符合条件的卡片再去判断续播条件,上述操作完成后,还需判断卡片类型是否是一些实验命中的卡,然后走相应的起播逻辑。
上述内容是笔者精简后的起播逻辑,整体从检索到起播的代码行数在2000行左右,里面包含了各种各样的条件判断,逻辑的组合与避让,基本上是一段“上帝”代码,让维护的人苦不堪言,每天都祈祷着千万不要有新的需求进来。
轻量化
反思
在上述例子中,我们不难发现,每一次的需求过来,我们设计的方案都“够用”,"巧妙"地完成了需求,但后来,越来越多的逻辑集中在 ViewController 及checkInlinePlayCard方法中,导致了越来越多的 Flag 和重复的代码。
原以为只修改了部分相关逻辑,却影响了风马牛不相及的其它功能,同时相同功能的代码散落在各个地方,一次变化可能会带来多次相同改动,如果漏改,就意味着某个功能的不可用,往往修复一处问题就会引入新的问题,形成了恶性循环,给我们带来了极大的心智负担。
 设计思路
痛定思痛,我们需要抽象出 inline 播放的基础功能,降低重复代码的再次开发,同时对业务的差异、快速试错提供友好支撑。
大概设计思路如下:
将 ViewController 从繁琐的找卡片、播放卡片中解放出来,将相关逻辑托管;
要支持不同类型的页面:如 UITableView、UICollectionView、UIScrollView 驱动的页面或简单的 UIView 驱动的页面;
要满足不同的检索条件及起播条件:不同卡片的播放条件由各种避让操作变为策略驱动;
管理相同的播放逻辑及状态维护;
要满足不同卡片的不同 UI 及交互诉求:提供自定义面板接口,同时管理面板的展示与移除逻辑。
它的大概的层级如下:
ModuleInlineManager
它负责管理所有与inline相关的事件,由 VC 去持有它的实例,将 VC 从繁重的 inline 管理工作中解放出来,它的构造方法如下:
其内部持有着"两大护法",它们是 ViewHandler、ViewFetcher
ViewHandler
内部监听了vc的生命周期方法,在相应的生命周期回调时,通知 ModuleInlineManager 进行起播停播操作持有 tableView/collectionView 的delegate,当列表真正滚动停止时,将事件告知 ModuleInlineManager,同时将原有的 tableView/collectionView 相关代理方法通过 ModuleInlineManager 回调给VC,不影响其它的逻辑。
ViewFetcher
接收到 ModuleInlineManager 发出的检索消息后,它负责寻找最符合播放条件的那张卡,去除了之前基于 visibleCells 驱动的找卡逻辑,采用 subViews 及剪枝的方式来寻找可播卡片。
找卡流程如下:
它将原先使用的一系列标记位抽象成配置与策略,内部采用优先级标记卡片,最终找到符合业务逻辑的可播卡片。
代码如下:
InlineController
前面我们有聊到,inline 播放在不同业务的表现形式五花八门,尤其是首页推荐上的 inline 播放的功能更是变化快、差异性大,线上同时在跑的实验多达十余种,过去,我们在不同 inline 播放场景下对播放器的管理各自为战,大量相同逻辑在不同业务 copy 一份然后在此基础上做差异化的修改,同时,播放器有时不得不理解业务的一些逻辑,做相应的特殊处理。
不同业务 playerHelper 里的数据构建大致相同,功能不同,导致代码冗余且对 player 层侵染很大,它大概的样子如下:
首页 playerHelper 承载了更多卡片功能,在内部以 if else的方法做逻辑分支管理,如音量和点赞逻辑出现了耦合则又是一条新的分支,改动可能会影响到原有的功能,我们将首页的 playerHelper 展开,结构如下:
有没有一种方案,可以统一管理播放行为又具备良好的业务扩展性?答案是 YES,它就是 inlineController,它由列表上卡片的 ViewModel 持有,在 cell 出列时,将需要展示播放器的视图赋值给它的 inlineView 属性。
结构如下:
DataSource
构建播放器所需的参数。
CustomLayer
播放器上的自定义面板,如音量按钮、弹幕开关等,业务可按需传入自定义视图。
PlayerCallback
接收播放器的一些回调,如播放状态的变化,处理后提供独立 API 抛出,方便业务特殊诉求。
PlayerControl
提供了play、suspend、replay等操作,供 ModuleInlineManager 或业务特殊场景使用,统一与播放器的交互行为。
InlineController 管理了卡片上与播放有关的事务,承载播放视图、提供播放所需数据源、接收播放器的生命周期回调等,我们可以发现,原来散落在各个业务 Helper 里相同的播放控制逻辑、各种自定义的数据模型被收敛在了 InlineController 里。同时,CustomLayer、PlayerCallback 有很好的支撑了业务的差异性,不同业务如有特殊需要可继承 InlineController 完成定制化需求。框架内部提供了一套默认实现,它包含播放生命周期的管理及默认播控面板,如果新业务初期比较轻量,那么 inline 播放功能可以被很快的接入进来。
以默认实现来接入这套框架,大概代码如下:
经过以上相关配置,一个 Feed 流即可完成列表播放功能,如有复杂面板或者交互诉求,在 InlineController 上异化即可。
总结
以上就是我们这次轻量化项目 Feed 流的架构变化,受限于篇幅,有一些细节如起播数据监控、复用逻辑处理等来不及说,我们下次一定!
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路
继续阅读
阅读原文