久久创业网

得物App直播复杂页面架构实践

2023-06-17
原创 李振全 患上物技能 1. 布景 以后直播间营业迭代愈来愈频仍,开辟职员也愈来愈多,而简直百分之九十的需要都是正在 直播不雅众页,直播主播开播这两个页面上的功用开辟以及代码积累。因而,页面中代……

原创 李振全 患上物技能

1. 布景

以后直播间营业迭代愈来愈频仍,开辟职员也愈来愈多,而简直百分之九十的需要都是正在 直播不雅众页,直播主播开播这两个页面上的功用开辟以及代码积累。因而,页面中代码的收缩速率相称快。

直播代码以前做了一次 Layer 层级的拆分,将全部直播间依照视图的层级停止了分别,经过 ViewStub 对于每一个层级停止渐进式的加载,以进步页面的加载速率,Layer 层级拆分后的组件图以下:

但如今愈来愈不克不及支持今朝迭代的速率,由于以前拆分的粒度因此视图层级为单元的,粒度比拟粗,简单代码收缩。为了避免 Layer 层代码逻辑过分收缩大师也各显法术,经过一个个协助类,xxxController/xxxHelper/xxxUtil/xxxManager/xxxAdapter/xxxHandler 等将各个营业逻辑封装起来,代码作风各别,浏览起来也比拟费力。即便如许各类抽取,也没法防止 Layer 逻辑的收缩,且各协助类不一致的API 以及代码标准, 很依附内部挪用的精确性,今朝最年夜的 Layer视图交互层代码量曾经靠近两千行,音讯面板及商卡层也有一千行摆布。

规划 XML 庞大,预埋控件良多。直播间有差别品种的直播间(带货、文娱),有各类 AB 战略,另有各类勾当出口,年夜局部组件实践是没有展现的,能够一个直播间只要要展现此中一两项,可是今朝良多都是预埋到 XML 中代码静态把持其显隐,这实在便是有意义的功能耗费。

Layer 层级之间相互援用,职责不但一,比方:语音连麦逻辑是正在一个独自的LiveRoomVoiceLinkLayer语音连麦层, 连麦按钮正在 FunctionLayer 的底部规划中,需要是当主播承受连麦的时分,这个按钮不成点击,当挂断连麦的时分,这个按钮能够点击。

代码是如许:

也便是正在 语音连麦层 挪用了宿主 Fragment,而后经过宿主Fragment获得到 FunctionLayer 也便是视图交互层的 View,去停止把持的。这就形成了层级之间的互相挪用,且修正了本没有属于本人层级的视图。如许会有甚么成绩呢?比方说一个没有分明这块逻辑的同窗需求对于连麦按钮的点击形态停止修正,他正在 FunctionLayer 找到了这个按钮,天经地义的正在 FunctionLayer 停止修正,可是能够收到预期外的后果,由于此时能够这个按钮被 LiveRoomVoiceLinkLayer 给改归去了,本属于 FunctionLayer 的视图被其余中央修正了。

视图层级架构由复杂的视图层级干系酿成了相互挪用的干系:

正在这个布景下,咱们一个开端的设法主意是将直播页面停止组件化拆分。组件外部处置组件各自的营业,粒度更小,逻辑更内聚。直播页只要注册这些组件,而后依据数据驱动的思惟,当数据发作改动时,由于组件外部 observe 了本人关怀的数据,就能够主动更新组件。如许另有个益处便是能够静态注册组件,也便是只要认真正需求展现此组件的时分再去注册组件模块,而不用一切组件都正在进入直播间的一霎时去初始化,增加功能的耗费。

也便是希冀是如许一个新的组件架构:

直播间、商详都黑白常庞大的一个页面,而咱们的买卖商详曾经有了组件化拆分的乐成示例,直播作为厥后者实在做起来的难度并非很年夜,可是却有可预感的较年夜的收益。

目的:由于咱们这次改革属于代码重构,以是中心目的是让顺序正在功用稳定的条件下代码更明晰,拓展性更好,更解耦,改进代码收缩成绩; 主要目的是正在做完此项重构后可以晋升代码的运转功能,能够便当的静态注册以及删除了组件,经过数据驱动静态 inflate 增加规划,删除了预埋正在 XML 中的规划,做到按需加载营业模块,进步页面启动速率。

