深入源码 UITableView 复用技术原理分析
在現(xiàn)在很多公司的 app 中,許多展示頁面為了多條數(shù)據(jù)內(nèi)容,而采用 UITableView 來設(shè)計(jì)頁面。在滑動(dòng) UITableView 的時(shí)候,并不會(huì)因?yàn)閿?shù)據(jù)量大而產(chǎn)生卡頓的情況,這正是因?yàn)槠?strong>復(fù)用機(jī)制的特點(diǎn)。但是其復(fù)用機(jī)制是如何實(shí)現(xiàn)的?我們可以一起來看看
Chameleon
Chameleon用于將 iOS 的功能遷移到macOS上 并且在其中為 macOS 實(shí)現(xiàn)了一套與 iOS UIKit 同名的框架 并且其代碼為開源. 所以我們可以來研究一下思路
首先 下載Chameleon 然后打開 UIKit 項(xiàng)目
UITableView 的初始化方法
當(dāng)我們定義一個(gè) UITableView 對(duì)象的時(shí)候,需要對(duì)這個(gè)對(duì)象進(jìn)行初始化。最常用的方法莫過于 - (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)theStyle。下面跟著這個(gè)初始化入口,逐漸來分析代碼:
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)theStyle {if ((self=[super initWithFrame:frame])) {// 確定 style_style = theStyle;// cell 緩存字典_cachedCells = [[NSMutableDictionary alloc] init];// section 緩存數(shù)組_sections = [[NSMutableArray alloc] init];// 復(fù)用 cell 可變集合_reusableCells = [[NSMutableSet alloc] init];// 基本屬性設(shè)置self.separatorColor = [UIColor colorWithRed:.88f green:.88f blue:.88f alpha:1];self.separatorStyle = UITableViewCellSeparatorStyleSingleLine;self.showsHorizontalScrollIndicator = NO;self.allowsSelection = YES;self.allowsSelectionDuringEditing = NO;self.sectionHeaderHeight = self.sectionFooterHeight = 22;self.alwaysBounceVertical = YES;if (_style == UITableViewStylePlain) {self.backgroundColor = [UIColor whiteColor];}// 加入 Layout 標(biāo)記,進(jìn)行手動(dòng)觸發(fā)布局設(shè)置[self _setNeedsReload];}return self; }復(fù)制代碼在初始化代碼中就看到了重點(diǎn),_cachedCells、_sections 和 _reusableCells 無疑是復(fù)用的核心成員。
來繼續(xù)代碼跟蹤
我們先來查看一下 _setNeedsReload 方法中做了什么:
- (void)_setNeedsReload {_needsReload = YES;[self setNeedsLayout]; } 復(fù)制代碼首先先對(duì)?_needsReload?進(jìn)行標(biāo)記,之后調(diào)用了?setNeedsLayout?方法。對(duì)于?UIView的?setNeedsLayout?方法,在調(diào)用后?Runloop?會(huì)在即將到來的周期中來檢測?displayIfNeeded?標(biāo)記,如果為?YES?則會(huì)進(jìn)行?drawRect ?視圖重繪。作為 Apple?UIKit層中的基礎(chǔ) Class,在屬性變化后都會(huì)進(jìn)行一次視圖重繪的過程。這個(gè)屬性過程的變化即為對(duì)象的初始化加載以及手勢(shì)交互過程。這也就是官方文檔中的?The Runtime Interaction Model。
當(dāng) Runloop 到來時(shí),開始重繪過程即調(diào)用 layoutSubViews 方法。在 UITableView 中這個(gè)方法已經(jīng)被重寫過:
- (void)layoutSubviews {// 會(huì)在初始化的末尾手動(dòng)調(diào)用重繪過程// 并且 UITableView 是 UIScrollView 的繼承,會(huì)接受手勢(shì)// 所以在滑動(dòng) UITableView 的時(shí)候也會(huì)調(diào)用_backgroundView.frame = self.bounds;// 根據(jù)標(biāo)記確定是否執(zhí)行數(shù)據(jù)更新操作[self _reloadDataIfNeeded];[self _layoutTableView];[super layoutSubviews]; } 復(fù)制代碼接下來我們開始查看 _reloadDataIfNeeded 以及 reloadData 方法:
- (void)_reloadDataIfNeeded {// 查詢 _needsReload 標(biāo)記if (_needsReload) {[self reloadData];} }- (void)reloadData {// 清除之前的緩存并刪除 Cell[[_cachedCells allValues] makeObjectsPerformSelector:@selector(removeFromSuperview)];[_cachedCells removeAllObjects];// 復(fù)用 Cell Set 也進(jìn)行刪除操作[_reusableCells makeObjectsPerformSelector:@selector(removeFromSuperview)];[_reusableCells removeAllObjects];// 刪除選擇的 Cell_selectedRow = nil;// 刪除被高亮的 Cell_highlightedRow = nil;// 更新緩存中狀態(tài)[self _updateSectionsCache];// 設(shè)置 Size[self _setContentSize];_needsReload = NO; } 復(fù)制代碼當(dāng) reloadData 方法被觸發(fā)時(shí),UITableView 默認(rèn)為在這個(gè) UITableView 中的數(shù)據(jù)將會(huì)全部發(fā)生變化。測試之前遺留下的緩存列表以及復(fù)用列表全部都喪失了利用性。為了避免出現(xiàn)懸掛指針的情況(有可能某個(gè) cell 被其他的視圖進(jìn)行了引用),我們需要對(duì)每個(gè) cell 進(jìn)行 removeFromSuperview 處理,這個(gè)處理即針對(duì)于容器 UITableView,又對(duì)其他的引用做出保障。然后我們更新當(dāng)前 tableView 中的兩個(gè)緩存容器,_reusableCells 和 _cachedCells,以及其他需要重置的成員屬性。
最關(guān)鍵的地方到了,緩存狀態(tài)的更新方法 _updateSectionsCache,其中涉及到數(shù)據(jù)如何存儲(chǔ)、如何復(fù)用的操作
- (void)_updateSectionsCache {// 使用 dataSource 來創(chuàng)建緩存容器// 如果沒有 dataSource 則放棄重用操作// 在這個(gè)逆向工程中并沒有對(duì) header 進(jìn)行緩存操作,但是 Apple 的 UIKit 中一定也做到了// 真正的 UIKit 中應(yīng)該會(huì)獲取更多的數(shù)據(jù)進(jìn)行存儲(chǔ),并實(shí)現(xiàn)了 TableView 中所有視圖的復(fù)用// 先移除每個(gè) Section 的 Header 和 Footer 視圖for (UITableViewSection *previousSectionRecord in _sections) {[previousSectionRecord.headerView removeFromSuperview];[previousSectionRecord.footerView removeFromSuperview];}// 清除舊緩存,對(duì)容器進(jìn)行初始化操作[_sections removeAllObjects];if (_dataSource) {// 根據(jù) dataSource 計(jì)算高度和偏移量const CGFloat defaultRowHeight = _rowHeight ?: _UITableViewDefaultRowHeight;// 獲取 Section 數(shù)目const NSInteger numberOfSections = [self numberOfSections];for (NSInteger section=0; section<numberOfSections; section++) {const NSInteger numberOfRowsInSection = [self numberOfRowsInSection:section];UITableViewSection *sectionRecord = [[UITableViewSection alloc] init];sectionRecord.headerTitle = _dataSourceHas.titleForHeaderInSection? [self.dataSource tableView:self titleForHeaderInSection:section] : nil;sectionRecord.footerTitle = _dataSourceHas.titleForFooterInSection? [self.dataSource tableView:self titleForFooterInSection:section] : nil;sectionRecord.headerHeight = _delegateHas.heightForHeaderInSection? [self.delegate tableView:self heightForHeaderInSection:section] : _sectionHeaderHeight;sectionRecord.footerHeight = _delegateHas.heightForFooterInSection ? [self.delegate tableView:self heightForFooterInSection:section] : _sectionFooterHeight;sectionRecord.headerView = (sectionRecord.headerHeight > 0 && _delegateHas.viewForHeaderInSection)? [self.delegate tableView:self viewForHeaderInSection:section] : nil;sectionRecord.footerView = (sectionRecord.footerHeight > 0 && _delegateHas.viewForFooterInSection)? [self.delegate tableView:self viewForFooterInSection:section] : nil;// 先初始化一個(gè)默認(rèn)的 headerView ,如果沒有直接設(shè)置 headerView 就直接更換標(biāo)題if (!sectionRecord.headerView && sectionRecord.headerHeight > 0 && sectionRecord.headerTitle) {sectionRecord.headerView = [UITableViewSectionLabel sectionLabelWithTitle:sectionRecord.headerTitle];}// Footer 也做相同的處理if (!sectionRecord.footerView && sectionRecord.footerHeight > 0 && sectionRecord.footerTitle) {sectionRecord.footerView = [UITableViewSectionLabel sectionLabelWithTitle:sectionRecord.footerTitle];}if (sectionRecord.headerView) {[self addSubview:sectionRecord.headerView];} else {sectionRecord.headerHeight = 0;}if (sectionRecord.footerView) {[self addSubview:sectionRecord.footerView];} else {sectionRecord.footerHeight = 0;}// 為高度數(shù)組動(dòng)態(tài)開辟空間CGFloat *rowHeights = malloc(numberOfRowsInSection * sizeof(CGFloat));// 初始化總高度CGFloat totalRowsHeight = 0;for (NSInteger row=0; row<numberOfRowsInSection; row++) {// 獲取 Cell 高度,未設(shè)置則使用默認(rèn)高度const CGFloat rowHeight = _delegateHas.heightForRowAtIndexPath? [self.delegate tableView:self heightForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section]] : defaultRowHeight;// 記錄高度rowHeights[row] = rowHeight;// 總高度統(tǒng)計(jì)totalRowsHeight += rowHeight;}sectionRecord.rowsHeight = totalRowsHeight;[sectionRecord setNumberOfRows:numberOfRowsInSection withHeights:rowHeights];free(rowHeights);// 緩存高度記錄[_sections addObject:sectionRecord];}} } 復(fù)制代碼我們發(fā)現(xiàn)在 _updateSectionsCache 更新緩存狀態(tài)的過程中對(duì) _sections 中的數(shù)據(jù)全部清除。之后緩存了更新后的所有 Section 數(shù)據(jù)。那么這些數(shù)據(jù)有什么利用價(jià)值呢?繼續(xù)來看布局更新操作。
- (void)_layoutTableView {// 在需要渲染時(shí)放置需要的 Header 和 Cell// 緩存所有出現(xiàn)的單元格,并添加至復(fù)用容器// 之后那些不顯示但是已經(jīng)出現(xiàn)的 Cell 將會(huì)被復(fù)用// 獲取容器視圖相對(duì)于父類視圖的尺寸及坐標(biāo)const CGSize boundsSize = self.bounds.size;// 獲取向下滑動(dòng)偏移量const CGFloat contentOffset = self.contentOffset.y;// 獲取可視矩形框的尺寸const CGRect visibleBounds = CGRectMake(0,contentOffset,boundsSize.width,boundsSize.height);// 表高紀(jì)錄值CGFloat tableHeight = 0;// 如果有 header 則需要額外計(jì)算if (_tableHeaderView) {CGRect tableHeaderFrame = _tableHeaderView.frame;tableHeaderFrame.origin = CGPointZero;tableHeaderFrame.size.width = boundsSize.width;_tableHeaderView.frame = tableHeaderFrame;tableHeight += tableHeaderFrame.size.height;}// availableCell 記錄當(dāng)前正在顯示的 Cell// 在滑出顯示區(qū)之后將添加至 _reusableCellsNSMutableDictionary *availableCells = [_cachedCells mutableCopy];const NSInteger numberOfSections = [_sections count];[_cachedCells removeAllObjects];// 滑動(dòng)列表,更新當(dāng)前顯示容器for (NSInteger section=0; section<numberOfSections; section++) {CGRect sectionRect = [self rectForSection:section];tableHeight += sectionRect.size.height;if (CGRectIntersectsRect(sectionRect, visibleBounds)) {const CGRect headerRect = [self rectForHeaderInSection:section];const CGRect footerRect = [self rectForFooterInSection:section];UITableViewSection *sectionRecord = [_sections objectAtIndex:section];const NSInteger numberOfRows = sectionRecord.numberOfRows;if (sectionRecord.headerView) {sectionRecord.headerView.frame = headerRect;}if (sectionRecord.footerView) {sectionRecord.footerView.frame = footerRect;}for (NSInteger row=0; row<numberOfRows; row++) {// 構(gòu)造 indexPath 為代理方法準(zhǔn)備NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];// 獲取第 row 個(gè)坐標(biāo)位置CGRect rowRect = [self rectForRowAtIndexPath:indexPath];// 判斷當(dāng)前 Cell 是否與顯示區(qū)域相交if (CGRectIntersectsRect(rowRect,visibleBounds) && rowRect.size.height > 0) {// 首先查看 availableCells 中是否已經(jīng)有了當(dāng)前 Cell 的存儲(chǔ)// 如果沒有,則請(qǐng)求 tableView 的代理方法獲取 CellUITableViewCell *cell = [availableCells objectForKey:indexPath] ?: [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];// 由于碰撞檢測生效,則按照邏輯需要更新 availableCells 字典if (cell) {// 獲取到 Cell 后,將其進(jìn)行緩存操作[_cachedCells setObject:cell forKey:indexPath];[availableCells removeObjectForKey:indexPath];cell.highlighted = [_highlightedRow isEqual:indexPath];cell.selected = [_selectedRow isEqual:indexPath];cell.frame = rowRect;cell.backgroundColor = self.backgroundColor;[cell _setSeparatorStyle:_separatorStyle color:_separatorColor];[self addSubview:cell];}}}}}// 將已經(jīng)退出屏幕且定義 reuseIdentifier 的 Cell 加入可復(fù)用 Cell 容器中for (UITableViewCell *cell in [availableCells allValues]) {if (cell.reuseIdentifier) {[_reusableCells addObject:cell];} else {[cell removeFromSuperview];}}// 不能復(fù)用的 Cell 會(huì)直接銷毀,可復(fù)用的 Cell 會(huì)存儲(chǔ)在 _reusableCells// 確保所有的可用(未出現(xiàn)在屏幕上)的復(fù)用單元格在 availableCells 中// 這樣緩存的目的之一是確保動(dòng)畫的流暢性。在動(dòng)畫的幀上都會(huì)對(duì)顯示部分進(jìn)行處理,重新計(jì)算可見 Cell。// 如果直接刪除掉所有未出現(xiàn)在屏幕上的單元格,在視覺上會(huì)觀察到突然消失的動(dòng)作// 整體動(dòng)畫具有跳躍性而顯得不流暢// 把在可視區(qū)的 Cell(但不在屏幕上)已經(jīng)被回收為可復(fù)用的 Cell 從視圖中移除NSArray* allCachedCells = [_cachedCells allValues];for (UITableViewCell *cell in _reusableCells) {if (CGRectIntersectsRect(cell.frame,visibleBounds) && ![allCachedCells containsObject: cell]) {[cell removeFromSuperview];}}if (_tableFooterView) {CGRect tableFooterFrame = _tableFooterView.frame;tableFooterFrame.origin = CGPointMake(0,tableHeight);tableFooterFrame.size.width = boundsSize.width;_tableFooterView.frame = tableFooterFrame;} } 復(fù)制代碼如果你已經(jīng)對(duì) UITableView 的緩存機(jī)制有所了解,那么你在閱讀完代碼之后會(huì)對(duì)其有更深刻的認(rèn)識(shí)。如果看完代碼還是一頭霧水,那么請(qǐng)繼續(xù)看下面的分析。
Cell 復(fù)用 的三個(gè)階段
1 布局方法觸發(fā)階段
在用戶觸摸屏幕后,硬件報(bào)告觸摸時(shí)間傳遞至 UIKit 框架,之后 UIKit 將觸摸事件打包成 UIEvent 對(duì)象,分發(fā)至指定視圖。這時(shí)候其視圖就會(huì)做出相應(yīng),并調(diào)用 setNeedsLayout 方法告訴視圖及其子視圖需要進(jìn)行布局更新。此時(shí),setNeedsLayout 被調(diào)用,也就變?yōu)?Cell 復(fù)用場景的入口。
2緩存 Cell 高度信息階段
當(dāng)視圖加載后,由 UIKit 調(diào)用布局方法 layoutSubviews 從而進(jìn)入緩存 Cell 高度階段 _updateSectionsCache。在這個(gè)階段,通過代理方法 heightForRowAtIndexPath: 獲取每一個(gè) Cell 的高度,并將高度信息緩存起來。這其中的高度信息由 UITableViewSection 的一個(gè)實(shí)例 sectionRecord 進(jìn)行存儲(chǔ),其中以 section 為單位,存儲(chǔ)每個(gè) section 中各個(gè) Cell 的高度、Cell 的數(shù)量、以及 section 的總高度、footer 和 header 高度這些信息。這一部分的信息采集是為了在 Cell 復(fù)用的核心部分,Cell 的 Rect 尺寸與 tableView 尺寸計(jì)算邊界情況建立數(shù)據(jù)基礎(chǔ)。
3 復(fù)用 Cell 的核心處理階段
我們要關(guān)注三個(gè)存儲(chǔ)容器的變化情況:
- NSMutableDictionary 類型 _cachedCells:用來存儲(chǔ)當(dāng)前屏幕上所有 Cell 與其對(duì)應(yīng)的 indexPath。以鍵值對(duì)的關(guān)系進(jìn)行存儲(chǔ)。
- NSMutableDictionary 類型 availableCells:當(dāng)列表發(fā)生滑動(dòng)的時(shí)候,部分 Cell 從屏幕移出,這個(gè)容器會(huì)對(duì) _cachedCells 進(jìn)行拷貝,然后將屏幕上此時(shí)的 Cell 全部去除。即最終取出所有退出屏幕的 Cell。
- NSMutableSet 類型 _reusableCells:用來收集曾經(jīng)出現(xiàn)過此時(shí)未出現(xiàn)在屏幕上的 Cell。當(dāng)再出滑入主屏幕時(shí),則直接使用其中的對(duì)象根據(jù) CGRectIntersectsRect Rect 碰撞試驗(yàn)進(jìn)行復(fù)用。
在整個(gè)核心復(fù)用階段,這三個(gè)容器都充當(dāng)著很重要的角色。我們給出以下的場景實(shí)例,例如下圖的一個(gè)場景,圖 ① 為頁面剛剛載入的階段,圖 ② 為用戶向下滑動(dòng)一個(gè)單元格時(shí)的狀態(tài):
當(dāng)?shù)綘顟B(tài) ② 的時(shí)候,我們發(fā)現(xiàn) _reusableCells 容器中,已經(jīng)出現(xiàn)了狀態(tài) ① 中已經(jīng)退出屏幕的 Cell 0。而當(dāng)我們重新將 Cell 0 滑入界面的時(shí)候,在系統(tǒng) addView 渲染階段,會(huì)直接將 _reusableCells 中的 Cell 0 立即取出進(jìn)行渲染,從而代替創(chuàng)建新的實(shí)例再進(jìn)行渲染,簡化了時(shí)間與性能上的開銷。
UITableView 的其他細(xì)節(jié)優(yōu)化
復(fù)用容器數(shù)據(jù)類型 NSMutableSet
在三個(gè)重要的容器中,只有 _reusableCells 使用了 NSMutableSet。這是因?yàn)槲覀冊(cè)诿恳淮螌?duì)于 _cachedCells 中的 Cell 進(jìn)行遍歷并在屏幕上渲染時(shí),都需要在 _reusableCells 進(jìn)行一次掃描。而且當(dāng)一個(gè)頁面反復(fù)的上下滑動(dòng)時(shí),_reusableCells 的檢索復(fù)雜度是相當(dāng)龐大的。為了確保這一情況下滑動(dòng)的流暢性,Apple 在設(shè)計(jì)時(shí)不得不將檢索復(fù)雜度最小化。并且這個(gè)復(fù)雜度要是非抖動(dòng)的,不能給體驗(yàn)造成太大的不穩(wěn)定性。
高度緩存容器 _sections
在每次布局方法觸發(fā)階段,由于 Cell 的狀態(tài)發(fā)生了變化。在對(duì) Cell 復(fù)用容器的修改之前,首先要做的一件事是以 Section 為單位對(duì)所有的 Cell 進(jìn)行緩存高度。從這里可以看出 UITableView 設(shè)計(jì)師的細(xì)節(jié)。 Cell 的高度在 UITableView 中充當(dāng)著十分重要的角色,一下列表是需要使用高度的方法:
- (CGFloat)_offsetForSection:(NSInteger)index:計(jì)算指定 Cell 的滑動(dòng)偏移量。
- (CGRect)rectForSection:(NSInteger)section:返回某個(gè) Section 的整體 Rect。
- (CGRect)rectForHeaderInSection:(NSInteger)section:返回某個(gè) Header 的 Rect。
- (CGRect)rectForFooterInSection:(NSInteger)section:返回某個(gè) Footer 的 Rect。
- (CGRect)rectForRowAtIndexPath:(NSIndexPath *)indexPath:返回某個(gè) Cell 的 Rect。
- (NSArray *)indexPathsForRowsInRect:(CGRect)rect:返回 Rect 列表。
- (void)_setContentSize:根據(jù)高度計(jì)算 UITableView 中實(shí)際內(nèi)容的 Size。 一次
下一個(gè)目標(biāo) 研究 FDTemplateLayoutCell 的優(yōu)化方案
參考
- Chameleon
- 《iOS 成長之路》
總結(jié)
以上是生活随笔為你收集整理的深入源码 UITableView 复用技术原理分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于Unity中的UGUI优化,你可能遇
- 下一篇: 关于Kafka 的 consumer 消