監聽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

相關文章
相關標籤/搜索