2. 根底页面组件计划

咱们经过笼统一个轻量化的页面内组件单位——IComponent 来处理下面布景中提到的多少个成绩。

(1)针对于各协助类不一致 API 成绩 ,根底组件需求一致 API 回谐和代码标准。

相似 Activity 性命周期模板办法,组件需求默许供给组件本身自力的性命周期以及宿主如 Activity/Fragment/父Component 性命周期回调。

以前的 Layer 是正在宿主中自动挪用 lifecycle.addObserver():

新的组件能够简化这步操纵,默许完成对于父组件的性命周期察看,同时,本身性命周期跟从父组件性命周期,父组件烧毁,子组件也会随着烧毁,相似于 ViewGroup 以及 View 之间的干系。这也契合营业上的界说,比方咱们要烧毁某一个年夜的组件咱们只要解绑这个年夜组件便可,无需关怀其外部的子组件的解绑。

若何完成呢?

这里咱们界说了 IComponent 接话柄现 LifecycleOwner 以及 DefaultLifecycleObserver, 作为以及宿主 Fragment/Activity/Component 性命周期的绑定,对于外表露零碎同名也以及零碎性命周期分歧的回调(onCreate/onStart/onResume/onPause/onStop/onDestroy),而且供给组件本身的 onAttach/onDetach 性命周期, onAttach 代表绑定上了宿主,onDetach 代表从宿主解绑;开辟者只要要正在特定的性命周期编写营业逻辑便可,比方 onAttach 停止一些初始化 View 以及监听的操纵, onDetach 停止一些开释资本的操纵,onResume/onPause 做一些暴光埋点的上报等等,使营业组件能够专一本身营业的逻辑完成。

(2)针对于代码收缩成绩,组件需求撑持恣意层级嵌套,细化组件的粒度。

组件能够有他的子组件,子组件也能够有子组件的子组件,组件之间的组合体式格局便是一个树形构造。

这里咱们笼统了 IComponentRegister 接口用来供给组件注册以及办理的功用,完成此接口需求供给一个 Map 记载注册的组件,和对于应的注册反注册办法。

父组件的性命周期是正在 register 的时分绑定,unregister 的时分解绑。今朝完成IComponentRegister接口的有 BaseLiveActivity/BaseLiveFragment/BaseComponent, 他们都撑持组件的注册以及反注册。

(3)针对于 Layer 层级之间相互援用,职责不但一成绩,新组件需求做到同级组件之间没有强依附。

完成 IComponentRegister 接口的宿主供给的 map 是 protected 润色的,以是组件不克不及间接获得到同级的组件。

那本来组件之间的耦合怎样处置呢?

组件之间的耦合分为两种:

一种是组件 A 依附组件 B 的一个形态,比方点赞模块时能够需求晓得存眷模块的存眷形态;关于这类我的处理计划是组件 A 没有间接依附组件 B 而是依附一个 LiveData 数据形态,存眷的形态经过 LiveData 存正在ViewModel中,组件 A 依附的就酿成 ViewModel 便可。

一种是组件 A 依附组件 B 的 View 视图,比方领导组件正在表现批评领导的时分需求批评控件作为定位锚点,由于批评控件属于底部操纵区,而底部操纵区是需求依据直播范例和 AB 静态去创立的,以是正在初始化的时分能够批评控件并无初始化实现,以是正在注册领导模块的组件时经过结构函数没法获得到批评控件。

这类依附我的处理计划是组件 A 没有间接依附组件 B 而是经过根规划 view或许AB组件对于应的父规划 findViewById 获得 组件 B 的 View 视图,以下:

如许实在其实不算是完整解耦,只是将显式的依附其余组件酿成了依附规划中的一个控件,而且留意这类体式格局获得的View只能读,不克不及写,算是今朝解耦的暂时计划。也是本次改革做的没有完满的一个中央,但思索到假如要完全处理这个成绩,本钱较高,开辟也会更方便捷,这里终极弃取仍是开辟服从优先。大师假如有更好的处置体式格局,欢送评论辩论。

(4)针对于规划 XML 愈来愈庞大预埋控件良多的成绩,新组件便当的需求撑持 ViewStub 改革。

