UITableView的Cell複用原理和原始碼分析

發表於2016-08-04

簡介

在我們的日常開發中,絕大多數情況下只要詳細閱讀類標頭檔案裡的註釋,組合UIKit框架裡的大量控制元件就能很好的滿足工作的需求。但僅僅會使用UIKit裡的控制元件還遠遠不夠,假如現在產品需要一個類似 Excel 樣式的控制元件來呈現資料,需要這個控制元件能上下左右滑動,這時候你會發現UIKit裡就沒有現成的控制元件可用了。UITableView 可以看做一個只可以上下滾動的 Excel,所以我們的直覺是應該仿寫 UITableView 來實現這個自定義的控制元件。這篇文章我將會通過開源專案 Chameleon 來分析UITableView的 hacking 原始碼,閱讀完這篇文章後你將會了解 UITableView 的繪製過程和 UITableViewCell 的複用原理。 並且我會在下一篇文章中實現一個類似 Excel 的自定義控制元件。

Chameleon

Chameleon 是一個移植 iOS 的 UIKit 框架到 Mac OS X 下的開源專案。該專案的目的在於儘可能給出 UIKit 的可替代方案,並且讓 Mac OS 的開發者儘可能的開發出類似 iOS 的 UI 介面。

UITableView的簡單使用

建立UITableView例項物件

initWithFrame: style: 方法原始碼如下:

我將需要關注的地方做了詳細的註釋,這裡我們需要關注_cachedCells, _sections, _reusableCells 這三個變數的作用。

設定資料來源

下面是 dataSrouce 的 setter 方法原始碼:

_dataSourceHas 是用於記錄該資料來源實現了哪些協議方法的結構體,該結構體原始碼如下:

記錄是否實現了某協議可以使用布林值來表示,布林變數佔用的記憶體大小一般為一個位元組,即8位元。但該結構體使用了 bitfields 用一個位元(0或1)來記錄是否實現了某協議,大大縮小了佔用的記憶體。
在設定好了資料來源後需要打一個標記,告訴NSRunLoop資料來源已經設定好了,需要在下一次迴圈中使用資料來源進行佈局。下面看看 _setNeedReload 的原始碼:

在呼叫了 setNeedsLayout 方法後,NSRunloop 會在下一次迴圈中自動呼叫 layoutSubViews 方法。

  • 檢視的內容需要重繪時可以呼叫 setNeedsDisplay 方法,該方法會設定該檢視的 displayIfNeeded 變數為 YES ,NSRunLoop 在下一次迴圈檢中測到該值為 YES 則會自動呼叫 drawRect 進行重繪。
  • 檢視的內容沒有變化,但在父檢視中位置變化了可以呼叫 setNeedsLayout,該方法會設定該檢視的 layoutIfNeeded 變數為YES,NSRunLoop 在下一次迴圈檢中測到該值為 YES 則會自動呼叫 layoutSubViews 進行重繪。
  • 更詳細的內容可參考 When is layoutSubviews called?

設定代理

下面是 delegate 的 setter 方法原始碼:

與設定資料來源一樣,這裡使用了類似的結構體來記錄代理實現了哪些協議方法。

UITableView繪製

由於在設定資料來源中呼叫了 setNeedsLayout 方法打上了需要佈局的 flag,所以會在 1/60 秒(NSRunLoop的迴圈週期)後自動呼叫 layoutSubViews。layoutSubViews 的原始碼如下:

需要注意的是由於 UITableView 是繼承於 UIScrollView,所以在 UITableView 滾動時會自動呼叫該方法,詳細內容可以參考 When is layoutSubviews called?

下面依次來看三個主要方法的實現。
_reloadDataIfNeeded 的原始碼如下

其中 _updateSectionsCashe 方法是最重要的,該方法在資料來源更新後至下一次資料來源更新期間只能呼叫一次,該方法的原始碼如下:

我在需要注意的地方加了註釋,上面方法主要是記錄每個 Cell 的高度和整個 section 的高度,並把結果同過 UITableViewSection 物件快取起來。

_layoutTableView 的原始碼實現如下:

關於 UIView 的 frame 和bounds 的區別可以參考 What’s the difference between the frame and the bounds?

這裡使用了三個容器 _cachedCells, availableCells, _reusableCells 完成了 Cell 的複用,這是 UITableView 最核心的地方。
下面一起看看三個容器在建立到滾動整個過程中所包含的元素的變化情況。
在第一次設定了資料來源呼叫該方法時,三個容器的內容都為空,在呼叫完該方法後 _cachedCells 包含了當前所有可視 Cell 與其對應的indexPath 的鍵值對,availableCells 與 _reusableCells 仍然為空。只有在滾動起來後 _reusableCells 中才會出現多餘的未顯示可複用的 Cell。

  • 剛建立 UITableView 時的狀態如下圖(紅色為螢幕內容即可視區域,藍色為超出螢幕的內容,即不可視區域):
    111322498-28c8d3a2b8b8f00f
    初始狀態.png

    如圖,當前 _cachedCells 的元素為當前可視的所有 Cell 與其對應的 indexPath 的鍵值對。

  • 向上滾動一個 Cell 的過程中,由於 availableCells 為 _cachedCells 的拷貝,所以可根據 indexPath 直接取到對應的 Cell,這時從底部滾上來的第7行,由於之前的 _reusableCells 為空,所以該 Cell 是直接建立的而並非複用的,由於頂部 Cell 滾動出了可視區域,所以被加入了 _reusableCells 中以便後續滾動複用。滾動完一行後的狀態變為了 _cachedCells 包含第 2 行到第 7 行 Cell 的引用,_reusableCells 包含第一行 之前滾動出可視區域的第一行 Cell 的引用。
    121322498-e02c571c469a729d
    向上滾動1個Cell.png
  • 當向上滾動兩個 Cell 的過程中,同理第 3 行到第 7 行的 Cell 可以通過對應的 indexPath 從 _cachedCells 中獲取。這時 _reusableCells 中正好有一個可以複用的 Cell 用來從底部滾動上來的第 8 行。滾動出頂部的第 2 行 Cell 被加入 _reusableCells 中。
    1322498-05fb712b0dfa16ba
    向上滾動2個Cell.png

總結

到此你已經瞭解了 UITableView 的 Cell 的複用原理,可以根據需要定製出更復雜的控制元件。

相關文章