美文网首页
UITableView、UICollectionView之vis

UITableView、UICollectionView之vis

作者: 吕木 | 来源:发表于2018-11-05 15:01 被阅读140次

    前言:

    对于[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:正确

    你答对了吗?

    资料

    相关文章

      网友评论

          本文标题:UITableView、UICollectionView之vis

          本文链接:https://www.haomeiwen.com/subject/xhjsxqtx.html