2.1 笼统 BaseComponent

Android 使用架构指南说到,最紧张的准绳便是别离存眷点:

那末咱们笼统出的 BaseComponent 要做的便是这么一件事,将页面逻辑依照营业停止分别为一个个的页面内组件,将存眷点内聚到本人营业的自力组件中。

BaseComponent 完成 IComponent 以及 IComponentRegister 作为组件的最小单位;IComponent 用来供给最根本的组件的绑定解绑性命周期,IComponentRegister 接口用来供给组件注册以及办理的功用,和以及宿主的性命周期绑定。组件以营业逻辑为中心,能够对于应1或者多个 UI 元素或许 0 个 UI 元素,多个UI 元素从营业逻辑层面属于统一个模块,0 个便是纯逻辑层面的组件罢了。

2.2 笼统 BaseLiveComponent

由于用户端有高低滑切换直播间的交互,为了兼容汗青逻辑,以是正在 BaseComponent 之上又笼统了一个 BaseLiveComponent,BaseLiveComponent 承继自 BaseComponent 以外又完成了 ILiveLifecycle(直播特定性命周期), LayoutContainer(kotlin 主动findViewById 插件)。

ILiveLifecycle 有三个性命周期,辨别是 onSelected 选中,unSelected 没有选中,对于应 ViewPager 的 onPageSelected 以及滑出,这两个性命周期仍是比拟好了解的,destroy 又是甚么呢?

