背景
抖音 Feed 容器在推薦、關注、同城、朋友等多個場景中使用,每個場景都有自身的邏輯和業(yè)務,最終匯總在 FeedViewController 中,隨著業(yè)務的迭代,代碼越來越臃腫,面臨如下的問題:
- 容器類(FeedViewController) 有 10000+行,還有十多個業(yè)務分類,整體的理解和維護成本高
- 容器類 框架和業(yè)務邊界不清晰,框架代碼的修改不收斂和不規(guī)范,業(yè)務改動可能導致線上問題,如數(shù)據(jù)層不收斂導致的問題:自動刪除導致一次滑動多個視頻或者自動跳轉(zhuǎn)到第一個視頻等問題
- 容器類 承擔了推薦、關注、朋友三個大場景,細節(jié)的業(yè)務邏輯差異較多,目前多業(yè)務代碼耦合在一起,增加新功能時需要考慮其他業(yè)務方,容易引入問題,開發(fā)和測試效率低
- 內(nèi)流容器和外流容器,形態(tài)相似但是代碼分離,主體代碼重復,新增功能時需要在兩個類中做重復開發(fā),如:視頻預加載優(yōu)化等,開發(fā)和維護成本高
- 核心功能的監(jiān)控和代碼防劣化的體系不完善
Feed 容器多場景下承載業(yè)務
Feed 容器承載了基礎功能、直播、登錄、登出、性能監(jiān)控、預加載等多個功能。
由于之前沒有做好管控,導致容器中業(yè)務相互耦合嚴重,業(yè)務邊界不清晰,開發(fā)過程中稍有不慎,就會對其他業(yè)務造成影響。
而且隨著業(yè)務迭代,逐漸呈現(xiàn)劣化趨勢,尤其是對于新業(yè)務接入,面對負責的代碼無從下手。
業(yè)務迭代效率低
由于代碼都在容器類中直接修改,一個版本經(jīng)常會有多個業(yè)務在容器中進行修改導致沖突的情況,此時就需要多方進行 review,保證改動不出問題,往往還要平臺業(yè)務的同學進行支持,業(yè)務的整體迭代效率比較低。
防劣化&監(jiān)控缺失
業(yè)務耦合,對代碼改動沒有監(jiān)控,導致 FeedViewController 越來越膨脹。因為沒有合理架構導致無法做拆分,代碼劣化越來越嚴重,而且基于現(xiàn)狀無法進行防劣化。
目標方案
為了解決上述問題,首先設定好目標,然后根據(jù)目標提出解決方案,最終落地實現(xiàn),驗證目標是否達成。
目標
- 架構分層,明確每層職責,容器和業(yè)務解耦,多業(yè)務之間解耦,做到容器和業(yè)務各自閉環(huán);
- 業(yè)務組件可插拔,不同場景支持靈活的組合和擴展業(yè)務組件;
- 搭建監(jiān)控體系,實現(xiàn)穩(wěn)定性、性能、問題定位,建立看板,實時了解各項指標;
- 防劣化,容器和業(yè)務分倉隔離,收斂維護人員;
思路
根據(jù)上述的目標,從下面四點進行思考和設計:
- 明確業(yè)務開發(fā)痛點,多業(yè)務合作開發(fā)效率低、設計不合理模塊使用成本高等;
- 自上而下設計,保證整體業(yè)務架構設計的合理性,明確優(yōu)化方向;
- 分層開發(fā)和上線驗證,降低上線風險和全量成本;
- 架構防劣化,收益可衡量;
方案
針對 Feed 容器內(nèi)部多場景、多業(yè)務耦合導致整體維護困難,新業(yè)務接入成本高的問題,首先按照場景、業(yè)務和功能維護進行拆分梳理。在拆分完成后為了方便各個業(yè)務進行維護,設計了 ControlerKit 工具實現(xiàn)了生命周期方法的分發(fā),并且通過 Context 進行狀態(tài)管理,實現(xiàn)了各個業(yè)務間的通信和狀態(tài)維護。
整體架構
基礎容器
Feed 基礎容器,采用組件化框架,支持基礎組件和業(yè)務組件的動態(tài)組合和擴展,由業(yè)務無關、統(tǒng)一的列表形態(tài)組成,通過數(shù)據(jù)驅(qū)動頁面展現(xiàn)。同時對外暴露生命周期事件,方便組件進行監(jiān)聽。其中基礎容器由平臺方進行統(tǒng)一維護,并提供了完善的監(jiān)控體系,方便進行問題的定位和追查。
基礎組件
Feed 容器的基礎組件部分,采用的方式是平臺方統(tǒng)一進行維護。目前的基礎組件,主要包括播放控制、播放策略優(yōu)化、列表預加載以及頁面管理等。
其中,全屏 Feed 相關的基礎組件,為多業(yè)務共用,具備可復用、可擴展等優(yōu)勢。
業(yè)務組件
業(yè)務組件是和業(yè)務強相關的組件,業(yè)務方可以根據(jù)自身的需要進行靈活定制,組件本身可插拔,由各業(yè)務方進行維護。
應用場景
業(yè)務方基于 Feed 容器,組合業(yè)務組件和基礎組件構建的頁面,在構造過程中可以基于配置文件實現(xiàn)容器的定制,比如推薦和關注。
容器化工具
多個業(yè)務耦合在同一個容器中,導致容器類越來越臃腫,一方面造成各方同時維護越來越困難,另一方面對于新業(yè)務和新同學接入十分不友好,需要花費很多時間熟悉上下文以避免改動對其他業(yè)務造成影響。
為此設計了 ControllerKit 庫,該庫實現(xiàn)了復雜頁面的分發(fā),解決 ViewController 臃腫問題,規(guī)范代碼拆分標準,提供分發(fā)方法的能力。各個接入方按照規(guī)則注冊后,實現(xiàn)自己關心的生命周期方法,并在方法中實現(xiàn)對應的邏輯即可。
ContainerViewController
ContainerViewController 是容器 ViewController,實現(xiàn)了 ContainerProtocol,保存了上下文環(huán)境,負責了各個生命周期方法的分發(fā)。
ContainerProtocol
聲明了容器對外提供的屬性和方法,方便各個 SubController 進行訪問。
ControllerProtocol
聲明了基礎的聲明周期和共有的方法。
Controller
Controller 是將 ViewController 中的代碼拆分出來的子模塊,可以接收分發(fā)出來的 viewDidLoad、viewWillAppear 等生命周期及自定義方法調(diào)用,還可以向 ViewController 中添加子 View。
ControllerManager
ControllerManager 負責 Controller 的注冊、管理、方法分發(fā)。通過 classNameArray 返回 Controller 的字符串類名數(shù)組即可,可以支持 Controller 在其他倉庫的能力
Manager 需要聲明分發(fā)的 Controller 協(xié)議,只需要聲明,不需要實現(xiàn),Manager 內(nèi)部會通過消息轉(zhuǎn)發(fā)機制統(tǒng)一分發(fā)。
各角色之間的關系
ContainerViewController 實現(xiàn)了 ContainerProtocol,并持有 ControllerManager,各個子 Controller 注冊到 ControllerManager 中,各個 Controller 可以通過 ContainerProtocol 訪問容器的能力,ControllerManager 通過 ControllerProtocol 里面聲明的方法進行分發(fā)。
比如:ContainerViewController 初始化后調(diào)用 viewDidLoad 時,會通過 ControllerManager 依次分發(fā)到實現(xiàn)該方法的 controller 中,各個 Controller 在自己的 viewDidLoad 方法中實現(xiàn)自己的邏輯即可。
Controller 優(yōu)先級
- 方法分發(fā)優(yōu)先級按照數(shù)組提供的順序,因此更基礎的 Controller 應排在前面
- 優(yōu)先級由注冊順序決定,因此不同方法優(yōu)先級無法調(diào)整,也不希望有調(diào)整,無法滿足時,通過其他方式實現(xiàn)
Feed 容器的實現(xiàn)
根據(jù) ControllerKit 對 Feed 容器的類結構改造如下所示
- FeedViewController 作為容器,實現(xiàn)容器能力,對外通過 FeedContainerProtocol 被訪問
- Controller 對應業(yè)務組件
- FeedControllerManager 負責組件的注冊、管理和事件的分發(fā)
基于 ControllerKit 的設計和實現(xiàn)
各個類和協(xié)議的介紹:
FeedContainerProtocol
- 容器層通過 FeedContainerProtocol 對外提供能力
- 避免業(yè)務方直接訪問和修改容器類
- 該協(xié)議提供了業(yè)務層需要的各種能力和接口
- 由平臺方進行維護
FeedControllerProtocol
- 業(yè)務層協(xié)議通過 FeedControllerProtocol 聲明
- 定義了各個生命周期相關的方法,被各個業(yè)務 controller 實現(xiàn)
- 各個實現(xiàn)業(yè)務只需要在對應的生命周期方法中增加自身的邏輯即可
- 被注入的 controller 會在相應的時機被調(diào)用到
- 業(yè)務自閉環(huán)
Context 與 ContainerProtocol 的定位和區(qū)別
- FeedContainerProtocol 用來給 controller 提供 FeedViewController 實現(xiàn)的能力
- FeedContext 中存放 Controller 共用的狀態(tài)
- 兩個都能實現(xiàn)通信,但 context 更偏重于狀態(tài),而 ContainerProtocol 更偏重于能力,比如頁面滾動、數(shù)據(jù)刷新
業(yè)務組件定義
- 定義業(yè)務 Controller 類
- 實現(xiàn) FeedControllerProtocol 協(xié)議
- 在對應的生命周期方法中實現(xiàn)對應的業(yè)務邏輯
- 若 FeedControllerProtocol 不滿足情況時根據(jù)之前說明方式在協(xié)議中增加新的生命周期方法,同時同步增加到 FeedContainerProtocol ,以便分發(fā)
重構后業(yè)務迭代方式
- 框架由平臺業(yè)務架構方維護
- 其他業(yè)務的框架擴展需要提交到架構方,由架構方開發(fā)
- 其他業(yè)務提交的方案和修改,交由架構方 review
- 業(yè)務方的代碼,業(yè)務方自閉環(huán)
防劣化建設
為了防止隨著業(yè)務的迭代,F(xiàn)eed 容器逐漸劣化,需要進行防劣化建設。首先進行框架和業(yè)務分倉:
- 代碼隔離,修改權限收斂;
- 框架部分,線下做 Pipeline 準入,Lint 檢查是否符合容器規(guī)則; 業(yè)務方修改容器代碼,review 通過后才能合入
新方案優(yōu)勢
- 業(yè)務解耦,明確了業(yè)務和容器的職責,邊界清晰
- 降低 FeedViewController 維護成本
- 減少新業(yè)務接入成本
- 方便做防劣化
接入示例
以下以興趣選擇和業(yè)務為例,介紹新老業(yè)務的接入。
新功能接入 - 興趣選擇
興趣選擇是新的類型的卡片,需要進行卡片注冊并處理相關邏輯。
歷史方案
FeedViewController 直接進行修改,包括如下內(nèi)容:
- 增加狀態(tài)管理屬性
- 需要在 tableview delegate 和 scroll 滾動等多個方法中增加相應的處理邏輯
- 處理注冊卡片邏輯
新方案
抽取單獨的業(yè)務 Controller
- 在生命周期方法中處理興趣選擇相關邏輯
- 業(yè)務相關的屬性在 Controller 中聲明和維護
Controller 注冊到 ControllerManager
在對應的 Controller 中進行自己的業(yè)務處理即可,不需要了解容器本身的其他業(yè)務邏輯
存量功能拆分 - Feed 監(jiān)控
Feed 監(jiān)控功能在 FeedTableVC 中處理了很多業(yè)務,而且這些邏輯也其他業(yè)務存在著耦合。
- 網(wǎng)絡請求監(jiān)控和數(shù)據(jù)處理
- 頁面滾動
- 播放處理
- ...
采用新方案進行拆分
首先創(chuàng)建 FeedMonitorController,增加業(yè)務相關的屬性、生命周期方法中實現(xiàn)對應的邏輯,之后抽取單獨的業(yè)務 controller 在生命周期方法中處理熟人相關邏輯。同時注冊到 controllerManager 中,并設置 AB、原有代碼判斷 AB。上線驗證,全量后刪除容器老代碼。之后業(yè)務自閉環(huán),再進行迭代時直接在 FeedMonitorControlle r 內(nèi)容修改即可。
當前進展&后續(xù)規(guī)劃
規(guī)劃和節(jié)奏
1 | 2 | 3 | 4 |
梳理現(xiàn)狀; 重構方案設計和評審; | 新增功能基于新組件開發(fā); | 業(yè)務接口合理化:Feed 容器對外暴露能力,業(yè)務調(diào)用; | 組件化框架橫向應用,詳情頁 Feed 等使用新架構 |
重構后的收益
- 業(yè)務解耦后,容器本身穩(wěn)定,業(yè)務方各自維護自身業(yè)務,提高了整體的穩(wěn)定性
老容器 | 新容器 |
因為業(yè)務耦合,需要了解 Feed 的結構和多業(yè)務的細節(jié),新同學熟悉的時間需要 2 天左右;在實現(xiàn)過程中,由于多個業(yè)務同時進行迭代,相互影響,質(zhì)量無法保障 | 只需要在自己的業(yè)務 Controller 開發(fā)即可,無需關心容器的結構以及其他業(yè)務方,極大的提高了開發(fā)和迭代效率;改動不影響其他業(yè)務線的代碼,保障了代碼的穩(wěn)定性 |
- 全量業(yè)務在業(yè)務組件中實現(xiàn)了自閉環(huán)
版本進行了映射
版本 | 新方案 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% |