// Copyright 2004-present Facebook. All Rights Reserved. #import "ASDataController.h" #import #import "ASAssert.h" #import "ASCellNode.h" #import "ASDisplayNode.h" #import "ASMultidimensionalArrayUtils.h" #define INSERT_NODES(multidimensionalArray, indexPath, elements) \ { \ if ([_delegate respondsToSelector:@selector(dataController:willInsertNodes:atIndexPaths:)]) { \ [_delegate dataController:self willInsertNodes:elements atIndexPaths:indexPath]; \ } \ ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(multidimensionalArray, indexPath, elements); \ if ([_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:)]) { \ [_delegate dataController:self didInsertNodes:elements atIndexPaths:indexPath]; \ } \ } #define DELETE_NODES(multidimensionalArray, indexPath) \ { \ if ([_delegate respondsToSelector:@selector(dataController:willDeleteNodesAtIndexPaths:)]) { \ [_delegate dataController:self willDeleteNodesAtIndexPaths:indexPath]; \ } \ ASDeleteElementsInMultidimensionalArrayAtIndexPaths(multidimensionalArray, indexPath); \ if ([_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:)]) { \ [_delegate dataController:self didDeleteNodesAtIndexPaths:indexPath]; \ } \ } #define INSERT_SECTIONS(multidimensionalArray, indexSet, sections) \ { \ if ([_delegate respondsToSelector:@selector(dataController:willInsertSections:atIndexSet:)]) { \ [_delegate dataController:self willInsertSections:sections atIndexSet:indexSet]; \ } \ [multidimensionalArray insertObjects:sections atIndexes:indexSet]; \ if ([_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:)]) { \ [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet]; \ } \ } #define DELETE_SECTIONS(multidimensionalArray, indexSet) \ { \ if ([_delegate respondsToSelector:@selector(dataController:willDeleteSectionsAtIndexSet:)]) { \ [_delegate dataController:self willDeleteSectionsAtIndexSet:indexSet]; \ } \ [multidimensionalArray removeObjectsAtIndexes:indexSet]; \ if ([_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:)]) { \ [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet]; \ } \ } // // The background update is not fully supported yet, although it is trivial to fix it. The underline // problem is we need to do the profiling between the main thread updating and background updating, // and then decided which way to go. // // For background update, we could avoid the multi-dimensinonal array operation (insertion / deletion) // on main thread. However, the sideback is we need to dispatch_sync to lock main thread for data query, // although it is running on a concurrent queue and should be fast enough. // // For main thread update, we need to do the multi-dimensional operations (insertion / deletion) on // main thread, but we will gain the performance in data query. Considering data query is much more // frequent than data updating, so we keep it on main thread for the initial version. // // #define ENABLE_BACKGROUND_UPDATE 0 const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; static void *kASSizingQueueContext = &kASSizingQueueContext; static void *kASDataUpdatingQueueContext = &kASDataUpdatingQueueContext; @interface ASDataController () { NSMutableArray *_nodes; } @end @implementation ASDataController - (instancetype)init { if (self = [super init]) { _nodes = [NSMutableArray array]; } return self; } #pragma mark - Utils + (NSUInteger)parallelProcessorCount { static NSUInteger parallelProcessorCount; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ parallelProcessorCount = [[NSProcessInfo processInfo] processorCount]; }); return parallelProcessorCount; } + (dispatch_queue_t)sizingQueue { static dispatch_queue_t sizingQueue = NULL; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sizingQueue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDataController.sizingQueue", DISPATCH_QUEUE_SERIAL); dispatch_queue_set_specific(sizingQueue, kASSizingQueueContext, kASSizingQueueContext, NULL); dispatch_set_target_queue(sizingQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); }); return sizingQueue; } + (BOOL)isSizingQueue { return kASSizingQueueContext == dispatch_get_specific(kASSizingQueueContext); } /** * Concurrent queue for query / updating the cached data. * The data query is more frequent than the data updating, so we use dispatch_sync for reading, and dispatch_barrier_async for writing. */ + (dispatch_queue_t)dataUpdatingQueue { static dispatch_queue_t dataUpdatingQueue = NULL; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ dataUpdatingQueue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDataController.dataUpdatingQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_queue_set_specific(dataUpdatingQueue, kASDataUpdatingQueueContext, kASDataUpdatingQueueContext, NULL); }); return dataUpdatingQueue; } + (BOOL)isDataUpdatingQueue { return kASDataUpdatingQueueContext == dispatch_get_specific(kASDataUpdatingQueueContext); } - (void)asyncUpdateDataWithBlock:(dispatch_block_t)block { #if ENABLE_BACKGROUND_UPDATE dispatch_barrier_async([ASDataController dataUpdatingQueue], ^{ block(); }); #else dispatch_async(dispatch_get_main_queue(), ^{ block(); }); #endif } - (void)syncUpdateDataWithBlock:(dispatch_block_t)block { #if ENABLE_BACKGROUND_UPDATE dispatch_barrier_sync([ASDataController dataUpdatingQueue], ^{ block(); }); #else dispatch_sync(dispatch_get_main_queue(), ^{ block(); }); #endif } - (void)queryDataWithBlock:(dispatch_block_t)block { #if ENABLE_BACKGROUND_UPDATE if ([ASDataController isDataUpdatingQueue]) { block(); } else { dispatch_sync([ASDataController dataUpdatingQueue], ^{ block(); }); } #else ASDisplayNodeAssertMainThread(); block(); #endif } #pragma mark - Initial Data Loading - (void)initialDataLoading { NSMutableArray *indexPaths = [NSMutableArray array]; NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; // insert sections [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)]]; for (NSUInteger i = 0; i < sectionNum; i++) { NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; for (NSUInteger j = 0; j < rowNum; j++) { [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; } } // insert elements [self insertRowsAtIndexPaths:indexPaths]; } #pragma mark - Data Update - (void)insertSections:(NSIndexSet *)indexSet { __block int nodeTotalCnt = 0; NSMutableArray *nodeCounts = [NSMutableArray arrayWithCapacity:indexSet.count]; [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { NSUInteger cnt = [_dataSource dataController:self rowsInSection:idx]; [nodeCounts addObject:@(cnt)]; nodeTotalCnt += cnt; }]; NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:nodeTotalCnt]; NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:nodeTotalCnt]; __block NSUInteger idx = 0; [indexSet enumerateIndexesUsingBlock:^(NSUInteger sectionIdx, BOOL *stop) { NSUInteger cnt = [nodeCounts[idx++] unsignedIntegerValue]; for (int i = 0; i < cnt; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sectionIdx]; [indexPaths addObject:indexPath]; ASCellNode *node = [_dataSource dataController:self nodeAtIndexPath:indexPath]; [nodes addObject:node]; } }]; dispatch_async([[self class] sizingQueue], ^{ [self syncUpdateDataWithBlock:^{ NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; for (NSUInteger i = 0; i < indexSet.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } INSERT_SECTIONS(_nodes , indexSet, sectionArray); }]; [self _insertNodes:nodes atIndexPaths:indexPaths]; }); } - (void)deleteSections:(NSIndexSet *)indexSet { dispatch_async([[self class] sizingQueue], ^{ [self asyncUpdateDataWithBlock:^{ // remove elements NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, indexSet); DELETE_NODES(_nodes, indexPaths); DELETE_SECTIONS(_nodes, indexSet); }]; }); } - (void)reloadSections:(NSIndexSet *)sections { // We need to keep data query on data source in the calling thread. NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { NSUInteger rowNum = [_dataSource dataController:self rowsInSection:sections.count]; NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; for (NSUInteger i = 0; i < rowNum; i++) { NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; [updatedIndexPaths addObject:indexPath]; [updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; } }]; dispatch_async([ASDataController sizingQueue], ^{ [self syncUpdateDataWithBlock:^{ // remove elements NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, sections); DELETE_NODES(_nodes, indexPaths); }]; // reinsert the elements [self _insertNodes:updatedNodes atIndexPaths:updatedIndexPaths]; }); } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { dispatch_async([ASDataController sizingQueue], ^{ [self asyncUpdateDataWithBlock:^{ // remove elements NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, [NSIndexSet indexSetWithIndex:section]); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); DELETE_NODES(_nodes, indexPaths); // update the section of indexpaths NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; }]; // Don't re-calculate size for moving INSERT_NODES(_nodes, updatedIndexPaths, nodes); }]; }); } - (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths { if (!nodes.count) { return; } dispatch_group_t layoutGroup = dispatch_group_create(); for (NSUInteger j = 0; j < nodes.count && j < indexPaths.count; j += kASDataControllerSizingCountPerProcessor) { NSArray *subIndexPaths = [indexPaths subarrayWithRange:NSMakeRange(j, MIN(kASDataControllerSizingCountPerProcessor, indexPaths.count - j))]; NSMutableArray *nodeBoundSizes = [[NSMutableArray alloc] initWithCapacity:kASDataControllerSizingCountPerProcessor]; [subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [nodeBoundSizes addObject:[NSValue valueWithCGSize:[_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]]]; }]; dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { ASCellNode *node = nodes[j + idx]; [node measure:[nodeBoundSizes[idx] CGSizeValue]]; node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); }]; }); } dispatch_block_t block = ^{ dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); [self asyncUpdateDataWithBlock:^{ // updating the cells INSERT_NODES(_nodes, indexPaths, nodes); }]; }; if ([ASDataController isSizingQueue]) { block(); } else { dispatch_async([ASDataController sizingQueue], block); } } - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths { NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; // sort indexPath to avoid messing up the index when inserting in several batches NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; // Processing in batches for (NSUInteger i = 0; i < sortedIndexPaths.count; i += blockSize) { NSArray *batchedIndexPaths = [sortedIndexPaths subarrayWithRange:NSMakeRange(i, MIN(sortedIndexPaths.count - i, blockSize))]; NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:batchedIndexPaths.count]; [batchedIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; }]; [self _insertNodes:nodes atIndexPaths:batchedIndexPaths]; } } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths { // sort indexPath in order to avoid messing up the index when deleting NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; dispatch_async([ASDataController sizingQueue], ^{ [self asyncUpdateDataWithBlock:^{ DELETE_NODES(_nodes, sortedIndexPaths); }]; }); } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths { // The reloading operation required reloading the data // Loading data in the calling thread NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; }]; dispatch_async([ASDataController sizingQueue], ^{ [self syncUpdateDataWithBlock:^{ DELETE_NODES(_nodes, nodes); }]; [self _insertNodes:nodes atIndexPaths:indexPaths]; }); } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { dispatch_async([ASDataController sizingQueue], ^{ [self asyncUpdateDataWithBlock:^{ NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [NSArray arrayWithObject:indexPath]); NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; DELETE_NODES(_nodes, indexPaths); // Don't re-calculate size for moving INSERT_NODES(_nodes, indexPaths, nodes); }]; }); } - (void)reloadData { // Fetching data in calling thread NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; for (NSUInteger i = 0; i < sectionNum; i++) { NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; for (NSUInteger j = 0; j < rowNum; j++) { NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j]; [updatedIndexPaths addObject:indexPath]; [updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; } } dispatch_async([ASDataController sizingQueue], ^{ [self syncUpdateDataWithBlock:^{ NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_nodes); DELETE_NODES(_nodes, indexPaths); NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _nodes.count)]; DELETE_SECTIONS(_nodes, indexSet); // Insert section NSMutableArray *sections = [[NSMutableArray alloc] initWithCapacity:sectionNum]; for (int i = 0; i < sectionNum; i++) { [sections addObject:[[NSMutableArray alloc] init]]; } INSERT_SECTIONS(_nodes, [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, sectionNum)], sections); }]; [self _insertNodes:updatedNodes atIndexPaths:updatedIndexPaths]; }); } #pragma mark - Data Querying - (NSUInteger)numberOfSections { __block NSUInteger sectionNum; [self queryDataWithBlock:^{ sectionNum = [_nodes count]; }]; return sectionNum; } - (NSUInteger)numberOfRowsInSection:(NSUInteger)section { __block NSUInteger rowNum; [self queryDataWithBlock:^{ rowNum = [_nodes[section] count]; }]; return rowNum; } - (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath { __block ASCellNode *node; [self queryDataWithBlock:^{ node = _nodes[indexPath.section][indexPath.row]; }]; return node; } - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths { __block NSArray *arr = nil; [self queryDataWithBlock:^{ arr = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); }]; return arr; } @end