天天看點

UITableView 元件化源起元件化方案

源起

在 iOS 開發中,UITableView 可以說是最常用的控件。幾行代碼,實作對應方法,系統就會給你呈現一個 60 幀無比流暢的清單,讓初學者成就感爆棚。然而随着開發的深入,我們就會慢慢覺察到目前的 UITableView 實作會有這樣或那樣的問題。

  • 繁瑣的重用流程

幾乎所有 TableView Adapter 中都有如下的代碼 

registerClass(Nib):forCellReuseIdentifier

 進行 cell 重用的注冊,後續又需要使用 

dequeueReusableCellWithIdentifier:

 擷取對應 cell。蘋果的這套重用機制對于開發者來說相當簡單友好,但寫多了難免覺得重複乏味。同時如何給 cell 設定一個有意義且不重複的 reuseIdentifier 又會成為衆多強迫症程式員的煩惱之一。

  • 不安全的 model 和 cell 映射關系

随着業務深入,一個 UITableView 往往會包含多種 model,對應不同形式的 cell,那麼建立 model 和 cell 的映射關系就會非常蛋疼,無論是if else,switch,還是 map 都不是那麼的優雅,每當 model 類型有所增删,開發者往往需要心驚膽戰地檢查各處實作方法裡是否進行了正确的處理。

  • 單調的優化過程

業務繼續深入,為了保證相關代碼整潔,易于拓展和性能高效,除了維護 model 和 cell 關系(

ModelCellMap

)外,我們往往需要引入各種類做職責分離:

DataSource

 管理資料源,

LayoutManager

 負責排版和提供預計算高度能力,

CellHeightCache

 提供高度緩存,

Interactor

 提供事件路由和處理等等,這樣可以一定程度減輕代碼膨脹的問題。但也不是完美的:套路都是類似的,即使你熟練掌握了這些所謂的設計原則,在實際操作中仍有大量的重複代碼。

  • 資料源和 UI 不綁定

當 model 變化時,我們往往需要通過目前 model 位置反推出 cell 在 UITableView 中的位置(即 

indexPath

),然後做相應的更新處理,反之亦然。但這部分工作無非是數組周遊,尋找 index,重複且繁瑣,稍有不慎還有出錯導緻崩潰的可能。

元件化方案

為了解決如上問題,同時也受到 IGListKit 和 React.js 的啟發,M80TableViewComponent 提出了一種元件化的解決方案,實作類似 React.js 的 “單向資料綁定” 功能,同時将大量的重複計算歸納在元件内部,上層使用者隻需要根據目前業務建立相應元件并組合使用即可。

基礎元件

為了實作整個 UITableView 的流程, M80TableViewComponent 引入三個基礎元件:

  • M80TableViewComponent
  • M80TableViewSectionComponent
  • M80TableViewCellComponent

顧名思義,他們分别對應 UITableView,Section 和 UITableViewCell。用前端技術做類比的話,M80TableViewComponent 就是我們定義的 VirtualDOM,而 UITableView 則是真正的 DOM。前者記錄虛拟的層次結構,後者仍負責最終的渲染。具體關系參考下圖:

簡單使用

定義元件

一個簡單的 M80TableViewComponent 定義如下

這是一個用于文本清單顯示的元件,隻實作最基本元件協定

  • 目前元件對應何種 UITableViewCell: - (Class)cellClass
  • 目前元件對應 UITableViewCell 高度是多少: - (CGFloat)height
  • 如何通過目前元件配置 UITableViewCell: - (void)configure:(UITableViewCell *)cell

和 UITableView 關聯

定義完元件後,我們隻需要按照順序将元件加入父元件中,即可完成和 UITableView 的綁定。

具體效果詳見 

Example Project

特性

看完上述的使用方式後,你很可能将 M80TableViewComponent 當成一種固定資料源組裝方式而已,并沒有其他新意。但事實上,除了充當固定結構資料源外,它還有如下優勢

單向綁定

