不管是系統自帶的還是自定義的UITableViewCell,對於它們合理的使用都是決定一個UITableView的性能的關鍵因素。應該確保以下三條:
-
UITableViewCell的重復利用:首先對象的創建,尤其是UI控件的創建,會帶來性能損耗。假設在一個很短的時間內重復分配內存,比如用戶滾動一個TableView的時候,如果我們可以重復利用一些之前創建的cell,而不是再次創建新的對象,這將顯著提升UITableView的性能。
-
避免對內容重新布局:當使用一個自定義的Cell時,避免在UITableView請求顯示cell時針對cell的subViews重新布局(重新計算子控件的位置),要做到一次創建重復使用。
-
使用不透明的subViews:當自定義自己的 cell時,讓cell的subviews不透明。
同樣的,針對UITableView的每一個Section,我們同樣需要遵循上面的原則來對它的Header和Footer來重用。這兩個View都是UITableViewHeaderFooterView類型。
下面我們討論一下普通模式與緩存模式(可能不太恰當)的代碼組織上的不同。
一般情況下我們可以像如下代碼一樣為指定的NSIndexPath創建一個UITableViewCell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil]; /* ...可以在此處設置 cell的相關屬性或者是子控件的信息 */ return cell; }
我們都知道UITableView顯示時僅僅會為可見部分創建UITableViewCell以及UITableViewHeaderFooterView控件,當用戶滾動 UITableView而導致其顯示下方或上方的Section時,它都會重繪當前區域,那么意味着每一次的滑動操作可能都導致調用cellForRowAtIndexPath方法,從上面的代碼來看,每次都會創建新的cell,當用戶快速滑動時,這個創建過程尤其頻繁,這或許是某些應用滾動時卡頓的原因之一。
當用戶向上滾動並隱藏了之前創建的Section,然后再次向下滾動顯示這些section時,系統會再次創建,這就意味着,之前創建的UITableViewCell或UITableViewHeaderFooterView沒有被系統很好的利用起來,這些控件有無當做垃圾回收還有待驗證。
我們知道創建一個控件的代價是非常大的,系統出於這方面考慮,提供了緩存機制,在 UITableView中維持了一個隊列,用於緩存之前創建過的CellView或HeaderFooterView,緩存項都有一個唯一的標識Identifier來表示。用戶可以通過標識從緩存出取得對應的控件。
既然這樣,就有了下面的改進方案:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // static修飾局部變量:可以保證局部變量只分配一次存儲空間(只初始化一次) static NSString *ID = @"XXXX"; // 1.通過一個標識去緩存池中尋找可循環利用的cell UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; // 2.如果沒有可循環利用的cell,再行分配 if (cell == nil){ cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID]; } /* ...可以在此處設置 cell的相關屬性或者是子控件的信息。 */ return cell; }
使用上面的代碼可以重復利用已有的控件。看起來都很美好,但是其實這里存在重復判斷,[tableView dequeueReusableCellWithIdentifier:ID]是一個很智能的方法,方法內部首先從緩存中相應標識的控件,在返回前系統會執行一次判斷是否nil的操作,如果為nil,系統嘗試為你創建一個。這個時候系統會做如下操作。
// 1.通過一個標識去緩存池中尋找可循環利用的cell UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; // 2.如果沒有可循環利用的cell,調用初始化方法 if (cell == nil){ cell = [[UITableViewCell alloc] initWithReuseIdentifier:ID]; }
既然系統有了上面的邏輯,那么我們只需要覆寫 super類的initWithReuseIdentifier:方法即可,例如如下代碼片段:
- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier{ if (self = [super initWithReuseIdentifier:reuseIdentifier]) { UIButton *button = [UIButton buttonWithType:UIButtonTypeContactAdd]; [self.contentView addSubview:button]; self.button = button; [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside]; } return self; }
這也就意味着,代碼現在可以簡化,下面是某個小例子中的代碼片段:(這里引用的是UITableViewHeaderFooterView的代碼,原理其實一樣。)
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{ FriendGroupHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"FriendGroupHeaderView"]; headerView.model = self.friendGroups[section]; return headerView; }
但是系統如何根據一個標識就知道創建哪一個控件呢,這就需要我們個告知系統在從緩存中讀取失敗后,如何創建這些控件,以下為截取的UITableView頭文件的相關方法:
// Beginning in iOS 6, clients can register a nib or class for each cell. // If all reuse identifiers are registered, use the newer -dequeueReusableCellWithIdentifier:forIndexPath: to guarantee that a cell instance is returned. // Instances returned from the new dequeue method will also be properly sized when they are returned. - (void)registerNib:(UINib *)nib forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0); - (void)registerClass:(Class)cellClass forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0); - (void)registerNib:(UINib *)nib forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0); - (void)registerClass:(Class)aClass forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);
系統提供了兩種創建指定標識控件的方法。
1、對象類:
通過對象類創建的方法會自動調用 initWithReuseIdentifier:方法,這種模式一般用於自定義控件的子控件位置不固定或者是子控件不固定的場景,例如QQ聊天消息,每一行消息的高度是不一致的,針對是否會員還有可能選擇是否創建皇冠圖標。
2、Nib/Xib:
這個和通過Xib方法創建控件方式一樣。適用於子控件固定的場景。
我們需要主動調用這些方法中的一個來告知系統,代碼片段如下:
- (void)viewDidLoad { [super viewDidLoad];
// 注冊指定標示的控件創建方式 [self.tableView registerClass:NSClassFromString(@"FriendGroupHeaderView") forHeaderFooterViewReuseIdentifier:@"FriendGroupHeaderView"]; }