背景
1. B站的直播间作为整个APP中交互最为复杂的单页面之一,其承担的业务量已经不亚于一个小型APP。对比APP的结构会发现许多相同处,但与组成APP的各个独立Activity不同,直播间由各个独立的视图组成。
从APP维度看每个Activity是一个业务单元,类比直播间维度每个直播间中的View是一个业务单元。
2. 业务逻辑基于MVVM的设计分为三层(Service即M层),理想状态下各个业务间的交互是内聚的,各业务间不会感知到其他业务的存在,View显示需要的数据和状态都由各自对应的ViewModel提供。
3. 现实情况中业务交互并没有那么理想,直播间中的一个View显示的数据会受到其他业务的数据、状态影响,因此一个View除了需要处理自己内聚的逻辑为还需要关心其他View的数据变化。
01 直播间业务交互现状
1.1 现有交互逻辑
直播间是一种单页面强交互型业务场景,一个业务就经常需要会关心其他业务的状态,因此垂直方向拓展业务场景就会很多,直播间中的业务几乎都是在垂直屏幕方向上进行拓展的,产品在新增业务时往往会将重要的业务放在更显眼的地方,因此需要尽量使重要的业务不被遮挡。
然而,直播间的视图又不是一成不变的,与常规页面面对的业务场景不同,直播间的视图除了需要响应用户自己的操作外,还需要根据主播和其他正在看直播的用户操作展示和改变视图,为了保证整体展示逻辑没问题经常会有视图联动的需求。
下图列举了部分直播间业务的构建位置和层级关系,按层划分类似的视图直播间中有60多个:
现存的设计将大部分业务逻辑集中在ViewModel中处理,因为有着LiveData的存在,数据变化的监听在ViewModel和View间变得容易。
当其他ViewModel已经存在自己View关心的LiveData或调用方法时,开发者很自然的会去引用一个现有的其他ViewModel来观察他持有的LiveData或调用其方法。
1.2 现有交互逻辑带来的问题
不规范的LiveData的使用和糅杂在一起的业务逻辑导致了ViewModel引用的滥用,使得View间的耦合愈发严重,下图是现有直播间ViewModel的引用关系:
直播间内日益复杂的业务会进一步加重View之间的耦合,在分析了上图中的引用关系后,会发现目前之所以会出现View引用其他业务的ViewModel的场景,主要有两个原因:
1. 与View对应的ViewModel无法提供需要的数据,其他Viewmodel持有需要的数据
2. View的某个操作或数据改变需要告知其他业务,其他Viewmodel包含需要调用的方法
直播间内日益复杂的业务会进一步加重View之间的耦合,特别是在新增的业务对老业务有改动时,开发人员惯性的去寻找有没有现成的逻辑处理,如果有就会想办法复用,而现存的设计将大部分业务逻辑集中在ViewModel中处理,就必定会导致ViewModel引用的不可控。
1.3 问题的分析
直播间各业务引用关系错乱只是表象,最直接的原因就是业务数据的访问的不规范,错综复杂的引用关系会加重业务间的耦合情况,耦合的业务逻辑又会增加业务加载流程和数据分发的复杂度,周而复始,形成了恶性循环。
针对以上问题打破恶性循环,我们通过脚本分析了直播间内60+业务模块,列出了1400+个引用ViewModel的具体使用场景,并且整理了的理想中的数据提供方作为后续改造的参考:
02 基于视图划分的业务数据
直播间中的业务使用MVVM的结构构建,我们提供了一套统一的构建模板来构建和管理各层逻辑,单个业务中每层有各自维护的数据和状态信息,这些数据禁止跃层访问,并跟随各层的生命周期创建和销毁。
2.1 数据使用场景
数据使用分为三个场景:初始化数据,交互数据,对外提供数据
初始化数据
一个业务的初始化一般处于房间加载的某个任务中
业务初始化,由当前任务提供该阶段可以访问的数据,作为初始化数据
初始化数据会转化为业务专有的数据结构(图中Data),供内部逻辑、视图使用和管理
交互数据
一个房间所有业务初始化完成后,如果没后续的交互,理论上是完全静止的,任何改变当前直播间的动作都可以看做是一个交互,而每个交互都会带上一些数据
用户每次对业务View的点击、滑动等都会产生一些事件并带上相应的数据,这些事件可能直接在View层就已经消费掉,也可能会触发一系列的逻辑交互
Socket和Http的响应作为另一类交互数据的来源,由Service层向上通知到各个业务逻辑层,业务逻辑需要监听这些数据变化做相应处理
此外每个业务都可能会关心其他业务的改变,这些往往改变也会带来一些数据,依据这些数据业务可能需要对自己的逻辑和View进行相应的操作
对外提供数据
在交互数据里有提到关心其他业务的变化,这部分的变化应该由每个业务在API中决定暴露那些事件和数据供外部使用
如果关心某个业务的变化,可以通过ServiceManager获取对应业务的API,通过关心业务API暴露的方法来获取、订阅数据
a. provideData类型方法:对外暴露提供数据的方法,由类型方法提供的数据表示,该业务可以对外提供的数据
b. notifyChange类型方法:有数据变化需要对外通知时对外暴露的方法,其他业务通过该类型方法可以订阅相应的变化通知
每个业务在提供数据时应该考虑清楚需要暴露的数据,不可直接暴露Data给外部使用
每个业务在接收到其他业务的变化通知时,应该在对应的处理里消费掉传过来的数据,不要持有该部分数据
2.2 数据的流向
进入直播间时会请求一组初始化接口,响应数据将会由数据分发器管理,分发到各个业务的Services,不同各个业务拿到各自关心的数据后放到各自的businessData中
各业务的Service中将会管理业务所持有的数据,ViewModel想要获取或改变某个数据时,需要持有对应业务的Service
ViewModel中将各业务的原始数据组合处理后通过LiveData通知对应的View,View可以通过ViewModel对原始数据进行修改
View间的事件(纯粹的UI变化)将由ViewEventManager作为通道进行传递,传递过程中的数据为一次性数据,不可作为该次事件处理外的逻辑数据使用
03 直播间的视图结构和业务区域
3.1 直播间的结构和区域划分
在加入上下滑逻辑之前,房间的概念与整个Activity等价,一个房间在Activity被销毁时释放所有资源
在加入上下滑逻辑后,房间的概念变为滑动组件中的一个Item,一个房间在Item被划走时释放所有资源,为了更好的理解业务运行逻辑,我们根据直播间的视图结构对业务区域进行了划分
a. 容器区域(Global)包含滑动组件和DIALOG业务层(目前仅话题和各种引导用到),在进房时创建该区域
b. 房间区域(Room)包含房间业务层和播放器业务层,这两部分视图均挂载在滑动组件的RoomItem上,在滑动停止时创建该区域
在用户执行的滑动操作停止后会释放上一个RoomItem的资源,并重新创建房间区域挂载到停止后的RoomItem根布局上,而容器区域中的资源仍然随Activity的销毁而销毁,当前的房间区域也会随容器区域的销毁而销毁
业务仅需要声明自己属于Global还是Room区域,并在创建、销毁的回调中编写逻辑,而不需要关心自己何时被创建和销毁
3.2 按区域划分加载流程
以构建item中房间容器的时机为分割点,之前的加载流程属于容器区域,之后的加载流程属于房间区域
房间初始化接口请求(P0、P1接口)比较特殊,请求时机以及请求的上游处理逻辑属于容器区域,但是接口响应数据的处理逻辑属于房间区域
在直播间销毁的流程中,在容器销毁流程和房间销毁流程中都需要销毁的逻辑,归为房间区域管理,仅在容器销毁流程中销毁的逻辑归容器区域管理
04 架构演进中的一些思考
1. 架构最后是为业务需求场景服务的,那它也要顺应业务的变化而适时调整。来B站直播的期间经历了直播间从不能滑动到可以上下滑,从老直播间为主到以新版直播间为主,整个产品交互形态发生了巨大变化,新的架构演进方向往往取决于对新业务形态的认知。
2. 随着业务的不断发展和改变以及组织架构的调整,因为赶工期、图方便而设计不合理但刚好能用的代码会越来越多,原先用起来很顺畅的架构必定会慢慢腐烂变质,一直修修补补只是在掩饰问题和推迟问题的爆发,作为一线开发我完全可以理解开发时的内心想法:
别人都这么写,就算是不合理,跟着也总不会错
时间不够了,这坨代码真烂,但我只是来改点小功能,等谁改不动了谁去改
现有的架构根本没考虑到我这种场景,先随便找个地方放着,能实现需求再说
3. 基于以上思考,我认为架构演进的目的主要有两个:
打破团队的不满:打破保守的做法,要积极面对不合理的地方。团队不定期需要着手开启重构,将大家平日对代码的不满释放出来。整理直播间老大难的历史债,将架构的腐化(效率降低、抱怨上升)转化为架构优化的动力。
团队意识的培养:培养全员架构的意识,架构演进的过程中会牵扯众多模块的重构,在各个模块重构的过程中传达架构的思想、形成团队共识,形成“人人都是架构师”的氛围。