diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index 9f83a3046c..d65a900c60 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -107,6 +107,26 @@ */ - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath; +@optional + +/** + * Indicator to lock the data source for data loading in asyn mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistence or exception + * due to the data access in async mode. + * + * @param tableView The sender. + */ +- (void)collectionViewLockDataSourceForDataUpdating:(ASCollectionView *)collectionView; + +/** + * Indicator to unlock the data source for data loading in asyn mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistence or exception + * due to the data access in async mode. + * + * @param tableView The sender. + */ +- (void)collectionViewUnlockDataSourceForDataUpdating:(ASCollectionView *)collectionView; + @end diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 36cc0e2f60..ddd5ac254e 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -103,6 +103,8 @@ static BOOL _isInterceptedSelector(SEL sel) NSMutableArray *_batchUpdateBlocks; } +@property (atomic, assign) BOOL asyncDataSourceLocked; + @end @implementation ASCollectionView @@ -131,6 +133,8 @@ static BOOL _isInterceptedSelector(SEL sel) _proxyDelegate = [[_ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; super.delegate = (id)_proxyDelegate; + _asyncDataSourceLocked = NO; + _performingBatchUpdates = NO; _batchUpdateBlocks = [NSMutableArray array]; @@ -375,6 +379,26 @@ static BOOL _isInterceptedSelector(SEL sel) } } +- (void)dataControllerLockDataSourceForDataUpdating +{ + ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked"); + + self.asyncDataSourceLocked = YES; + if ([_asyncDataSource respondsToSelector:@selector(collectionViewLockDataSourceForDataUpdating:)]) { + [_asyncDataSource collectionViewLockDataSourceForDataUpdating:self]; + } +} + +- (void)dataControllerUnlockDataSourceForDataUpdating +{ + ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has alredy been unlocked !"); + + self.asyncDataSourceLocked = NO; + if ([_asyncDataSource respondsToSelector:@selector(collectionViewUnlockDataSourceForDataUpdating:)]) { + [_asyncDataSource collectionViewUnlockDataSourceForDataUpdating:self]; + } +} + #pragma mark - #pragma mark ASRangeControllerDelegate. diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index faf1b29cc4..e163bd69d1 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -109,6 +109,26 @@ */ - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath; +@optional + +/** + * Indicator to lock the data source for data loading in asyn mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistence or exception + * due to the data access in async mode. + * + * @param tableView The sender. + */ +- (void)tableViewLockDataSourceForDataUpdating:(ASTableView *)tableView; + +/** + * Indicator to unlock the data source for data loading in asyn mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistence or exception + * due to the data access in async mode. + * + * @param tableView The sender. + */ +- (void)tableViewUnlockDataSourceForDataUpdating:(ASTableView *)tableView; + @end diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 4169dc5f8a..2c2c43cf80 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -112,6 +112,8 @@ static BOOL _isInterceptedSelector(SEL sel) ASRangeController *_rangeController; } +@property (atomic, assign) BOOL asyncDataSouceLocked; + @end @implementation ASTableView @@ -137,6 +139,8 @@ static BOOL _isInterceptedSelector(SEL sel) _proxyDelegate = [[_ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; super.delegate = (id)_proxyDelegate; + _asyncDataSouceLocked = NO; + return self; } @@ -425,6 +429,28 @@ static BOOL _isInterceptedSelector(SEL sel) return CGSizeMake(self.bounds.size.width, FLT_MAX); } +- (void)dataControllerLockDataSourceForDataUpdating +{ + ASDisplayNodeAssert(!self.asyncDataSouceLocked, @"The data source has already been locked !"); + + self.asyncDataSouceLocked = YES; + + if ([_asyncDataSource respondsToSelector:@selector(tableViewLockDataSourceForDataUpdating:)]) { + [_asyncDataSource tableViewLockDataSourceForDataUpdating:self]; + } +} + +- (void)dataControllerUnlockDataSourceForDataUpdating +{ + ASDisplayNodeAssert(self.asyncDataSouceLocked, @"The data source has already been unlocked !"); + + self.asyncDataSouceLocked = NO; + + if ([_asyncDataSource respondsToSelector:@selector(tableViewUnlockDataSourceForDataUpdating:)]) { + [_asyncDataSource tableViewUnlockDataSourceForDataUpdating:self]; + } +} + - (NSUInteger)dataController:(ASDataController *)dataControllre rowsInSection:(NSUInteger)section { return [_asyncDataSource tableView:self numberOfRowsInSection:section]; diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index f271066d44..f493eee28d 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -3,6 +3,19 @@ #import #import +/** + Enable the data fetching in async mode. + + If enabled, we will fetch data through `dataController:nodeAtIndexPath:` and `dataController:rowsInSection:` in background thread. + Otherwise, the methods will be invoked synchronically in calling thread. + + Enabling data fetching in async mode could avoid blocking main thread while allocating cell on main thread, which is frequently reported + issue for handing large scale data. On another hand, the application code will take the responsibility to avoid data inconsistence. + Specifically, we will lock the data source through `dataControllerLockDataSourceForDataUpdating`, and unlock it by `dataControllerUnlockDataSourceForDataUpdating` + after the data fetching. The application should not update the data source while the data source is locked. + */ +#define ENABLE_ASYNC_DATA_FETCHING 1 + @class ASCellNode; @class ASDataController; @@ -27,13 +40,23 @@ typedef NSUInteger ASDataControllerAnimationOptions; /** Fetch the number of rows in specific section. */ -- (NSUInteger)dataController:(ASDataController *)dataControllre rowsInSection:(NSUInteger)section; +- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section; /** Fetch the number of sections. */ - (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController; +/** + Lock the data source for data fetching. + */ +- (void)dataControllerLockDataSourceForDataUpdating; + +/** + Unlock the data source after data fetching. + */ +- (void)dataControllerUnlockDataSourceForDataUpdating; + @end /** diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 483b5d29c2..78356e94c5 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -10,6 +10,18 @@ #import "ASMultidimensionalArrayUtils.h" #import "ASDisplayNodeInternal.h" +#ifdef ENABLE_ASYNC_DATA_FETCHING + #define BEGIN_DATA_FETCHING \ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ \ + [_dataSource dataControllerLockDataSourceForDataUpdating]; + #define END_DATA_FETCHING \ + [_dataSource dataControllerUnlockDataSourceForDataUpdating]; \ + }); +#else + #define BEGIN_DATA_FETCHING + #define END_DATA_FETCHING +#endif + #define INSERT_NODES(multidimensionalArray, indexPath, elements, animationOption) \ { \ if ([_delegate respondsToSelector:@selector(dataController:willInsertNodes:atIndexPaths:withAnimationOption:)]) { \ @@ -131,7 +143,10 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Initial Data Loading -- (void)initialDataLoadingWithAnimationOption:(ASDataControllerAnimationOptions)animationOption { +- (void)initialDataLoadingWithAnimationOption:(ASDataControllerAnimationOptions)animationOption +{ + BEGIN_DATA_FETCHING + NSMutableArray *indexPaths = [NSMutableArray array]; NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; @@ -150,6 +165,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // insert elements [self insertRowsAtIndexPaths:indexPaths withAnimationOption:animationOption]; + + END_DATA_FETCHING } #pragma mark - Data Update @@ -187,6 +204,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)insertSections:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption { + BEGIN_DATA_FETCHING + __block int nodeTotalCnt = 0; NSMutableArray *nodeCounts = [NSMutableArray arrayWithCapacity:indexSet.count]; [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { @@ -222,6 +241,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self _batchInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOption]; }); + + END_DATA_FETCHING } - (void)deleteSections:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption @@ -239,6 +260,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)reloadSections:(NSIndexSet *)sections withAnimationOption:(ASDataControllerAnimationOptions)animationOption { + BEGIN_DATA_FETCHING + // We need to keep data query on data source in the calling thread. NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; @@ -264,6 +287,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // reinsert the elements [self _batchInsertNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOption]; }); + + END_DATA_FETCHING } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOption:(ASDataControllerAnimationOptions)animationOption @@ -349,6 +374,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption { + BEGIN_DATA_FETCHING + // sort indexPath to avoid messing up the index when inserting in several batches NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; @@ -357,6 +384,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } [self _batchInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOption]; + + END_DATA_FETCHING } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption @@ -373,6 +402,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption { + BEGIN_DATA_FETCHING + // The reloading operation required reloading the data // Loading data in the calling thread NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; @@ -388,6 +419,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self _batchInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOption]; }); + + END_DATA_FETCHING } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOption:(ASDataControllerAnimationOptions)animationOption @@ -407,6 +440,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)reloadDataWithAnimationOption:(ASDataControllerAnimationOptions)animationOption { + BEGIN_DATA_FETCHING + // Fetching data in calling thread NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; @@ -446,6 +481,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self _batchInsertNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOption]; }); + + END_DATA_FETCHING } #pragma mark - Data Querying