UITableView、UICollectionView之visibleCells方法的前世今生

前言:

对于[UITableView visibleCells]的正确获取,相信很多人,都会采用切换主队列的方式,来保证visibleCells数据的正确。究其原因,和RealoadData方法的异步性和RunLoop机制有关。

那么在当前以iOS8+环境下,切换主队列的方式,是否还生效呢?请大家随本文一起探究下去。

[self.tableView reloadData];
// 切换到主队列
dispatch_async(dispatch_get_main_queue(), ^{
    NSArray *array = [self.tableView visibleCells];
    NSLog(@"visibleCells====:%@",@(array.count));
});

UITableView

首先确认ReloadData的异步性。执行测试代码:

- (void)testTableView {
    [self.tableView reloadData];
    NSLog(@"reloadData 执行完成");
}

监听reloadData执行前后,并同时监听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 执行完成
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 方法内部,执行了高度计算,但没有进行Cell的渲染工作。而是调用了setNeedsLayout,并将在下一个RunLoop处理时机,调用[UITableView layoutSubviews]方法对cell们进行渲染。

由此可见,reloadData方法执行完成后,cells 并没有被渲染,此时立即调用visibleCells方法会获取到数据的数据将是错误的。

但真实的情况呢?

测试visibleCells方法如下:

- (void)testTableView {

    [self.tableView reloadData];
    NSLog(@"reloadData 执行完成");

    NSArray *array = [self.tableView visibleCells];
    NSLog(@"visibleCells----:%@",@(array.count));

    dispatch_async(dispatch_get_main_queue(), ^{
        NSArray *array = [self.tableView visibleCells];
        NSLog(@"visibleCells==== :%@",@(array.count));
    });
}

同时监听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 执行完成
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数据。这是为何?进一步查看visibleCells前后调用堆栈信息:

tableView.visibleCells.jpg
-[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:]

分析上述日志:

原来如此,调用[UITableView visibleCells]方法时,会促使cell进行渲染。

小结

  • reloadData 方法内部分为两部分:
  • 计算了contenSize、contentOffset相关的内容。
  • 设置当前TableView的子视图需要修改渲染和布局。这些任务将放在后续主队列中。
  • 在下一次RunLoop时机,执行layoutSubviews方法。对TableView的子视图进行渲染和布局。
  • visibleCells 方法内部,会执行createPreparedCell方法,使cell内容提前渲染。并在渲染过后,返回正确的Cells内容。

因此,UITableView之visibleCells,如今(iOS8+),可以直接调用,不需要再担心,数据不正确了!

UICollectionView

确认了[UITableView visibleCells]的机制之后,忍不住联想到UICollectionView是不是也这样呢。两者有很多的相似,同样继承自UIScrollView,类似的协议等等,那么直接调用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 执行完成
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数据都是0,两种方式都错了,这让人情何以堪!

注意到,下面代码中,visibleCells 数据获取正确了。

16:15:10 -[TestCollectionView layoutSubviews] -----
16:15:10 layoutSubviews - visibleCells:3
16:15:10 -[TestCollectionView layoutSubviews] =====

这是因为collectionView 和 tableView 在visibleCells内部有所不同。collectionView.visibleCells 方法执行时,没有立即触发layout相关事件。
因此,visibleCells 获取失败。需要在[TestCollectionView layoutSubviews]执行后才获取才能成功。

进一步打印 cellforRow方法的调用栈如下

UICollectionView.loadCells.png

总归是和UITableView有了相似,创建Cell视图前,也执行前缀为createPreparedCell的方法

-[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:]

小结

也就是说,想要立即获取正确的visibleCells,需要主动触发layoutSubviews后,才进行获取。

  • reloadData 方法内部分为两部分:
  • 计算了contenSize、contentOffset相关的内容。
  • 设置当前TableView的子视图需要修改渲染和布局。这些任务将放在后续主队列中。
  • 在下一次RunLoop时机,执行layoutSubviews方法。对UICollectionView的子视图进行渲染和布局。
  • visibleCells 方法内部,不会执行createPreparedCell方法,及无法确保cells被正确获取。
  • 如果需要正确的获取visibleCells,则需要确保证获取时机在layoutSubView之后。
  • 譬如,可以主动调用 [UICollectionView layoutIfNeeded]、[UICollectionView layouSubViews]等

总结

  • [UITableView visibleCells]方法,内部机制,已经保证了获取数据的正确性。
  • [UICollectionView visibleCells]方法,想要确保数据正确性,需确保layouSubViews事件被提前执行。

引申

如果,UICollectionView 作为一个Cell,被加载在UITableView上。此时获取visibleCells会是怎样的?请尝试回答以下代码中的问题。


[self.tableView reloadData];
NSArray *tableViewVisibleCells = [self.tableView visibleCells];
//  问题1:tableViewVisibleCells 是否正确?

for (UITableViewCell *cell in tableViewVisibleCells) {
// cell 上加载内容的视图,对应可能是CollectionView
    UIView *view = cell.realContentView;
    if ([view isKindOfClass:[UICollectionView class]]) {

    UICollectionView *collectionView = (UICollectionView *)view;
    NSArray *array = [collectionView visibleCells];
    // 问题2:array 获取是否正确呢?
    }
}

答案:

  • 问题1:正确
  • 问题2:正确

你答对了吗?

资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,300评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 面向对象的三大特性:封装、继承、多态 OC内存管理 _strong 引用计数器来控制对象的生命周期。 _weak...
    运气不够技术凑阅读 1,083评论 0 10
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • 《暗香》 文 / 文君 月光旖旎。 见曲廊疏影,气如兰芷。 料峭春寒,几许清芬惹人醉。 残雪西风嫉妒,又怎的、任他...
    君無情545阅读 192评论 2 8