前言:
對于[UITableView visibleCells]的正確獲取,相信很多人,都會采用切換主隊列的方式,來保證visibleCells數(shù)據(jù)的正確。究其原因,和RealoadData方法的異步性和RunLoop機制有關(guān)。
那么在當(dāng)前以iOS8+環(huán)境下,切換主隊列的方式,是否還生效呢?請大家隨本文一起探究下去。
[self.tableView reloadData];
// 切換到主隊列
dispatch_async(dispatch_get_main_queue(), ^{
NSArray *array = [self.tableView visibleCells];
NSLog(@"visibleCells====:%@",@(array.count));
});
UITableView
首先確認ReloadData的異步性。執(zhí)行測試代碼:
- (void)testTableView {
[self.tableView reloadData];
NSLog(@"reloadData 執(zhí)行完成");
}
監(jiān)聽reloadData執(zhí)行前后,并同時監(jiān)聽RunLoop時機:
16:45:08 被喚醒
16:45:08 即將處理Timer事件
16:45:08 即將處理Source事件
16:45:08 -[ViewController btn:]
16:32:54 -[TestTableView reloadData] -----
16:45:08 -[ViewController numberOfSectionsInTableView:]
16:32:54 -[TestTableView setNeedsLayout] -----
16:32:54 -[TestTableView setNeedsLayout] =====
16:45:08 -[ViewController tableView:numberOfRowsInSection:]
16:32:54 -[TestTableView setNeedsLayout] -----
16:32:54 -[TestTableView setNeedsLayout] =====
16:32:54 -[TestTableView setNeedsLayout] -----
16:32:54 -[TestTableView setNeedsLayout] =====
16:32:54 -[TestTableView setNeedsLayout] -----
16:32:54 -[TestTableView setNeedsLayout] =====
16:32:54 -[TestTableView reloadData] =====
16:45:08 reloadData 執(zhí)行完成
16:45:08 即將處理Timer事件
16:45:08 即將處理Source事件
16:45:08 即將休眠
16:32:54 -[TestTableView layoutSubviews] -----
16:45:08 -[ViewController tableView:cellForRowAtIndexPath:]=====
16:45:08 -[ViewController tableView:heightForRowAtIndexPath:]
16:45:08 -[ViewController tableView:heightForRowAtIndexPath:]
16:32:54 -[TestTableView layoutSubviews] =====
16:45:08 被喚醒
16:45:08 即將處理Timer事件
16:45:08 即將處理Source事件
16:45:08 即將休眠
分析日志:
reloadData 方法內(nèi)部,執(zhí)行了高度計算,但沒有進行Cell的渲染工作。而是調(diào)用了setNeedsLayout,并將在下一個RunLoop處理時機,調(diào)用[UITableView layoutSubviews]方法對cell們進行渲染。
由此可見,reloadData方法執(zhí)行完成后,cells 并沒有被渲染,此時立即調(diào)用visibleCells方法會獲取到數(shù)據(jù)的數(shù)據(jù)將是錯誤的。
但真實的情況呢?
測試visibleCells方法如下:
- (void)testTableView {
[self.tableView reloadData];
NSLog(@"reloadData 執(zhí)行完成");
NSArray *array = [self.tableView visibleCells];
NSLog(@"visibleCells----:%@",@(array.count));
dispatch_async(dispatch_get_main_queue(), ^{
NSArray *array = [self.tableView visibleCells];
NSLog(@"visibleCells==== :%@",@(array.count));
});
}
同時監(jiān)聽runloop時機,日志如下:
13:00:32 即將處理Timer事件
13:00:32 即將處理Source事件
13:00:32 -[ViewController btn:]
13:00:32 -[TestTableView reloadData] -----
13:00:32 -[ViewController numberOfSectionsInTableView:]
13:00:32 -[ViewController tableView:numberOfRowsInSection:]
13:00:32 -[ViewController tableView:heightForRowAtIndexPath:]
13:00:32 -[ViewController tableView:heightForRowAtIndexPath:]
13:00:32 -[TestTableView reloadData] =====
13:00:32 reloadData 執(zhí)行完成
13:00:32 -[ViewController tableView:cellForRowAtIndexPath:]
13:00:32 -[ViewController tableView:heightForRowAtIndexPath:]
13:00:32 -[ViewController tableView:cellForRowAtIndexPath:]
13:00:32 -[ViewController tableView:heightForRowAtIndexPath:]
13:00:32 visibleCells----:2
13:00:32 visibleCells====:2
13:00:32 即將處理Timer事件
13:00:32 即將處理Source事件
16:30:14 即將休眠
15:03:46 -[TestTableView layoutSubviews] -----
15:03:46 -[TestTableView layoutSubviews] =====
16:30:14 被喚醒
兩次visibleCells方法都返回了正確的cells數(shù)據(jù)。這是為何?進一步查看visibleCells前后調(diào)用堆棧信息:

