Merge pull request #303 from facebook/enable_async_data_fetching

Move ASTableView & ASCollectionView data fetching to background thread
This commit is contained in:
Nadine Salter 2015-02-23 15:25:36 -08:00
commit b023cfbb2a
8 changed files with 334 additions and 126 deletions

View File

@ -35,6 +35,19 @@
*/
@property (nonatomic, assign) ASRangeTuningParameters rangeTuningParameters;
/**
* Initializer.
*
* @discussion If asyncDataFetching is enabled, the `AScollectionView` will fetch data through `collectionView:numberOfRowsInSection:` and
* `collectionView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically
* from calling thread.
* Enabling asyncDataFetching could avoid blocking main thread for `ASCellNode` allocation, which is frequently reported issue for
* large scale data. On another hand, the application code need take the responsibility to avoid data inconsistence. Specifically,
* we will lock the data source through `collectionViewLockDataSource`, and unlock it by `collectionViewUnlockDataSource` after the data fetching.
* The application should not update the data source while the data source is locked, to keep data consistence.
*/
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout asyncDataFetching:(BOOL)asyncDataFetchingEnabled;
/**
* Reload everything from scratch, destroying the working range and all cached nodes.
*
@ -108,6 +121,24 @@
*/
- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath;
/**
* Indicator to lock the data source for data fetching 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 collectionView The sender.
*/
- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView;
/**
* Indicator to unlock the data source for data fetching 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 collectionView The sender.
*/
- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView;
@end

View File

@ -101,8 +101,12 @@ static BOOL _isInterceptedSelector(SEL sel)
BOOL _performingBatchUpdates;
NSMutableArray *_batchUpdateBlocks;
BOOL _asyncDataFetchingEnabled;
}
@property (atomic, assign) BOOL asyncDataSourceLocked;
@end
@implementation ASCollectionView
@ -111,6 +115,11 @@ static BOOL _isInterceptedSelector(SEL sel)
#pragma mark Lifecycle.
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
{
return [self initWithFrame:frame collectionViewLayout:layout asyncDataFetching:NO];
}
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout asyncDataFetching:(BOOL)asyncDataFetchingEnabled
{
if (!(self = [super initWithFrame:frame collectionViewLayout:layout]))
return nil;
@ -124,13 +133,16 @@ static BOOL _isInterceptedSelector(SEL sel)
_rangeController.delegate = self;
_rangeController.layoutController = _layoutController;
_dataController = [[ASDataController alloc] init];
_dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled];
_dataController.delegate = _rangeController;
_dataController.dataSource = self;
_proxyDelegate = [[_ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self];
super.delegate = (id<UICollectionViewDelegate>)_proxyDelegate;
_asyncDataFetchingEnabled = asyncDataFetchingEnabled;
_asyncDataSourceLocked = NO;
_performingBatchUpdates = NO;
_batchUpdateBlocks = [NSMutableArray array];
@ -375,6 +387,26 @@ static BOOL _isInterceptedSelector(SEL sel)
}
}
- (void)dataControllerLockDataSource
{
ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked");
self.asyncDataSourceLocked = YES;
if ([_asyncDataSource respondsToSelector:@selector(collectionViewLockDataSource:)]) {
[_asyncDataSource collectionViewLockDataSource:self];
}
}
- (void)dataControllerUnlockDataSource
{
ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has alredy been unlocked !");
self.asyncDataSourceLocked = NO;
if ([_asyncDataSource respondsToSelector:@selector(collectionViewUnlockDataSource:)]) {
[_asyncDataSource collectionViewUnlockDataSource:self];
}
}
#pragma mark -
#pragma mark ASRangeControllerDelegate.

View File

