1. 程式人生 > >監聽reloadData重新整理列表完畢的時機

監聽reloadData重新整理列表完畢的時機

@(IOS零落的記憶)[runloop, GCD死鎖, 多執行緒]

分析:

reloadData是一個非同步方法,並不會等待UITableView或者UICollectionView(後面統稱listView)真正重新整理完畢後才執行後續程式碼,而是立即執行後續程式碼。我們執行reloadData的本意是重新整理listView,隨後會進入一系列的DataSource和Delegate回撥,有些是和reloadData同步發生的,有些是非同步發生的。

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

問題:

由於cell複用的原因,直接在reloadData後執行程式碼是有可能出問題的。比如在reloadData前保留了一個cell,在reloadData後,對這個cell(已經不是原來的cell了)進行某些操作,會出現一些異常問題。

解決辦法:

reloadData前不是保留cell,二是保留當前cell對應的NSIndexPath,然後在reloadData完畢(listView真正重新整理完畢)後通過方法cellForItemAtIndexPath:重新獲取cell,然後進行相應的操作。

獲取listView真正重新整理完畢的時機的幾種方法

方法1、通過layoutIfNeeded方法,強制重繪並等待完成。
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
// 重新整理完成,執行後續需要執行的程式碼
if ( self.didPlayIdx ) {
    MyCell* cell = (MyCell*)[self.collectionView cellForItemAtIndexPath:self.didPlayIdx];
    if (cell) {
	[cell playWithPlayer:self.player];
    }
}
複製程式碼
方法2、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死鎖、Runloop

// 發生死鎖,永遠不會執行任務2和3
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
   NSLog(@"2");
});
NSLog(@"3");
複製程式碼
方法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 事件處理機制與影象渲染過程

為什麼都要在主執行緒中更新UI(iOS開發)

IOS為什麼在主執行緒重新整理UI?(子執行緒重新整理UI測試)

更新UI放在主執行緒的原因

如何安全使用dispatch_sync