-[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:]
分析上述日志:
原來如此,調(diào)用[UITableView visibleCells]方法時,會促使cell進行渲染。
小結(jié)
- reloadData 方法內(nèi)部分為兩部分:
- 計算了contenSize、contentOffset相關(guān)的內(nèi)容。
- 設(shè)置當(dāng)前TableView的子視圖需要修改渲染和布局。這些任務(wù)將放在后續(xù)主隊列中。
- 在下一次RunLoop時機,執(zhí)行l(wèi)ayoutSubviews方法。對TableView的子視圖進行渲染和布局。
- visibleCells 方法內(nèi)部,會執(zhí)行createPreparedCell方法,使cell內(nèi)容提前渲染。并在渲染過后,返回正確的Cells內(nèi)容。
因此,UITableView之visibleCells,如今(iOS8+),可以直接調(diào)用,不需要再擔(dān)心,數(shù)據(jù)不正確了!
UICollectionView
確認了[UITableView visibleCells]的機制之后,忍不住聯(lián)想到UICollectionView是不是也這樣呢。兩者有很多的相似,同樣繼承自UIScrollView,類似的協(xié)議等等,那么直接調(diào)用visibleCells是不是也可以了?
Don't talk(bb),show you the code!
測試代碼如下:
- (void)testCollectionView {
[self.collectionView reloadData];
NSLog(@"[self.collectionView reloadData];");
NSLog(@"visibleCells---%@",@([self.collectionView visibleCells].count));
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"visibleCells===%@",@([self.collectionView visibleCells].count));
});
}
16:13:56 被喚醒
16:13:56 即將處理Timer事件
16:13:56 即將處理Source事件
16:13:56 -[ViewController btn:]
16:15:10 -[TestCollectionView reloadData] -----
16:15:10 -[TestCollectionView reloadData] =====
16:13:56 reloadData 執(zhí)行完成
16:13:56 visibleCells---0
16:13:56 visibleCells===0
16:13:56 即將處理Timer事件
16:13:56 即將處理Source事件
16:13:56 即將休眠
16:15:10 -[TestCollectionView layoutSubviews] -----
16:15:10 layoutSubviews - visibleCells:3
16:15:10 -[TestCollectionView layoutSubviews] =====
16:13:56 被喚醒
額,兩次獲取visibleCells數(shù)據(jù)都是0,兩種方式都錯了,這讓人情何以堪!
注意到,下面代碼中,visibleCells 數(shù)據(jù)獲取正確了。
16:15:10 -[TestCollectionView layoutSubviews] -----
16:15:10 layoutSubviews - visibleCells:3
16:15:10 -[TestCollectionView layoutSubviews] =====
這是因為collectionView 和 tableView 在visibleCells內(nèi)部有所不同。collectionView.visibleCells 方法執(zhí)行時,沒有立即觸發(fā)layout相關(guān)事件。
因此,visibleCells 獲取失敗。需要在[TestCollectionView layoutSubviews]執(zhí)行后才獲取才能成功。
進一步打印 cellforRow方法的調(diào)用棧如下

總歸是和UITableView有了相似,創(chuàng)建Cell視圖前,也執(zhí)行前綴為createPreparedCell的方法
-[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:]
小結(jié)
也就是說,想要立即獲取正確的visibleCells,需要主動觸發(fā)layoutSubviews后,才進行獲取。
- reloadData 方法內(nèi)部分為兩部分:
- 計算了contenSize、contentOffset相關(guān)的內(nèi)容。
- 設(shè)置當(dāng)前TableView的子視圖需要修改渲染和布局。這些任務(wù)將放在后續(xù)主隊列中。
- 在下一次RunLoop時機,執(zhí)行l(wèi)ayoutSubviews方法。對UICollectionView的子視圖進行渲染和布局。
- visibleCells 方法內(nèi)部,不會執(zhí)行createPreparedCell方法,及無法確保cells被正確獲取。
- 如果需要正確的獲取visibleCells,則需要確保證獲取時機在layoutSubView之后。
- 譬如,可以主動調(diào)用 [UICollectionView layoutIfNeeded]、[UICollectionView layouSubViews]等
總結(jié)
- [UITableView visibleCells]方法,內(nèi)部機制,已經(jīng)保證了獲取數(shù)據(jù)的正確性。
- [UICollectionView visibleCells]方法,想要確保數(shù)據(jù)正確性,需確保layouSubViews事件被提前執(zhí)行。
引申
如果,UICollectionView 作為一個Cell,被加載在UITableView上。此時獲取visibleCells會是怎樣的?請嘗試回答以下代碼中的問題。
[self.tableView reloadData];
NSArray *tableViewVisibleCells = [self.tableView visibleCells];
// 問題1:tableViewVisibleCells 是否正確?
for (UITableViewCell *cell in tableViewVisibleCells) {
// cell 上加載內(nèi)容的視圖,對應(yīng)可能是CollectionView
UIView *view = cell.realContentView;
if ([view isKindOfClass:[UICollectionView class]]) {
UICollectionView *collectionView = (UICollectionView *)view;
NSArray *array = [collectionView visibleCells];
// 問題2:array 獲取是否正確呢?
}
}
答案:
- 問題1:正確
- 問題2:正確
你答對了嗎?