@ -35,6 +35,19 @@
*/
@property (nonatomic, assign) ASRangeTuningParameters rangeTuningParameters;
/**
* initializer.
*
* @discussion If asyncDataFetching is enabled, the `ASTableView` will fetch data through `tableView:numberOfRowsInSection:` and
* `tableView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically
* from calling thread.
* Enabling asyncDataFetching could avoid blocking main thread for `ASCellNode` allocation, which is frequently reported issue for
* large scale data. On another hand, the application code need take the responsibility to avoid data inconsistence. Specifically,
* we will lock the data source through `tableViewLockDataSource`, and unlock it by `tableViewUnlockDataSource` after the data fetching.
* The application should not update the data source while the data source is locked, to keep data consistence.
*/
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled;
/**
* Reload everything from scratch, destroying the working range and all cached nodes.
*
@ -110,6 +123,24 @@
*/
- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath;
/**
* Indicator to lock the data source for data fetching 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)tableViewLockDataSource:(ASTableView *)tableView;
/**
* Indicator to unlock the data source for data fetching 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)tableViewUnlockDataSource:(ASTableView *)tableView;
@end

View File

@ -110,8 +110,12 @@ static BOOL _isInterceptedSelector(SEL sel)
ASFlowLayoutController *_layoutController;
ASRangeController *_rangeController;
BOOL _asyncDataFetchingEnabled;
}
@property (atomic, assign) BOOL asyncDataSourceLocked;
@end
@implementation ASTableView
@ -121,6 +125,12 @@ static BOOL _isInterceptedSelector(SEL sel)
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
{
return [self initWithFrame:frame style:style asyncDataFetching:NO];
}
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled
{
if (!(self = [super initWithFrame:frame style:style]))
return nil;
@ -130,13 +140,16 @@ static BOOL _isInterceptedSelector(SEL sel)
_rangeController.layoutController = _layoutController;
_rangeController.delegate = self;
_dataController = [[ASDataController alloc] init];
_dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled];
_dataController.dataSource = self;
_dataController.delegate = _rangeController;
_proxyDelegate = [[_ASTableViewProxy alloc] initWithTarget:nil interceptor:self];
super.delegate = (id<UITableViewDelegate>)_proxyDelegate;
_asyncDataFetchingEnabled = asyncDataFetchingEnabled;
_asyncDataSourceLocked = NO;
return self;
}
@ -425,6 +438,28 @@ static BOOL _isInterceptedSelector(SEL sel)
return CGSizeMake(self.bounds.size.width, FLT_MAX);
}
- (void)dataControllerLockDataSource
{
ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked !");
self.asyncDataSourceLocked = YES;
if ([_asyncDataSource respondsToSelector:@selector(tableViewLockDataSource:)]) {
[_asyncDataSource tableViewLockDataSource:self];
}
}
- (void)dataControllerUnlockDataSource
{
ASDisplayNodeAssert(self.asyncDataSourceLocked, @"The data source has already been unlocked !");
self.asyncDataSourceLocked = NO;
if ([_asyncDataSource respondsToSelector:@selector(tableViewUnlockDataSource:)]) {
[_asyncDataSource tableViewUnlockDataSource:self];
}
}
- (NSUInteger)dataController:(ASDataController *)dataControllre rowsInSection:(NSUInteger)section
{
return [_asyncDataSource tableView:self numberOfRowsInSection:section];

View File

@ -28,13 +28,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)dataControllerLockDataSource;
/**
Unlock the data source after data fetching.
*/
- (void)dataControllerUnlockDataSource;
@end
/**
@ -97,6 +107,20 @@ typedef NSUInteger ASDataControllerAnimationOptions;
*/
@property (nonatomic, weak) id<ASDataControllerDelegate> delegate;
/**
* Designated iniailizer.
*
* @param asyncDataFetchingEnabled Enable the data fetching in async mode.
* @discussion 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 `dataControllerLockDataSource`,
* and unlock it by `dataControllerUnlockDataSource` after the data fetching. The application should not update the data source while
* the data source is locked.
*/
- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled;
/** @name Initial loading */
- (void)initialDataLoadingWithAnimationOption:(ASDataControllerAnimationOptions)animationOption;

View File

