
源起
在 iOS 開發(fā)中,UITableView 可以說是最常用的控件。幾行代碼,實現(xiàn)對應方法,系統(tǒng)就會給你呈現(xiàn)一個 60 幀無比流暢的列表,讓初學者成就感爆棚。然而隨著開發(fā)的深入,我們就會慢慢覺察到當前的 UITableView 實現(xiàn)會有這樣或那樣的問題。
- 繁瑣的重用流程
幾乎所有 TableView Adapter 中都有如下的代碼 registerClass(Nib):forCellReuseIdentifier 進行 cell 重用的注冊,后續(xù)又需要使用 dequeueReusableCellWithIdentifier: 獲取對應 cell。蘋果的這套重用機制對于開發(fā)者來說相當簡單友好,但寫多了難免覺得重復乏味。同時如何給 cell 設置一個有意義且不重復的 reuseIdentifier 又會成為眾多強迫癥程序員的煩惱之一。
- 不安全的 model 和 cell 映射關系
隨著業(yè)務深入,一個 UITableView 往往會包含多種 model,對應不同形式的 cell,那么建立 model 和 cell 的映射關系就會非常蛋疼,無論是if else,switch,還是 map<model,cell> 都不是那么的優(yōu)雅,每當 model 類型有所增刪,開發(fā)者往往需要心驚膽戰(zhàn)地檢查各處實現(xiàn)方法里是否進行了正確的處理。
- 單調(diào)的優(yōu)化過程
業(yè)務繼續(xù)深入,為了保證相關代碼整潔,易于拓展和性能高效,除了維護 model 和 cell 關系(ModelCellMap)外,我們往往需要引入各種類做職責分離:DataSource 管理數(shù)據(jù)源,LayoutManager 負責排版和提供預計算高度能力,CellHeightCache 提供高度緩存,Interactor 提供事件路由和處理等等,這樣可以一定程度減輕代碼膨脹的問題。但也不是完美的:套路都是類似的,即使你熟練掌握了這些所謂的設計原則,在實際操作中仍有大量的重復代碼。
- 數(shù)據(jù)源和 UI 不綁定
當 model 變化時,我們往往需要通過當前 model 位置反推出 cell 在 UITableView 中的位置(即 indexPath),然后做相應的更新處理,反之亦然。但這部分工作無非是數(shù)組遍歷,尋找 index,重復且繁瑣,稍有不慎還有出錯導致崩潰的可能。
組件化方案
為了解決如上問題,同時也受到 IGListKit 和 React.js 的啟發(fā),M80TableViewComponent 提出了一種組件化的解決方案,實現(xiàn)類似 React.js 的 “單向數(shù)據(jù)綁定” 功能,同時將大量的重復計算歸納在組件內(nèi)部,上層使用者只需要根據(jù)當前業(yè)務創(chuàng)建相應組件并組合使用即可。
基礎組件
為了實現(xiàn)整個 UITableView 的流程, M80TableViewComponent 引入三個基礎組件:
- M80TableViewComponent
- M80TableViewSectionComponent
- M80TableViewCellComponent
顧名思義,他們分別對應 UITableView,Section 和 UITableViewCell。用前端技術做類比的話,M80TableViewComponent 就是我們定義的 VirtualDOM,而 UITableView 則是真正的 DOM。前者記錄虛擬的層次結構,后者仍負責最終的渲染。具體關系參考下圖:

簡單使用
定義組件
一個簡單的 M80TableViewComponent 定義如下

這是一個用于文本列表顯示的組件,只實現(xiàn)最基本組件協(xié)議
- 當前組件對應何種 UITableViewCell: - (Class)cellClass
- 當前組件對應 UITableViewCell 高度是多少: - (CGFloat)height
- 如何通過當前組件配置 UITableViewCell: - (void)configure:(UITableViewCell *)cell
和 UITableView 聯(lián)動
定義完組件后,我們只需要按照順序?qū)⒔M件加入父組件中,即可完成和 UITableView 的綁定。