Destroy 从字面意义看是烧毁的时分做一些资本的开释,但实践并非宿主 onDestroy性命周期 而是 onPause时且isFinishing=true,这是由于 onStop/onDestroy 回调是依据 IdleHandler 来的,假如主线程音讯行列步队音讯比拟多,那末 onStop/onDestroy 就会耽误回调(具体可参考http://juejin.cn/post/6936440588635996173),资本开释onStop/onDestroy这里就会开释不迭时。因而咱们给直播间的 ILiveLifecycle 加了一个特定的 destroy 办法,用来实时开释资本。

下面咱们说到 BaseComponent 有两个性命周期,onAttach 代表绑定上了宿主,onDetach 代表解绑了宿主,那开释资本如今就有了三个中央,宿主的 onDestroy(),ILiveLifecycle 的 destroy(), BaseComponent 的 onDetach()。

onDetach destroy onDestroy 傻傻分没有清

onDestroy 是甚么?
宿主的性命周期回调(Fragment/Activity)。

onDetach 是甚么?
Component 被反注册时回调。

destroy 是甚么?
直播间特定性命周期,由于 onDestroy 性命周期的耽误回调,咱们再 onPause 判别 isFinish 履行 destroy 停止实时的资本开释。

明白了三个办法的寄义,为了简化组件的运用,咱们将 destroy 以及 onDetach 停止了兼并,由于二者实质目标都是做资本开释的。为了增加对于汗青代码的影响,这里 BaseLiveComponent 完成 destroy 办法,用final 润色便可,如许承继了 BaseLiveComponent 的组件就没法完成 destroy 办法只能挑选 onDetach 办法了。

别的 BaseLiveComponent 多了一个 CustomLiveLifecycleOwner 这个是干甚么的呢?CustomLiveLifecycleOwner 是 BaseLiveComponent 的一个外部类:

次要用来处置直播间高低滑的场景成绩。

直播间 Activity 内有一个 ViewPager2, ViewPager2 高低滑的是一个个自力的直播间 Fragment, 以是直播间 Fragment 就有了 onSelected/unSelected 的自界说性命周期,如今有些状况下咱们需求监听 Activity 域的 ViewModel 的 LiveData 形态,那末咱们希冀的便是正在直播间滑出 unSelected 的时分没有呼应此监听,咱们能够依据 isSelected 属性判别,也能够间接运用 customLiveLifecycleOwner 作为 observe 传入的 LifecycleOwner 参数。

页面滑出的时分会挪用到 customLiveLifecycleOwner 的 unSelected(), 如许customLiveLifecycleOwner 的 lifecycle便是 ON_STOP形态, ON_STOP 没有会呼应对于应的 LiveData 监听,也就契合了咱们的希冀。onSelected, destroy 同理。

2.3 组件之间通讯

依据咱们的需求以及架构近况,这个并无甚么纠结的,由于咱们以前的架构形式便是 Jetpack MVVM 形式,以是这里仍是持续相沿 ViewModel LiveData 的体式格局停止通讯。如许以前的代码根本就不必动,间接挪到 Component 便可。组件外部本人办理本人的形态,组件只要存眷本身功用完成而不用干系与其余组件的交互。这里需求留意的两点,一个是LiveData数据倒灌成绩,这个网上良多材料我就再也不赘述了,另一个是若何让 LiveData 坚持音讯同步的分歧性,甚么意义呢?

比方咱们正在 ViewModel 界说了一个心跳接口前往的 MutableLiveData 数据:

那末里面只需拿到此 ViewModel 就能够更新这个 MutableLiveData 的数据,这便是音讯同步的纷歧致。若何做到分歧呢?实在也十分复杂,只要要界说成如许:

公有化 MutableLiveData,表露出一个 pulic 的 LiveData,为何要这么做呢?

这是由于 LiveData 关于写操纵是 protected 的,关于读操纵是 public 的,也就限定了内部拿到 ViewModel 的时分只能读,不克不及写,如许就限定了咱们只能正在 ViewModel 外部停止写操纵,这就内聚了逻辑确保了音讯同步的分歧性。

概况能够参考“重学安卓:架构组件 “分歧性成绩” 片面剖析”【1】

3. 改革

正在实现根底组件的代码计划后,咱们来看下若何停止代码的详细改革。

3.1 根底运用以及留意点

(1)界说一个营业 Component, 挑选承继 BaseComponent or BaseLiveComponent。

BaseLiveComponent 以及 BaseComponent 的差别正在于,BaseLiveComponent 有直播间高低滑非凡性命周期处置(onSelected, unSelected, destroy)

比方直播间用户端由于是有高低滑交互的,以是能够挑选承继 BaseLiveComponent;主播真个组件由于不高低滑的交互,以是能够挑选承继 BaseComponent;假如是想主播端以及用户端共用的组件而且没有触及高低滑的交互处置,那末能够挑选承继 BaseComponent。

(2)按需完成 onAttach onDetach 办法,和需求正在宿主性命周期履行的营业逻辑。
正在 onAttach 绑定性命周期宿主落后行初始化的操纵,如注册 LiveData 监听,初始化 View 形态,配置点击事情等,由于此时 Component 工具曾经创立实现且绑定了宿主的性命周期。

这里需求留意的是:宿主性命周期办法如 onCreate 能够是跟绑定的 Fragment onCreate 回调机遇有必定耽误的,宿主性命周期办法回调取决于你registerComponent 的机遇。比方咱们能够是 View 创立终了 onCreatedView 的时分才会 registerComponent,或许更晚的机遇,此时 Fragment onCreate 曾经履行终了,以是实践上 Component 组件回调的 onCreate 真正机遇是你创立 Component 并绑定宿主性命周期的时分。固然假如你是正在 Fragment 的 onCreate 就去注册 Component 组件,那末onCreate 回调机遇是以及 Fragment 差未几的。

(3)正在宿主中经过registerComponent 注册子组件。

registerComponent 能够主动跟从宿主的性命周期。宿主能够是 BaseLiveActivity, BaseLiveFragment, BaseComponent, 或许其余完成了 IComponentRegister 接口的类。(引荐只正在庞大的 Activity/Fragment 运用)

以优惠券组件为例(这里省略了年夜局部营业逻辑代码,侧重凸起组件支流程代码):

//1. 创立 CouponComponent 承继 BaseLiveComponent class CouponComponent( override val containerView: View, private val itemViewModel: LiveItemViewModel, private val fragment: LiveRoomLayerFragment ) : BaseLiveComponent(containerView) { private val couponViewModel by fragment.viewModels<CouponActivityViewModel>() //2. onAttach 初始化 View, 注册 LiveData 监听 override fun onAttach(lifecycleOwner: LifecycleOwner) { super.onAttach(lifecycleOwner) initView() registerObservers() } private fun initView() { couponNewIcon?.setOnClickListener { val couponListFragment = LiveProductCouponListFragment.newInstance(LiveProductCouponListFragment.ADAPTER_ROOM) couponListFragment.show(fragment.childFragmentManager) } } private fun registerObservers() { //心跳 itemViewModel.notifySyncModel.observe(this, { notifySyncModel(it, itemViewModel.roomId) }) //获得优惠券信息 487 couponViewModel.couponPopupRequest.observe(this, onSuccess = { m, _, _ -> showCouponDialog(m) }) 。。。。 } // 2. 页面滑出逻辑如暗藏弹窗,重置形态等 override fun unSelected() { super.unSelected() dissmissCouponDialog() } // 3. 假如要做一些资本开释写正在 onDetach override fun onDetach(lifecycleOwner: LifecycleOwner) { super.onDetach(lifecycleOwner) } // 4. 可选,能否开启 EventBus 开关 override fun enableEventBus(): Boolean { return true } @Subscribe(threadMode = ThreadMode.MAIN) fun onAutoPopTypeEvent(event: LiveAutoPopTypeEvent) { if (event.autoPopType == AutoPopTypeConstant.TYPE_COUPON) { couponActivityInclude.syncAutoCoupon() } } }

3.2 进步页面翻开速率

直播间有相称多的这类挂件视图,今朝咱们年夜少数都是将规划预埋到 XML,而后静态把持其表现暗藏,但实践上用户侧真正需求展现的挂件依据直播间,依据用户都是纷歧样的,有的人没有是新人,那末这个新人义务的视图就不必加载,有的直播间不抽奖,不限时直降勾当也同样没有需求加载对于应的挂件视图。

颠末组件化拆分后,每个挂件实践上都相称于一个页面内小组件,被注册到直播间中,组件依附 的是一个 View,那末只需咱们将 View 改为 ViewStub 并正在 ViewStub inflate 实现后再注册到直播间就能够了,改革起来也就非常轻松了。

以抽奖出口以及限时直降出口为例:

第一步: 先将其组件预埋的 XML 规划交换成 ViewStub。

第二步:配置ViewStub inflate 监听,inflate实现时注册组件。

第三步:正在需求加载组件的时分静态 inflate ViewStub 组件。

改革儿女码大约是如许:

/** * 直播间交互层级 */ class LiveRoomFunctionLayer( override val containerView: View, private val layerFragment: LiveRoomLayerFragment ) : BaseLiveComponent(containerView) { override fun onAttach(lifecycleOwner: LifecycleOwner) { super.onAttach(lifecycleOwner) initView() initViewStubComponent() registerObserver() initChildComponent() initViewStubObserver() } /** * 初始化静态加载的组件 */ private fun initViewStubComponent() { vsLotteryEntrance?.setOnInflateListener { _, inflated -> registerComponent(LotteryEntranceComponent(inflated, lotteryViewModel, layerFragment)) } vsSecKillEntrance?.setOnInflateListener { _, inflated -> registerComponent(SecKillComponent(inflated, liveInfoViewModel, layerFragment.childFragmentManager)) } ...... } private fun initViewStubObserver() { lotteryViewModel.notifyAutoLotteryInfo.observe(this, Observer { it?.apply { vsLotteryEntrance?.inflate() } }) liveInfoViewModel.notifyLiveDiscountInfo.observe(this, Observer { it?.apply { vsSecKillEntrance?.inflate() } }) .... } /** * 初始化非静态加载组件 */ private fun initChildComponent() { registerComponent(BottomViewComponent(containerView, layerFragment)) registerComponent(EnergyComponent(energyContainerView, layerFragment)) registerComponent(ShareOrReportComponent(layerFragment)) registerComponent(FansGroupComponent(containerView, layerFragment)) registerComponent(CouponComponent(containerView, liveInfoViewModel, layerFragment)) registerComponent(XXXComponent(depenView, dependData)) ...... }

如许就实现了直播间挂件真正需求的时分再去加载,改革本钱很低,且代码可读性也很好。

小Tip:这里是先收到 LiveData 形态判别需求加载 ViewStub 以后再去注册子组件的,存正在一个前后干系,那会没有会招致后注册的子组件外部收没有到 LiveData 形态更新呢?谜底是没有会的,由于咱们这里更新数据形态运用的 MutableLiveData, MutableLiveData 能够复杂的了解为粘性的事情,也便是后注册监听,假如发明这个 MutableLiveData 是有值的就会回调 observe 监听。比方,抽奖组件外部把持 View 的显隐逻辑都不必动。

4. 收益

(1)一致代码作风,晋升了代码可读性;经过运用一致的自界说的 Component ,供给明白的组件性命周期回调,让大师写一个营业组件,依照模板就能够疾速完成,无需再借助良多特定的协助类,且代码作风一致,浏览起来也会更易。

(2)无效改进了代码收缩成绩;经过便当的注册子组件的体式格局,代码能够内聚到子组件中去,组件以营业逻辑分别,全部直播间就由一个个或者年夜或者小的组件积木搭建起来。这也契合职责繁多准绳,父组件担任组合子组件,子组件担任外部详细营业,当一个组件渐渐迭代愈来愈庞大后,就能够思索能否能够拆份子组件,由于子组件的拆分计划的十分便当,根本只要挪一下代码地位,注册一下就行了,以是也没有怕呈现组件代码收缩浏览坚苦的成绩了。

(3)进步了页面功能;采纳呼应式编程思惟,运用 ViewStub 静态加载组件视图以及注册组件,防止 View 会合加载的功能成绩,和能够间接从泉源防止不应呼应到的事情呼应。由于不被注册的组件,其外部的营业逻辑也都是相称于没有存正在的。

(4)经过公有化组件汇合,防止营业组件间的耦合依附;能够注册子组件之处城市有一个 map 来保护子组件,能够新增以及删除了子组件,可是不供给获得的办法。也便是说,防止了子组件A被其余子组件B所援用,每一个组件只处置本人的营业,增加营业间不用要的耦合。

5. 考虑&总结

5.1 考虑

正在汗青代码迁徙进程中要有所让步,有些汗青代码写的欠好之处很想随手改失落,抑制一下,由于如许做极端简单形成线上成绩。咱们一次改革就专一于一件事,比方此次就次要存眷组件之间的分别,本来的逻辑只需注入对于应的视图依附和数据依附放到对于应的性命周期便可,无需窜改任何营业逻辑。比拟庞大一点的便是改革进程中发明的营业之间的耦合局部若何解耦,这个由于每一个营业都纷歧样,这里就需求详细成绩详细剖析。

没有要一下步子跨的太年夜,按部就班的停止改革,组件是跟着迭代静态添加以及删除了的,一会儿局部拆分其实不理想,今朝直播间的组件化阅历了三个版本继续停止改革,根底组件框架曾经搭建终了,已经拆出了二十多个中心组件,后续方案依据营业迭代的详细营业,再停止代码上的拆分或者聚合改革,如许也没有会给测试照成太年夜压力。

同时要思索到组员之间的承受本钱,主动以及大师停止评论辩论以及同步,大师的认同才是代码架构改革前面可否落地的关头,如许才干增加改革进程中招致的成绩,过渡也更加滑润圆滑。

5.2 总结

架构不全能的,只要更合适的。正在直播间庞大页面的场景下,为了改进日趋收缩的营业代码积累,进步代码可读性,因而计划了如许一套页面组件化的计划。每一个民气中大概都有本人对于组件计划,代码拆分的设法主意,以上是我的一些考虑,复杂总结下有以下多少点:

(1)更过细的营业组件拆分,别离营业存眷点,组件撑持父子干系,契合繁多职责准绳;

(2)组件供给一致性命周期回谐和模板办法,进步代码可读性以及波动性;

(3)组件之间的通讯采纳呼应式编程的体式格局,让组件监听内部形态的变革,当外界形态变革时,组件外部保护好本人的形态便可,完成了依附反转;

(4)正在 BaseComponent 之上笼统了一个 BaseLiveComponent,兼容直播间非凡的高低滑交互,对于外屏障非凡的处置,增加运用者的心智担负。

假如你对于页面组件化有甚么共同的见地,欢送批评区以及我交换~

参考链接:

【1】http://xiaozhuanlan.com/topic/9340256871

*文/李振全

存眷患上物技能,每一周一三五晚18:30更新技能干货
如果感到文章对于你有协助的话,欢送批评转发点赞~

关头词:抢手