當我們使用元件時,一旦目前 M80TableViewComponent 和 UITableView 關聯,後續針對 M80TableViewComponent 的所有操作都會實時反應到 UITableView 之上,包括對 cell component 的移除,重新整理,插入,以及 section component 的插入,移除和重新整理。我們不再需要繁瑣地通過 controller 同時操作 view 和 model 以保證其一緻性,隻需要單純操作 component 即可:component 将根據自身層次結構計算出對應的 UI 層次結構,在修改 component 内部結構的同時也會自動擷取到對應的 cell 對象進行修改。這樣做的好處是上層開發隻需要關注 component 即可,而不再關心 indexPath 相關的計算過程,進而規避繁複的 indexPath 計算及計算錯誤導緻的崩潰。

靈活組裝功能

使用 M80TableViewComponent 可以輕易支援多種不同類型的資料模型,同時由于我們将複用層次從 vc/tableview 下降到 cell/section component 層次,也更友善了在不同場景下的組合使用。

自動重用

每一個 M80TableViewCellComponent 在第一次被使用時都會通過 

M80TableViewComponentRegister

 根據上下文資訊自動綁定 reuseIdentifier 和 cellClass 的關系,完成 cell 的重用。預設使用目前 cell component 的類名作為 reuseIdentifier,既能保證不與其他 cell 重名,又省去了取名之苦。

高度優化和局部重新整理

在 iOS 中比較蛋疼的事情是如何判斷兩個對象相等:在不使用 runtime 的場景下,往往需要業務層添加大量備援代碼用于支援對象比較,而使用了 runtime 又會對業務侵入過多。在 M80TableViewComponent 中我們使用了一種不基于 runtime 且比較輕量的方法:

所有的 M80TableViewCellComponent 都遵循 M80ListDiffable 協定,以用于元件内部的一緻性判斷:

  • (NSString *)diffableHash;

預設情況下,每個 cell component 在初始化時都會有自己唯一的 cellIdentifier 作為 diffableHash。

以此為出發點,我們就可以進行如下場景的優化。

  • 自動 cell 高度緩存
  • 通過 ListDiff 算法實作的 section 局部重新整理

當開啟高度緩存選項時,M80TableViewComponent 計算 cell 高度後會自動記錄 diffableHash 和 height 的對應關系。後續再次重新整理将自動擷取對應高度而無需再次計算。當一個 cell 有多重狀态,需要在不同狀态下展示不同高度時,則可以通過業務狀态傳回不同的 diffableHash 進行高度切換。除了高度緩存外,M80TableViewComponent 也提供了一種預計算高度的機制,在組裝完 cell component 後,隻需要簡單調用基類方法 

measure

 就可以直接完成預計算。

而适用局部重新整理時,cell component 的 diffableHash 将做為唯一辨別:old components 和 new components 根據 diffableHash 被 hash 到不同桶内,沖突桶中的 component 标記為 move,不沖突桶中的 component 則為 add/remove。詳細算法可參考 M80ListDiff 函數。在合适的場景下,使用 ListDiff 進行 section 的重新載入,而不是人工計算各種變化資訊後進行逐一操作,能夠在保證性能的前提下,簡化開發過程和良好的界面表現。

使用貼士

不同于以往建構 UITableView 的常見用法,使用 M80TableViewComponent 推薦所有操作都針對 component 進行。

  • 涉及單個 cell 的操作,直接使用 cell component 本身的方法,如 remove,reload 方法。
  • 涉及單個 section 内多個 cell 變化,可以考慮每次重新 setComponents 或調用 reloadUsingListDiff 進行局部重新整理。
  • 涉及到多 section 多 cell 變化,則可以重新組裝所有 component。一方面這樣做比較簡單,不容易出錯。另一方面 component 隻是 viewmodel,在真正重新整理前的批量操作并不會有過多性能問題。

推薦文集

* 抖音效果實作

BAT—最新iOS面試題總結

iOS面試題合集

繼續閱讀