@ -61,6 +61,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
@interface ASDataController () {
NSMutableArray *_nodes;
NSMutableArray *_pendingBlocks;
BOOL _asyncDataFetchingEnabled;
}
@property (atomic, assign) NSUInteger batchUpdateCounter;
@ -69,11 +70,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
@implementation ASDataController
- (instancetype)init {
- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled {
if (self = [super init]) {
_nodes = [NSMutableArray array];
_pendingBlocks = [NSMutableArray array];
_batchUpdateCounter = 0;
_asyncDataFetchingEnabled = asyncDataFetchingEnabled;
}
return self;
@ -129,11 +131,23 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
});
}
- (void)performDataFetchingWithBlock:(dispatch_block_t)block {
if (_asyncDataFetchingEnabled) {
[_dataSource dataControllerLockDataSource];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
block();
[_dataSource dataControllerUnlockDataSource];
});
} else {
block();
}
}
#pragma mark - Initial Data Loading
- (void)initialDataLoadingWithAnimationOption:(ASDataControllerAnimationOptions)animationOption {
[self performDataFetchingWithBlock:^{
NSMutableArray *indexPaths = [NSMutableArray array];
NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self];
// insert sections
@ -150,6 +164,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
// insert elements
[self insertRowsAtIndexPaths:indexPaths withAnimationOption:animationOption];
}];
}
#pragma mark - Data Update
@ -187,6 +203,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)insertSections:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption
{
[self performDataFetchingWithBlock:^{
__block int nodeTotalCnt = 0;
NSMutableArray *nodeCounts = [NSMutableArray arrayWithCapacity:indexSet.count];
[indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
@ -222,6 +239,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[self _batchInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOption];
});
}];
}
- (void)deleteSections:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption
@ -239,6 +257,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)reloadSections:(NSIndexSet *)sections withAnimationOption:(ASDataControllerAnimationOptions)animationOption
{
[self performDataFetchingWithBlock:^{
// 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 +283,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
// reinsert the elements
[self _batchInsertNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOption];
});
}];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOption:(ASDataControllerAnimationOptions)animationOption
@ -349,6 +369,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption
{
[self performDataFetchingWithBlock:^{
// 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 +378,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
}
[self _batchInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOption];
}];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption
@ -373,6 +395,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption
{
[self performDataFetchingWithBlock:^{
// The reloading operation required reloading the data
// Loading data in the calling thread
NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];
@ -388,6 +411,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[self _batchInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOption];
});
}];
}
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOption:(ASDataControllerAnimationOptions)animationOption
@ -407,6 +431,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)reloadDataWithAnimationOption:(ASDataControllerAnimationOptions)animationOption
{
[self performDataFetchingWithBlock:^{
// Fetching data in calling thread
NSMutableArray *updatedNodes = [[NSMutableArray alloc] init];
NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init];
@ -446,6 +471,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[self _batchInsertNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOption];
});
}];
}
#pragma mark - Data Querying

View File

@ -34,7 +34,7 @@
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
_collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:YES];
_collectionView.asyncDataSource = self;
_collectionView.asyncDelegate = self;
_collectionView.backgroundColor = [UIColor whiteColor];
@ -78,4 +78,13 @@
return 300;
}
- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView {
// lock the data source
// The data source should not be change until it is unlocked.
}
- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView {
// unlock the data source to enable data source updating.
}
@end

View File

@ -12,6 +12,7 @@
#import "ViewController.h"
#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import <AsyncDisplayKit/ASAssert.h>
#import "BlurbNode.h"
#import "KittenNode.h"
@ -25,8 +26,13 @@ static const NSInteger kLitterSize = 20;
// array of boxed CGSizes corresponding to placekitten kittens
NSArray *_kittenDataSource;
BOOL _dataSourceLocked;
}
@property (nonatomic, strong) NSArray *kittenDataSource;
@property (atomic, assign) BOOL dataSourceLocked;
@end
@ -40,7 +46,7 @@ static const NSInteger kLitterSize = 20;
if (!(self = [super init]))
return nil;
_tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain asyncDataFetching:YES];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator
_tableView.asyncDataSource = self;
_tableView.asyncDelegate = self;
@ -59,6 +65,12 @@ static const NSInteger kLitterSize = 20;
return self;
}
- (void)setKittenDataSource:(NSArray *)kittenDataSource {
ASDisplayNodeAssert(!self.dataSourceLocked, @"Could not update data source when it is locked !");
_kittenDataSource = kittenDataSource;
}
- (void)viewDidLoad
{
[super viewDidLoad];
@ -105,4 +117,12 @@ static const NSInteger kLitterSize = 20;
return NO;
}
- (void)tableViewLockDataSource:(ASTableView *)tableView {
self.dataSourceLocked = YES;
}
- (void)tableViewUnlockDataSource:(ASTableView *)tableView {
self.dataSourceLocked = NO;
}
@end