抖音 iOS 推荐 Feed 容器化总结 原创 精选
作者|王展、张宇、罗群锋、谷春晖
背景
抖音 Feed 容器在推荐、关注、同城、朋友等多个场景中使用,每个场景都有自身的逻辑和业务,最终汇总在 FeedViewController 中,随着业务的迭代,代码越来越臃肿,面临如下的问题:
- 容器类(FeedViewController) 有 10000+行,还有十多个业务分类,整体的理解和维护成本高
- 容器类 框架和业务边界不清晰,框架代码的修改不收敛和不规范,业务改动可能导致线上问题,如数据层不收敛导致的问题:自动删除导致一次滑动多个视频或者自动跳转到第一个视频等问题
- 容器类 承担了推荐、关注、朋友三个大场景,细节的业务逻辑差异较多,目前多业务代码耦合在一起,增加新功能时需要考虑其他业务方,容易引入问题,开发和测试效率低
- 内流容器和外流容器,形态相似但是代码分离,主体代码重复,新增功能时需要在两个类中做重复开发,如:视频预加载优化等,开发和维护成本高
- 核心功能的监控和代码防劣化的体系不完善
Feed 容器多场景下承载业务
Feed 容器承载了基础功能、直播、登录、登出、性能监控、预加载等多个功能。
由于之前没有做好管控,导致容器中业务相互耦合严重,业务边界不清晰,开发过程中稍有不慎,就会对其他业务造成影响。
而且随着业务迭代,逐渐呈现劣化趋势,尤其是对于新业务接入,面对负责的代码无从下手。
业务迭代效率低
由于代码都在容器类中直接修改,一个版本经常会有多个业务在容器中进行修改导致冲突的情况,此时就需要多方进行 review,保证改动不出问题,往往还要平台业务的同学进行支持,业务的整体迭代效率比较低。
防劣化&监控缺失
业务耦合,对代码改动没有监控,导致 FeedViewController 越来越膨胀。因为没有合理架构导致无法做拆分,代码劣化越来越严重,而且基于现状无法进行防劣化。
目标方案
为了解决上述问题,首先设定好目标,然后根据目标提出解决方案,最终落地实现,验证目标是否达成。
目标
- 架构分层,明确每层职责,容器和业务解耦,多业务之间解耦,做到容器和业务各自闭环;
- 业务组件可插拔,不同场景支持灵活的组合和扩展业务组件;
- 搭建监控体系,实现稳定性、性能、问题定位,建立看板,实时了解各项指标;
- 防劣化,容器和业务分仓隔离,收敛维护人员;
思路
根据上述的目标,从下面四点进行思考和设计:
- 明确业务开发痛点,多业务合作开发效率低、设计不合理模块使用成本高等;
- 自上而下设计,保证整体业务架构设计的合理性,明确优化方向;
- 分层开发和上线验证,降低上线风险和全量成本;
- 架构防劣化,收益可衡量;
方案
针对 Feed 容器内部多场景、多业务耦合导致整体维护困难,新业务接入成本高的问题,首先按照场景、业务和功能维护进行拆分梳理。在拆分完成后为了方便各个业务进行维护,设计了 ControlerKit 工具实现了生命周期方法的分发,并且通过 Context 进行状态管理,实现了各个业务间的通信和状态维护。
整体架构
基础容器
Feed 基础容器,采用组件化框架,支持基础组件和业务组件的动态组合和扩展,由业务无关、统一的列表形态组成,通过数据驱动页面展现。同时对外暴露生命周期事件,方便组件进行监听。其中基础容器由平台方进行统一维护,并提供了完善的监控体系,方便进行问题的定位和追查。
基础组件
Feed 容器的基础组件部分,采用的方式是平台方统一进行维护。目前的基础组件,主要包括播放控制、播放策略优化、列表预加载以及页面管理等。
其中,全屏 Feed 相关的基础组件,为多业务共用,具备可复用、可扩展等优势。
业务组件
业务组件是和业务强相关的组件,业务方可以根据自身的需要进行灵活定制,组件本身可插拔,由各业务方进行维护。
应用场景
业务方基于 Feed 容器,组合业务组件和基础组件构建的页面,在构造过程中可以基于配置文件实现容器的定制,比如推荐和关注。
容器化工具
多个业务耦合在同一个容器中,导致容器类越来越臃肿,一方面造成各方同时维护越来越困难,另一方面对于新业务和新同学接入十分不友好,需要花费很多时间熟悉上下文以避免改动对其他业务造成影响。
为此设计了 ControllerKit 库,该库实现了复杂页面的分发,解决 ViewController 臃肿问题,规范代码拆分标准,提供分发方法的能力。各个接入方按照规则注册后,实现自己关心的生命周期方法,并在方法中实现对应的逻辑即可。
ContainerViewController
ContainerViewController 是容器 ViewController,实现了 ContainerProtocol,保存了上下文环境,负责了各个生命周期方法的分发。
ContainerProtocol
声明了容器对外提供的属性和方法,方便各个 SubController 进行访问。
ControllerProtocol
声明了基础的声明周期和共有的方法。
Controller
Controller 是将 ViewController 中的代码拆分出来的子模块,可以接收分发出来的 viewDidLoad、viewWillAppear 等生命周期及自定义方法调用,还可以向 ViewController 中添加子 View。
ControllerManager
ControllerManager 负责 Controller 的注册、管理、方法分发。通过 classNameArray 返回 Controller 的字符串类名数组即可,可以支持 Controller 在其他仓库的能力
Manager 需要声明分发的 Controller 协议,只需要声明,不需要实现,Manager 内部会通过消息转发机制统一分发。
各角色之间的关系
ContainerViewController 实现了 ContainerProtocol,并持有 ControllerManager,各个子 Controller 注册到 ControllerManager 中,各个 Controller 可以通过 ContainerProtocol 访问容器的能力,ControllerManager 通过 ControllerProtocol 里面声明的方法进行分发。
比如:ContainerViewController 初始化后调用 viewDidLoad 时,会通过 ControllerManager 依次分发到实现该方法的 controller 中,各个 Controller 在自己的 viewDidLoad 方法中实现自己的逻辑即可。
Controller 优先级
- 方法分发优先级按照数组提供的顺序,因此更基础的 Controller 应排在前面
- 优先级由注册顺序决定,因此不同方法优先级无法调整,也不希望有调整,无法满足时,通过其他方式实现
Feed 容器的实现
根据 ControllerKit 对 Feed 容器的类结构改造如下所示
- FeedViewController 作为容器,实现容器能力,对外通过 FeedContainerProtocol 被访问
- Controller 对应业务组件
- FeedControllerManager 负责组件的注册、管理和事件的分发
基于 ControllerKit 的设计和实现
各个类和协议的介绍:
FeedContainerProtocol
- 容器层通过 FeedContainerProtocol 对外提供能力
- 避免业务方直接访问和修改容器类
- 该协议提供了业务层需要的各种能力和接口
- 由平台方进行维护
FeedControllerProtocol
- 业务层协议通过 FeedControllerProtocol 声明
- 定义了各个生命周期相关的方法,被各个业务 controller 实现
- 各个实现业务只需要在对应的生命周期方法中增加自身的逻辑即可
- 被注入的 controller 会在相应的时机被调用到
- 业务自闭环
Context 与 ContainerProtocol 的定位和区别
- FeedContainerProtocol 用来给 controller 提供 FeedViewController 实现的能力
- FeedContext 中存放 Controller 共用的状态
- 两个都能实现通信,但 context 更偏重于状态,而 ContainerProtocol 更偏重于能力,比如页面滚动、数据刷新
业务组件定义
- 定义业务 Controller 类
- 实现 FeedControllerProtocol 协议
- 在对应的生命周期方法中实现对应的业务逻辑
- 若 FeedControllerProtocol 不满足情况时根据之前说明方式在协议中增加新的生命周期方法,同时同步增加到 FeedContainerProtocol ,以便分发
重构后业务迭代方式
- 框架由平台业务架构方维护
- 其他业务的框架扩展需要提交到架构方,由架构方开发
- 其他业务提交的方案和修改,交由架构方 review
- 业务方的代码,业务方自闭环
防劣化建设
为了防止随着业务的迭代,Feed 容器逐渐劣化,需要进行防劣化建设。首先进行框架和业务分仓:
- 代码隔离,修改权限收敛;
- 框架部分,线下做 Pipeline 准入,Lint 检查是否符合容器规则; 业务方修改容器代码,review 通过后才能合入
新方案优势
- 业务解耦,明确了业务和容器的职责,边界清晰
- 降低 FeedViewController 维护成本
- 减少新业务接入成本
- 方便做防劣化
接入示例
以下以兴趣选择和业务为例,介绍新老业务的接入。
新功能接入 - 兴趣选择
兴趣选择是新的类型的卡片,需要进行卡片注册并处理相关逻辑。
历史方案
FeedViewController 直接进行修改,包括如下内容:
- 增加状态管理属性
- 需要在 tableview delegate 和 scroll 滚动等多个方法中增加相应的处理逻辑
- 处理注册卡片逻辑
新方案
抽取单独的业务 Controller
- 在生命周期方法中处理兴趣选择相关逻辑
- 业务相关的属性在 Controller 中声明和维护
Controller 注册到 ControllerManager
在对应的 Controller 中进行自己的业务处理即可,不需要了解容器本身的其他业务逻辑
存量功能拆分 - Feed 监控
Feed 监控功能在 FeedTableVC 中处理了很多业务,而且这些逻辑也其他业务存在着耦合。
- 网络请求监控和数据处理
- 页面滚动
- 播放处理
- ...
采用新方案进行拆分
首先创建 FeedMonitorController,增加业务相关的属性、生命周期方法中实现对应的逻辑,之后抽取单独的业务 controller 在生命周期方法中处理熟人相关逻辑。同时注册到 controllerManager 中,并设置 AB、原有代码判断 AB。上线验证,全量后删除容器老代码。之后业务自闭环,再进行迭代时直接在 FeedMonitorControlle r 内容修改即可。
当前进展&后续规划
规划和节奏
1 | 2 | 3 | 4 |
梳理现状; 重构方案设计和评审; | 新增功能基于新组件开发; | 业务接口合理化:Feed 容器对外暴露能力,业务调用; | 组件化框架横向应用,详情页 Feed 等使用新架构 |
重构后的收益
- 业务解耦后,容器本身稳定,业务方各自维护自身业务,提高了整体的稳定性
老容器 | 新容器 |
因为业务耦合,需要了解 Feed 的结构和多业务的细节,新同学熟悉的时间需要 2 天左右;在实现过程中,由于多个业务同时进行迭代,相互影响,质量无法保障 | 只需要在自己的业务 Controller 开发即可,无需关心容器的结构以及其他业务方,极大的提高了开发和迭代效率;改动不影响其他业务线的代码,保障了代码的稳定性 |
- 全量业务在业务组件中实现了自闭环
版本进行了映射
版本 | 新方案 MR | 老方案 MR | 老方案占比(老 MR/(新 MR+老 MR)) |
1.7 - 2.0 | 39 | 19 | 32.8% |
1.3 - 1.6 | 31 | 18 | 46.15% |
0.9 - 1.2 | 25 | 13 | 34.21% |
0.5 - 0.8 | 16 | 23 | 58.9% |
0.1 - 0.4 | 12 | 19 | 61.2% |