监听reloadData刷新列表完毕的时机

@(IOS零落的记忆)[runloop, GCD死锁, 多线程]html

分析:

reloadData是一个异步方法,并不会等待UITableView或者UICollectionView(后面统称listView)真正刷新完毕后才执行后续代码,而是当即执行后续代码。咱们执行reloadData的本意是刷新listView,随后会进入一系列的DataSource和Delegate回调,有些是和reloadData同步发生的,有些是异步发生的。安全

  • 同步:numberOfSectionsInCollectionViewnumberOfItemsInSection
  • 异步:cellForItemAtIndexPath
  • 同步+异步:sizeForItemAtIndexPath

问题:

因为cell复用的缘由,直接在reloadData后执行代码是有可能出问题的。好比在reloadData前保留了一个cell,在reloadData后,对这个cell(已经不是原来的cell了)进行某些操做,会出现一些异常问题。bash

解决办法:

reloadData前不是保留cell,二是保留当前cell对应的NSIndexPath,而后在reloadData完毕(listView真正刷新完毕)后经过方法cellForItemAtIndexPath:从新获取cell,而后进行相应的操做。多线程

获取listView真正刷新完毕的时机的几种方法

方法一、经过layoutIfNeeded方法,强制重绘并等待完成。
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
// 刷新完成,执行后续须要执行的代码
if ( self.didPlayIdx ) {
    MyCell* cell = (MyCell*)[self.collectionView cellForItemAtIndexPath:self.didPlayIdx];
    if (cell) {
	[cell playWithPlayer:self.player];
    }
}
复制代码
方法二、reloadData方法会在主线程执行,经过GCD,使后续操做排队在reloadData后面执行。一次runloop有两个机会执行GCD dispatch main queue中的任务,分别在休眠前和被唤醒后。设置listViewlayoutIfNeeded为YES,在即将进入休眠时执行异步任务,重绘一次界面。
[self.collectionView reloadData];  
dispatch_async(dispatch_get_main_queue(), ^{  
    // 刷新完成,执行后续代码
    if ( self.didPlayIdx ) {
        MyCell* cell = (MyCell*)[self.collectionView cellForItemAtIndexPath:self.didPlayIdx];
        if (cell) {
            [cell playWithPlayer:self.player];
        }
    }
});
复制代码

知识点关联:GCD死锁、Runloopapp

// 发生死锁,永远不会执行任务2和3
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
   NSLog(@"2");
});
NSLog(@"3");
复制代码
方法三、自定义UICollectionView、UITableView,layoutSubviews以后看成reloadData完成(复杂,但能够更好的理解方法一)
#import "MyTableView.h"

@interface MyTableView()
@property (nonatomic, copy) void (^reloadDataCompletionBlock)();
@end

@implementation MyTableView
- (void)reloadDataWithCompletion:(void (^)())completionBlock {
    self.reloadDataCompletionBlock = completionBlock;
    [super reloadData];
}
- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.reloadDataCompletionBlock) {
        self.reloadDataCompletionBlock();
        self.reloadDataCompletionBlock = nil;
    }
}
@end

// 调用的时候
[self.tableView reloadDataWithCompletion:^{
     NSLog(@"完成刷新");
}];
复制代码

引伸:更新UI放在主线程的缘由

缘由一:安全+效率

由于UIKit框架不是线程安全的,当多个线程同时操做UI的时候,抢夺资源,致使崩溃,UI异常等问题。假如在两个线程中设置了同一张背景图片,颇有可能就会因为背景图片被释放两次,使得程序崩溃。或者某一个线程中遍历找寻某个subView,然而在另外一个线程中删除了该subView,那么就会形成错乱。apple有对大部分的绘图方法和诸如UIColor等类改写成线程安全可用,可仍是建议将UI操做保证在主线程中。例如说,咱们须要在子线程中读取一个image对象,使用接口[UIImage imageNamed:],但imageNamed:实际上在iOS9之后才是线程安全的,iOS9以前都须要在主线程获取。因此,咱们须要从子线程切换到主线程获取image,而后再切回子线程拿到这个image,这里咱们必须使用sync。框架

__block UIImage *image;
dispatch_sync_on_main_queue(^{
    image = [UIImage imageNamed:@"Resource/img"];
});
attachment.image = image;

// YYKit中提供了一个同步扔任务到主线程的安全方法:
/**
 Submits a block for execution on a main queue and waits until the block completes.
*/
static inline void dispatch_sync_on_main_queue(void (^block)()) {
    if (pthread_main_np()) {
        block();
    } else {
        dispatch_sync(dispatch_get_main_queue(), block);
    }
}
复制代码

缘由二:用户体验

iOS中只有主线程才能当即刷新UI。在子线程中是不可以更新UI,咱们看到的子线程可以更新UI的缘由是,等到子线程执行完毕,自动进入了主线程去执行子线程中更新UI的代码。因为子线程执行时间很是短暂,让咱们误觉得子线程能够更新UI。若是子线程一直在运行,则没法更新UI,由于没有办法进入主线程。异步

参考博客:

iOS 事件处理机制与图像渲染过程async

为何都要在主线程中更新UI(iOS开发)oop

IOS为何在主线程刷新UI?(子线程刷新UI测试)测试

更新UI放在主线程的缘由

如何安全使用dispatch_sync

相关文章
相关标签/搜索