From 3668f45286a56a87d200aea0299caee9d2e4a880 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 22 Mar 2016 08:09:59 -0700 Subject: [PATCH] Prevent deallocation of asyncDataSource and asyncDelegate in ASCollectionView and ASTableView Grab a strong reference for asyncDataSource and asyncDelegate in ASCollectionView and ASTableView before executing the range update to be sure they are not going away while executing the range update. This can happen in range updates while going back in the view controller hierarchy --- AsyncDisplayKit/ASCollectionView.mm | 9 +++++++- AsyncDisplayKit/ASTableView.mm | 9 +++++++- AsyncDisplayKit/Details/ASRangeController.mm | 23 ++++++++++++++++++- ...SRangeControllerUpdateRangeProtocol+Beta.h | 11 ++++++++- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index ae1f579232..a91c84fef3 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -1139,7 +1139,14 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their // their update in the layout pass if (![node supportsRangeManagedInterfaceState]) { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + // Grab a strong reference for data source and delegate to be sure they are not going away while executing + // the range update. This can happen in range updates while going back in the view controller hierarchy + __block id asyncDataSource = _asyncDataSource; + __block id asyncDelegate = _asyncDelegate; + [_rangeController scheduleRangeUpdateCompletion:^{ + asyncDataSource = nil; + asyncDelegate = nil; + }]; } } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index d8d9037d36..9e5ad025c7 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -1072,7 +1072,14 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their // their update in the layout pass if (![node supportsRangeManagedInterfaceState]) { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + // Grab a strong reference for data source and delegate to be sure they are not going away while executing + // the range update. This can happen in range updates while going back in the view controller hierarchy + __block id asyncDataSource = _asyncDataSource; + __block id asyncDelegate = _asyncDelegate; + [_rangeController scheduleRangeUpdateCompletion:^{ + asyncDataSource = nil; + asyncDelegate = nil; + }]; } } diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 7935564a3d..c9fffb4cd0 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -27,6 +27,7 @@ BOOL _didUpdateCurrentRange; BOOL _didRegisterForNotifications; CFAbsoluteTime _pendingDisplayNodesTimestamp; + NSMutableArray *_scheduledRangeUpdateCompletionBlocks; } @end @@ -46,6 +47,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; _rangeIsValid = YES; _currentRangeMode = ASLayoutRangeModeInvalid; _didUpdateCurrentRange = NO; + _scheduledRangeUpdateCompletionBlocks = [NSMutableArray array]; [[[self class] allRangeControllersWeakSet] addObject:self]; @@ -111,6 +113,15 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; - (void)scheduleRangeUpdate { + [self scheduleRangeUpdateCompletion:nil]; +} + +- (void)scheduleRangeUpdateCompletion:(void (^)(void))completion +{ + if (completion) { + [_scheduledRangeUpdateCompletionBlocks addObject:completion]; + } + if (_queuedRangeUpdate) { return; } @@ -118,8 +129,13 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; // coalesce these events -- handling them multiple times per runloop is noisy and expensive _queuedRangeUpdate = YES; + __block id dataSource = _dataSource; + __block id delegate = _delegate; dispatch_async(dispatch_get_main_queue(), ^{ - [self performRangeUpdate]; + [self _updateVisibleNodeIndexPaths]; + + dataSource = nil; + delegate = nil; }); } @@ -320,6 +336,11 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; _rangeIsValid = YES; _queuedRangeUpdate = NO; + for (void (^completionBlock)(void) in _scheduledRangeUpdateCompletionBlocks) { + completionBlock(); + } + [_scheduledRangeUpdateCompletionBlocks removeAllObjects]; + #if ASRangeControllerLoggingEnabled // NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; // BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet]; diff --git a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h index 4f34cd1cef..f9fcbf77b7 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h +++ b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h @@ -49,6 +49,12 @@ */ - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode; +/** + * Schedule a range update and call the completion block if finished. This drives updating the working + * ranges, and triggering their actions. + */ +- (void)scheduleRangeUpdateCompletion:(void (^)(void))completion; + @end @@ -64,7 +70,10 @@ @interface ASViewController (ASRangeControllerUpdateRangeProtocol) -/// Automatically adjust range mode based on view events if the containing node confirms to the ASRangeControllerUpdateRangeProtocol +/** + * Automatically adjust range mode based on view events if the containing node confirms to the + * ASRangeControllerUpdateRangeProtocol + */ @property (nonatomic, assign) BOOL automaticallyAdjustRangeModeBasedOnViewEvents; @end