具體效果詳見 Example Project
特性
看完上述的使用方式后,你很可能將 M80TableViewComponent 當成一種固定數(shù)據(jù)源組裝方式而已,并沒有其他新意。但事實上,除了充當固定結構數(shù)據(jù)源外,它還有如下優(yōu)勢
單向綁定
當我們使用組件時,一旦當前 M80TableViewComponent 和 UITableView 關聯(lián),后續(xù)針對 M80TableViewComponent 的所有操作都會實時反應到 UITableView 之上,包括對 cell component 的移除,刷新,插入,以及 section component 的插入,移除和刷新。我們不再需要繁瑣地通過 controller 同時操作 view 和 model 以保證其一致性,只需要單純操作 component 即可:component 將根據(jù)自身層次結構計算出對應的 UI 層次結構,在修改 component 內(nèi)部結構的同時也會自動獲取到對應的 cell 對象進行修改。這樣做的好處是上層開發(fā)只需要關注 component 即可,而不再關心 indexPath 相關的計算過程,從而規(guī)避繁復的 indexPath 計算及計算錯誤導致的崩潰。
靈活組裝功能
使用 M80TableViewComponent 可以輕易支持多種不同類型的數(shù)據(jù)模型,同時由于我們將復用層次從 vc/tableview 下降到 cell/section component 層次,也更方便了在不同場景下的組合使用。
自動重用
每一個 M80TableViewCellComponent 在第一次被使用時都會通過 M80TableViewComponentRegister 根據(jù)上下文信息自動綁定 reuseIdentifier 和 cellClass 的關系,完成 cell 的重用。默認使用當前 cell component 的類名作為 reuseIdentifier,既能保證不與其他 cell 重名,又省去了取名之苦。
高度優(yōu)化和局部刷新
在 iOS 中比較蛋疼的事情是如何判斷兩個對象相等:在不使用 runtime 的場景下,往往需要業(yè)務層添加大量冗余代碼用于支持對象比較,而使用了 runtime 又會對業(yè)務侵入過多。在 M80TableViewComponent 中我們使用了一種不基于 runtime 且比較輕量的方法:
所有的 M80TableViewCellComponent 都遵循 M80ListDiffable 協(xié)議,以用于組件內(nèi)部的一致性判斷:
- (NSString *)diffableHash;
默認情況下,每個 cell component 在初始化時都會有自己唯一的 cellIdentifier 作為 diffableHash。
以此為出發(fā)點,我們就可以進行如下場景的優(yōu)化。
- 自動 cell 高度緩存
- 通過 ListDiff 算法實現(xiàn)的 section 局部刷新
當開啟高度緩存選項時,M80TableViewComponent 計算 cell 高度后會自動記錄 diffableHash 和 height 的對應關系。后續(xù)再次刷新將自動獲取對應高度而無需再次計算。當一個 cell 有多重狀態(tài),需要在不同狀態(tài)下展示不同高度時,則可以通過業(yè)務狀態(tài)返回不同的 diffableHash 進行高度切換。除了高度緩存外,M80TableViewComponent 也提供了一種預計算高度的機制,在組裝完 cell component 后,只需要簡單調(diào)用基類方法 measure 就可以直接完成預計算。
而適用局部刷新時,cell component 的 diffableHash 將做為唯一標識:old components 和 new components 根據(jù) diffableHash 被 hash 到不同桶內(nèi),沖突桶中的 component 標記為 move,不沖突桶中的 component 則為 add/remove。詳細算法可參考 M80ListDiff 函數(shù)。在合適的場景下,使用 ListDiff 進行 section 的重新載入,而不是人工計算各種變化信息后進行逐一操作,能夠在保證性能的前提下,簡化開發(fā)過程和良好的界面表現(xiàn)。
使用貼士
不同于以往構建 UITableView 的常見用法,使用 M80TableViewComponent 推薦所有操作都針對 component 進行。
- 涉及單個 cell 的操作,直接使用 cell component 本身的方法,如 remove,reload 方法。
- 涉及單個 section 內(nèi)多個 cell 變化,可以考慮每次重新 setComponents 或調(diào)用 reloadUsingListDiff 進行局部刷新。
- 涉及到多 section 多 cell 變化,則可以重新組裝所有 component。一方面這樣做比較簡單,不容易出錯。另一方面 component 只是 viewmodel,在真正刷新前的批量操作并不會有過多性能問題。