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