From 6d01bbeb1939f6d769c28471c71e5b35519b8e60 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 9 Sep 2016 11:09:57 -0700 Subject: [PATCH 1/2] Let nodes deallocate naturally, manually trampoline UIKit ivars Be more aggressive with main thread punting Trampoline setting the dataSource/delegate onto the main thread Short-circuit the supplementary nodes method if no data source Don't rely on assertions Mark variable unused to fix release builds Handle ASCollectionNode/ASTableNode deallocation better Add some comments about new macro --- AsyncDisplayKit.xcodeproj/project.pbxproj | 16 - AsyncDisplayKit/ASCollectionNode.mm | 25 +- AsyncDisplayKit/ASCollectionView.mm | 102 +++- AsyncDisplayKit/ASDisplayNode.h | 3 +- AsyncDisplayKit/ASDisplayNode.mm | 121 ++++- AsyncDisplayKit/ASTableNode.mm | 26 +- AsyncDisplayKit/ASTableView.mm | 81 ++- AsyncDisplayKit/ASViewController.mm | 6 + AsyncDisplayKit/Details/ASDataController.h | 3 +- AsyncDisplayKit/Details/ASDataController.mm | 20 - .../Details/ASDealloc2MainObject.h | 26 - .../Details/ASDealloc2MainObject.m | 34 -- AsyncDisplayKit/Details/ASRangeController.h | 2 +- .../Private/ASDisplayNode+FrameworkPrivate.h | 1 - .../Private/ASDisplayNodeInternal.h | 2 +- AsyncDisplayKit/Private/ASInternalHelpers.h | 8 + AsyncDisplayKit/Private/ASInternalHelpers.m | 42 ++ AsyncDisplayKit/Private/_AS-objc-internal.h | 473 ------------------ AsyncDisplayKitTests/ASDisplayNodeTests.m | 54 +- 19 files changed, 394 insertions(+), 651 deletions(-) delete mode 100644 AsyncDisplayKit/Details/ASDealloc2MainObject.h delete mode 100644 AsyncDisplayKit/Details/ASDealloc2MainObject.m delete mode 100644 AsyncDisplayKit/Private/_AS-objc-internal.h diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 59b18dd574..b861e3bba2 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -64,7 +64,6 @@ 058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */; }; 058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A36195D057000B7D73C /* ASTextNodeTests.m */; }; 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */; }; - 05A6D05B19D0EB64002DD95E /* ASDealloc2MainObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */; }; 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 18C2ED801B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */; }; @@ -388,8 +387,6 @@ B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.mm */; }; B35062171B010EFD0018CF92 /* ASDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 464052191A3F83C40061C0BA /* ASDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521A1A3F83C40061C0BA /* ASDataController.mm */; }; - B35062191B010EFD0018CF92 /* ASDealloc2MainObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B350621A1B010EFD0018CF92 /* ASDealloc2MainObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; B350621B1B010EFD0018CF92 /* ASFlowLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */; settings = {ATTRIBUTES = (Public, ); }; }; B350621C1B010EFD0018CF92 /* ASFlowLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */; }; B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -414,7 +411,6 @@ B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 058D09FE195D050800B7D73C /* _ASAsyncTransactionGroup.m */; }; B35062431B010EFD0018CF92 /* UIView+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09FF195D050800B7D73C /* UIView+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */; }; - B35062481B010EFD0018CF92 /* _AS-objc-internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A02195D050800B7D73C /* _AS-objc-internal.h */; }; B35062491B010EFD0018CF92 /* _ASCoreAnimationExtras.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */; }; B350624A1B010EFD0018CF92 /* _ASCoreAnimationExtras.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */; }; B350624B1B010EFD0018CF92 /* _ASPendingState.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A05195D050800B7D73C /* _ASPendingState.h */; }; @@ -551,7 +547,6 @@ F7CE6C3A1D2CDB3E00BE4C15 /* ASBatchContext.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 299DA1A71A828D2900162D41 /* ASBatchContext.h */; }; F7CE6C3B1D2CDB3E00BE4C15 /* ASCollectionViewFlowLayoutInspector.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */; }; F7CE6C3D1D2CDB3E00BE4C15 /* ASCollectionViewLayoutController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */; }; - F7CE6C3E1D2CDB3E00BE4C15 /* ASDealloc2MainObject.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */; }; F7CE6C3F1D2CDB3E00BE4C15 /* ASEnvironment.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 698548611CA9E025008A345F /* ASEnvironment.h */; }; F7CE6C401D2CDB3E00BE4C15 /* ASFlowLayoutController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */; }; F7CE6C411D2CDB3E00BE4C15 /* ASHighlightOverlayLayer.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */; }; @@ -729,7 +724,6 @@ F7CE6C3A1D2CDB3E00BE4C15 /* ASBatchContext.h in CopyFiles */, F7CE6C3B1D2CDB3E00BE4C15 /* ASCollectionViewFlowLayoutInspector.h in CopyFiles */, F7CE6C3D1D2CDB3E00BE4C15 /* ASCollectionViewLayoutController.h in CopyFiles */, - F7CE6C3E1D2CDB3E00BE4C15 /* ASDealloc2MainObject.h in CopyFiles */, F7CE6C3F1D2CDB3E00BE4C15 /* ASEnvironment.h in CopyFiles */, F7CE6C401D2CDB3E00BE4C15 /* ASFlowLayoutController.h in CopyFiles */, F7CE6C411D2CDB3E00BE4C15 /* ASHighlightOverlayLayer.h in CopyFiles */, @@ -896,7 +890,6 @@ 058D09FD195D050800B7D73C /* _ASAsyncTransactionGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASAsyncTransactionGroup.h; sourceTree = ""; }; 058D09FE195D050800B7D73C /* _ASAsyncTransactionGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _ASAsyncTransactionGroup.m; sourceTree = ""; }; 058D09FF195D050800B7D73C /* UIView+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+ASConvenience.h"; sourceTree = ""; }; - 058D0A02195D050800B7D73C /* _AS-objc-internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "_AS-objc-internal.h"; sourceTree = ""; }; 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCoreAnimationExtras.h; sourceTree = ""; }; 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCoreAnimationExtras.mm; sourceTree = ""; }; 058D0A05195D050800B7D73C /* _ASPendingState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASPendingState.h; sourceTree = ""; }; @@ -921,8 +914,6 @@ 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNodeWordKernerTests.mm; sourceTree = ""; }; 058D0A43195D058D00B7D73C /* ASAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAssert.h; sourceTree = ""; }; 058D0A44195D058D00B7D73C /* ASBaseDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBaseDefines.h; sourceTree = ""; }; - 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASDealloc2MainObject.h; path = ../Details/ASDealloc2MainObject.h; sourceTree = ""; }; - 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASDealloc2MainObject.m; path = ../Details/ASDealloc2MainObject.m; sourceTree = ""; }; 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASSnapshotTestCase.m; sourceTree = ""; }; 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionNode.h; sourceTree = ""; }; @@ -1508,8 +1499,6 @@ 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */, 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */, 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.mm */, - 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */, - 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */, 698548611CA9E025008A345F /* ASEnvironment.h */, 9CFFC6BD1CCAC52B006A6476 /* ASEnvironment.mm */, 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */, @@ -1583,7 +1572,6 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( - 058D0A02195D050800B7D73C /* _AS-objc-internal.h */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, @@ -1823,7 +1811,6 @@ 698C8B621CAB49FC0052DC3F /* ASLayoutElementExtensibility.h in Headers */, 698548641CA9E025008A345F /* ASEnvironment.h in Headers */, AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */, - B35062481B010EFD0018CF92 /* _AS-objc-internal.h in Headers */, 69F10C871C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h in Headers */, B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */, 9C70F20D1CDBE9CB007D6C76 /* ASDefaultPlayButton.h in Headers */, @@ -1884,7 +1871,6 @@ B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */, B35061F81B010EFD0018CF92 /* ASControlNode.h in Headers */, B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, - B35062191B010EFD0018CF92 /* ASDealloc2MainObject.h in Headers */, 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, 68FC85EA1CE29C7D00EDD713 /* ASVisibilityProtocols.h in Headers */, 69C4CAF61DA3147000B1EC9B /* ASLayoutElementStylePrivate.h in Headers */, @@ -2238,7 +2224,6 @@ 058D0A13195D050800B7D73C /* ASControlNode.mm in Sources */, 464052211A3F83C40061C0BA /* ASDataController.mm in Sources */, B30BF6531C5964B0004FCD53 /* ASLayoutManager.m in Sources */, - 05A6D05B19D0EB64002DD95E /* ASDealloc2MainObject.m in Sources */, ACF6ED211B17843500DA7C62 /* ASDimension.mm in Sources */, 8021EC1E1D2B00B100799119 /* UIImage+ASConvenience.m in Sources */, 058D0A28195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm in Sources */, @@ -2432,7 +2417,6 @@ B35061F91B010EFD0018CF92 /* ASControlNode.mm in Sources */, 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.m in Sources */, B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */, - B350621A1B010EFD0018CF92 /* ASDealloc2MainObject.m in Sources */, 767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.m in Sources */, 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index 5ecac71efc..a71c3b7438 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -131,12 +131,6 @@ return nil; } -- (void)dealloc -{ - self.delegate = nil; - self.dataSource = nil; -} - #pragma mark ASDisplayNode - (void)didLoad @@ -226,7 +220,15 @@ _pendingState.delegate = delegate; } else { ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); - self.view.asyncDelegate = delegate; + + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASCollectionView *view = (ASCollectionView *)_view; + ASPerformBlockOnMainThread(^{ + view.asyncDelegate = delegate; + }); } } @@ -245,7 +247,14 @@ _pendingState.dataSource = dataSource; } else { ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); - self.view.asyncDataSource = dataSource; + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASCollectionView *view = (ASCollectionView *)_view; + ASPerformBlockOnMainThread(^{ + view.asyncDataSource = dataSource; + }); } } diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index cc870a9f79..891ae2089a 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -27,6 +27,25 @@ #import "ASSectionContext.h" #import "ASCollectionView+Undeprecated.h" +/** + * A macro to get self.collectionNode and assign it to a local variable, or return + * the given value if nil. + * + * Previously we would set ASCollectionNode's dataSource & delegate to nil + * during dealloc. However, our asyncDelegate & asyncDataSource must be set on the + * main thread, so if the node is deallocated off-main, we won't learn about the change + * until later on. Since our @c collectionNode parameter to delegate methods (e.g. + * collectionNode:didEndDisplayingItemWithNode:) is nonnull, it's important that we never + * unintentionally pass nil (this will crash in Swift, in production). So we can use + * this macro to ensure that our node is still alive before calling out to the user + * on its behalf. + */ +#define GET_COLLECTIONNODE_OR_RETURN(__var, __val) \ + ASCollectionNode *__var = self.collectionNode; \ + if (__var == nil) { \ + return __val; \ + } + /// What, if any, invalidation should we perform during the next -layoutSubviews. typedef NS_ENUM(NSUInteger, ASCollectionViewInvalidationStyle) { /// Perform no invalidation. @@ -302,10 +321,15 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)dealloc { + ASDisplayNodeAssertMainThread(); // Sometimes the UIKit classes can call back to their delegate even during deallocation, due to animation completion blocks etc. _isDeallocating = YES; [self setAsyncDelegate:nil]; [self setAsyncDataSource:nil]; + + // Data controller & range controller may own a ton of nodes, let's deallocate those off-main. + ASPerformBackgroundDeallocation(_dataController); + ASPerformBackgroundDeallocation(_rangeController); } #pragma mark - @@ -372,6 +396,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)setAsyncDataSource:(id)asyncDataSource { + // Changing super.dataSource will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong @@ -424,6 +451,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)setAsyncDelegate:(id)asyncDelegate { + // Changing super.delegate will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong @@ -814,7 +844,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); if (_asyncDelegateFlags.collectionNodeWillDisplayItem) { - [_asyncDelegate collectionNode:self.collectionNode willDisplayItemWithNode:cellNode]; + if (ASCollectionNode *collectionNode = self.collectionNode) { + [_asyncDelegate collectionNode:collectionNode willDisplayItemWithNode:cellNode]; + } } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -837,7 +869,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { - [_asyncDelegate collectionNode:self.collectionNode didEndDisplayingItemWithNode:cellNode]; + if (ASCollectionNode *collectionNode = self.collectionNode) { + [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; + } } else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -856,27 +890,30 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); - [_asyncDelegate collectionNode:self.collectionNode willDisplaySupplementaryElementWithNode:node]; + [_asyncDelegate collectionNode:collectionNode willDisplaySupplementaryElementWithNode:node]; } } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); - [_asyncDelegate collectionNode:self.collectionNode didEndDisplayingSupplementaryElementWithNode:node]; + [_asyncDelegate collectionNode:collectionNode didEndDisplayingSupplementaryElementWithNode:node]; } } - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldSelectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldSelectItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldSelectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewShouldSelectItem) { #pragma clang diagnostic push @@ -890,9 +927,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidSelectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didSelectItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didSelectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidSelectItem) { #pragma clang diagnostic push @@ -905,9 +943,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldDeselectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldDeselectItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldDeselectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewShouldDeselectItem) { #pragma clang diagnostic push @@ -921,9 +960,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidDeselectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didDeselectItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didDeselectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidDeselectItem) { #pragma clang diagnostic push @@ -936,9 +976,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldHighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldHighlightItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldHighlightItemAtIndexPath:indexPath]; } else { return YES; } @@ -954,9 +995,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidHighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didHighlightItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didHighlightItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidHighlightItem) { #pragma clang diagnostic push @@ -969,9 +1011,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidUnhighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didUnhighlightItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didUnhighlightItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidUnhighlightItem) { #pragma clang diagnostic push @@ -984,9 +1027,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldShowMenuForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldShowMenuForItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldShowMenuForItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewShouldShowMenuForItem) { #pragma clang diagnostic push @@ -1000,9 +1044,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.collectionNodeCanPerformActionForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode canPerformAction:action forItemAtIndexPath:indexPath sender:sender]; + return [_asyncDelegate collectionNode:collectionNode canPerformAction:action forItemAtIndexPath:indexPath sender:sender]; } } else if (_asyncDelegateFlags.collectionViewCanPerformActionForItem) { #pragma clang diagnostic push @@ -1016,9 +1061,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView performAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.collectionNodePerformActionForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode performAction:action forItemAtIndexPath:indexPath sender:sender]; + [_asyncDelegate collectionNode:collectionNode performAction:action forItemAtIndexPath:indexPath sender:sender]; } } else if (_asyncDelegateFlags.collectionViewPerformActionForItem) { #pragma clang diagnostic push @@ -1183,7 +1229,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; // if the delegate does not respond to this method, there is no point in starting to fetch BOOL canFetch = _asyncDelegateFlags.collectionNodeWillBeginBatchFetch || _asyncDelegateFlags.collectionViewWillBeginBatchFetch; if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionNode) { - return [_asyncDelegate shouldBatchFetchForCollectionNode:self.collectionNode]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + return [_asyncDelegate shouldBatchFetchForCollectionNode:collectionNode]; } else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1229,7 +1276,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; [_batchContext beginBatchFetching]; if (_asyncDelegateFlags.collectionNodeWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [_asyncDelegate collectionNode:self.collectionNode willBeginBatchFetchWithContext:_batchContext]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + [_asyncDelegate collectionNode:collectionNode willBeginBatchFetchWithContext:_batchContext]; }); } else if (_asyncDelegateFlags.collectionViewWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -1248,9 +1296,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASCellNodeBlock block = nil; if (_asyncDataSourceFlags.collectionNodeNodeBlockForItem) { - block = [_asyncDataSource collectionNode:self.collectionNode nodeBlockForItemAtIndexPath:indexPath]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + block = [_asyncDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]; } else if (_asyncDataSourceFlags.collectionNodeNodeForItem) { - ASCellNode *node = [_asyncDataSource collectionNode:self.collectionNode nodeForItemAtIndexPath:indexPath]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + ASCellNode *node = [_asyncDataSource collectionNode:collectionNode nodeForItemAtIndexPath:indexPath]; if ([node isKindOfClass:[ASCellNode class]]) { block = ^{ return node; @@ -1304,7 +1354,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section { if (_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection) { - return [_asyncDataSource collectionNode:self.collectionNode numberOfItemsInSection:section]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); + return [_asyncDataSource collectionNode:collectionNode numberOfItemsInSection:section]; } else if (_asyncDataSourceFlags.collectionViewNumberOfItemsInSection) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1317,7 +1368,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if (_asyncDataSourceFlags.numberOfSectionsInCollectionNode) { - return [_asyncDataSource numberOfSectionsInCollectionNode:self.collectionNode]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); + return [_asyncDataSource numberOfSectionsInCollectionNode:collectionNode]; } else if (_asyncDataSourceFlags.numberOfSectionsInCollectionView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1339,7 +1391,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASCellNode *node = nil; if (_asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) { - node = [_asyncDataSource collectionNode:self.collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, [[ASCellNode alloc] init] ); + node = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } else if (_asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1363,6 +1416,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section { + if (_asyncDataSource == nil) { + return 0; + } + return [self.layoutInspector collectionView:self supplementaryNodesOfKind:kind inSection:section]; } @@ -1372,7 +1429,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; id context = nil; if (_asyncDataSourceFlags.collectionNodeContextForSection) { - context = [_asyncDataSource collectionNode:self.collectionNode contextForSection:section]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + context = [_asyncDataSource collectionNode:collectionNode contextForSection:section]; } if (context != nil) { diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 18751b1120..289b6246dd 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -14,7 +14,6 @@ #import #import -#import #import #import #import @@ -106,7 +105,7 @@ extern NSInteger const ASDefaultDrawingPriority; * */ -@interface ASDisplayNode : ASDealloc2MainObject +@interface ASDisplayNode : NSObject /** @name Initializing a node object */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 804814b689..d22bd060c9 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -427,7 +427,8 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (void)dealloc { - ASDisplayNodeAssertMainThread(); + _flags.isDeallocating = YES; + // Synchronous nodes may not be able to call the hierarchy notifications, so only enforce for regular nodes. ASDisplayNodeAssert(_flags.synchronous || !ASInterfaceStateIncludesVisible(_interfaceState), @"Node should always be marked invisible before deallocating. Node: %@", self); @@ -443,15 +444,123 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) for (ASDisplayNode *subnode in _subnodes) [subnode __setSupernode:nil]; - _view = nil; + // Trampoline any UIKit ivars' deallocation to main + if (ASDisplayNodeThreadIsMain() == NO) { + [self _scheduleIvarsForMainDeallocation]; + } + _subnodes = nil; - _layer = nil; // TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway. [self __setSupernode:nil]; - _pendingViewState = nil; +} - _pendingDisplayNodes = nil; +- (void)_scheduleIvarsForMainDeallocation +{ + /** + * UIKit components must be deallocated on the main thread. We use this shared + * run loop queue to gradually deallocate them across many turns of the main run loop. + */ + static ASRunLoopQueue *queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { }]; + queue.batchSize = 10; + }); + + NSValue *ivarsObj = [[self class] _ivarsThatMayNeedMainDeallocation]; + + // Unwrap the ivar array + unsigned int count = 0; + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + Ivar ivars[count]; + [ivarsObj getValue:ivars]; + + for (Ivar ivar : ivars) { + id value = object_getIvar(self, ivar); + if (ASClassRequiresMainThreadDeallocation(object_getClass(value))) { + LOG(@"Trampolining ivar '%s' value %@ for main deallocation.", ivar_getName(ivar), value); + [queue enqueue:value]; + } else { + LOG(@"Not trampolining ivar '%s' value %@.", ivar_getName(ivar), value); + } + } +} + +/** + * Returns an NSValue-wrapped array of all the ivars in this class or its superclasses + * up through ASDisplayNode, that we expect may need to be deallocated on main. + * + * This method caches its results. + */ ++ (NSValue/*<[Ivar]>*/ * _Nonnull)_ivarsThatMayNeedMainDeallocation +{ + static NSCache *ivarsCache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ivarsCache = [[NSCache alloc] init]; + }); + + NSValue *result = [ivarsCache objectForKey:self]; + if (result != nil) { + return result; + } + + // Cache miss. + unsigned int resultCount = 0; + static const int kMaxDealloc2MainIvarsPerClassTree = 64; + Ivar resultIvars[kMaxDealloc2MainIvarsPerClassTree]; + + // Get superclass results first. + Class c = class_getSuperclass(self); + if (c != [NSObject class]) { + NSValue *ivarsObj = [c _ivarsThatMayNeedMainDeallocation]; + // Unwrap the ivar array and append it to our working array + unsigned int count = 0; + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + ASDisplayNodeCAssert(resultCount + count < kMaxDealloc2MainIvarsPerClassTree, @"More than %d dealloc2main ivars are not supported. Count: %d", kMaxDealloc2MainIvarsPerClassTree, resultCount + count); + [ivarsObj getValue:resultIvars + resultCount]; + resultCount += count; + } + + // Now gather ivars from this particular class. + unsigned int allMyIvarsCount; + Ivar *allMyIvars = class_copyIvarList(self, &allMyIvarsCount); + + for (NSUInteger i = 0; i < allMyIvarsCount; i++) { + Ivar ivar = allMyIvars[i]; + const char *type = ivar_getTypeEncoding(ivar); + + if (strcmp(type, @encode(id)) == 0) { + // If it's `id` we have to include it just in case. + resultIvars[resultCount] = ivar; + resultCount += 1; + LOG(@"Marking ivar '%s' for possible main deallocation due to type id", ivar_getName(ivar)); + } else { + // If it's an ivar with a static type, check the type. + Class c = ASGetClassFromType(type); + if (ASClassRequiresMainThreadDeallocation(c)) { + resultIvars[resultCount] = ivar; + resultCount += 1; + LOG(@"Marking ivar '%s' for main deallocation due to class %@", ivar_getName(ivar), c); + } else { + LOG(@"Skipping ivar '%s' for main deallocation.", ivar_getName(ivar)); + } + } + } + free(allMyIvars); + + // Encode the type (array of Ivars) into a string and wrap it in an NSValue + char arrayType[32]; + snprintf(arrayType, 32, "[%u^{objc_ivar}]", resultCount); + result = [NSValue valueWithBytes:resultIvars objCType:arrayType]; + + [ivarsCache setObject:result forKey:self]; + return result; } #pragma mark - Core @@ -539,7 +648,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) { ASDN::MutexLocker l(__instanceLock__); - if (self._isDeallocating) { + if (_flags.isDeallocating) { return; } diff --git a/AsyncDisplayKit/ASTableNode.mm b/AsyncDisplayKit/ASTableNode.mm index 48c62cbe9c..04be9876a5 100644 --- a/AsyncDisplayKit/ASTableNode.mm +++ b/AsyncDisplayKit/ASTableNode.mm @@ -123,12 +123,6 @@ } } -- (void)dealloc -{ - self.delegate = nil; - self.dataSource = nil; -} - - (ASTableView *)view { return (ASTableView *)[super view]; @@ -195,7 +189,15 @@ _pendingState.delegate = delegate; } else { ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); - self.view.asyncDelegate = delegate; + + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASTableView *view = (ASTableView *)_view; + ASPerformBlockOnMainThread(^{ + view.asyncDelegate = delegate; + }); } } @@ -214,7 +216,15 @@ _pendingState.dataSource = dataSource; } else { ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); - self.view.asyncDataSource = dataSource; + + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASTableView *view = (ASTableView *)_view; + ASPerformBlockOnMainThread(^{ + view.asyncDataSource = dataSource; + }); } } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index aca1855e7b..99220dbad5 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -31,6 +31,15 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) +/** + * See note at the top of ASCollectionView.mm near declaration of macro GET_COLLECTIONNODE_OR_RETURN + */ +#define GET_TABLENODE_OR_RETURN(__var, __val) \ + ASTableNode *__var = self.tableNode; \ + if (__var == nil) { \ + return __val; \ + } + #pragma mark - #pragma mark ASCellNode<->UITableViewCell bridging. @@ -282,10 +291,15 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)dealloc { + ASDisplayNodeAssertMainThread(); // Sometimes the UIKit classes can call back to their delegate even during deallocation. _isDeallocating = YES; [self setAsyncDelegate:nil]; [self setAsyncDataSource:nil]; + + // Data controller & range controller may own a ton of nodes, let's deallocate those off-main + ASPerformBackgroundDeallocation(_dataController); + ASPerformBackgroundDeallocation(_rangeController); } #pragma mark - @@ -310,6 +324,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)setAsyncDataSource:(id)asyncDataSource { + // Changing super.dataSource will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong @@ -354,6 +371,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)setAsyncDelegate:(id)asyncDelegate { + // Changing super.delegate will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong @@ -792,7 +812,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); if (_asyncDelegateFlags.tableNodeWillDisplayNodeForRow) { - [_asyncDelegate tableNode:self.tableNode willDisplayRowWithNode:cellNode]; + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + [_asyncDelegate tableNode:tableNode willDisplayRowWithNode:cellNode]; } else if (_asyncDelegateFlags.tableViewWillDisplayNodeForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -821,7 +842,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); if (_asyncDelegateFlags.tableNodeDidEndDisplayingNodeForRow) { - [_asyncDelegate tableNode:self.tableNode didEndDisplayingRowWithNode:cellNode]; + if (ASTableNode *tableNode = self.tableNode) { + [_asyncDelegate tableNode:tableNode didEndDisplayingRowWithNode:cellNode]; + } } else if (_asyncDelegateFlags.tableViewDidEndDisplayingNodeForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -837,12 +860,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeWillSelectRow) { + GET_TABLENODE_OR_RETURN(tableNode, indexPath); NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; // If this item was is gone, just let the table view do its default behavior and select. if (result == nil) { return indexPath; } else { - result = [_asyncDelegate tableNode:self.tableNode willSelectRowAtIndexPath:result]; + result = [_asyncDelegate tableNode:tableNode willSelectRowAtIndexPath:result]; result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; return result; } @@ -859,9 +883,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidSelectRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate tableNode:self.tableNode didSelectRowAtIndexPath:indexPath]; + [_asyncDelegate tableNode:tableNode didSelectRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidSelectRow) { #pragma clang diagnostic push @@ -874,12 +899,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeWillDeselectRow) { + GET_TABLENODE_OR_RETURN(tableNode, indexPath); NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; // If this item was is gone, just let the table view do its default behavior and deselect. if (result == nil) { return indexPath; } else { - result = [_asyncDelegate tableNode:self.tableNode willDeselectRowAtIndexPath:result]; + result = [_asyncDelegate tableNode:tableNode willDeselectRowAtIndexPath:result]; result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; return result; } @@ -895,9 +921,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidDeselectRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate tableNode:self.tableNode didDeselectRowAtIndexPath:indexPath]; + [_asyncDelegate tableNode:tableNode didDeselectRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidDeselectRow) { #pragma clang diagnostic push @@ -910,9 +937,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeShouldHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode shouldHighlightRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode shouldHighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewShouldHighlightRow) { #pragma clang diagnostic push @@ -926,9 +954,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode didHighlightRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode didHighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidHighlightRow) { #pragma clang diagnostic push @@ -941,9 +970,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode didUnhighlightRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode didUnhighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidUnhighlightRow) { #pragma clang diagnostic push @@ -956,9 +986,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeShouldShowMenuForRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode shouldShowMenuForRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode shouldShowMenuForRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewShouldShowMenuForRow) { #pragma clang diagnostic push @@ -972,9 +1003,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (BOOL)tableView:(UITableView *)tableView canPerformAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.tableNodeCanPerformActionForRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; + return [_asyncDelegate tableNode:tableNode canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; } } else if (_asyncDelegateFlags.tableViewCanPerformActionForRow) { #pragma clang diagnostic push @@ -988,9 +1020,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView performAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.tableNodePerformActionForRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate tableNode:self.tableNode performAction:action forRowAtIndexPath:indexPath withSender:sender]; + [_asyncDelegate tableNode:tableNode performAction:action forRowAtIndexPath:indexPath withSender:sender]; } } else if (_asyncDelegateFlags.tableViewPerformActionForRow) { #pragma clang diagnostic push @@ -1119,7 +1152,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // if the delegate does not respond to this method, there is no point in starting to fetch BOOL canFetch = _asyncDelegateFlags.tableNodeWillBeginBatchFetch || _asyncDelegateFlags.tableViewWillBeginBatchFetch; if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableNode) { - return [_asyncDelegate shouldBatchFetchForTableNode:self.tableNode]; + GET_TABLENODE_OR_RETURN(tableNode, NO); + return [_asyncDelegate shouldBatchFetchForTableNode:tableNode]; } else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1165,7 +1199,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_batchContext beginBatchFetching]; if (_asyncDelegateFlags.tableNodeWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [_asyncDelegate tableNode:self.tableNode willBeginBatchFetchWithContext:_batchContext]; + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + [_asyncDelegate tableNode:tableNode willBeginBatchFetchWithContext:_batchContext]; }); } else if (_asyncDelegateFlags.tableViewWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -1401,9 +1436,14 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASCellNodeBlock block = nil; if (_asyncDataSourceFlags.tableNodeNodeBlockForRow) { - block = [_asyncDataSource tableNode:self.tableNode nodeBlockForRowAtIndexPath:indexPath]; + if (ASTableNode *tableNode = self.tableNode) { + block = [_asyncDataSource tableNode:tableNode nodeBlockForRowAtIndexPath:indexPath]; + } } else if (_asyncDataSourceFlags.tableNodeNodeForRow) { - ASCellNode *node = [_asyncDataSource tableNode:self.tableNode nodeForRowAtIndexPath:indexPath]; + ASCellNode *node = nil; + if (ASTableNode *tableNode = self.tableNode) { + node = [_asyncDataSource tableNode:tableNode nodeForRowAtIndexPath:indexPath]; + } if ([node isKindOfClass:[ASCellNode class]]) { block = ^{ return node; @@ -1453,7 +1493,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { ASSizeRange constrainedSize = kInvalidSizeRange; if (_asyncDelegateFlags.tableNodeConstrainedSizeForRow) { - ASSizeRange delegateConstrainedSize = [_asyncDelegate tableNode:self.tableNode constrainedSizeForRowAtIndexPath:indexPath]; + GET_TABLENODE_OR_RETURN(tableNode, constrainedSize); + ASSizeRange delegateConstrainedSize = [_asyncDelegate tableNode:tableNode constrainedSizeForRowAtIndexPath:indexPath]; // ignore widths in the returned size range (for TableView) constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.min.height), CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.max.height)); @@ -1475,7 +1516,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section { if (_asyncDataSourceFlags.tableNodeNumberOfRowsInSection) { - return [_asyncDataSource tableNode:self.tableNode numberOfRowsInSection:section]; + GET_TABLENODE_OR_RETURN(tableNode, 0); + return [_asyncDataSource tableNode:tableNode numberOfRowsInSection:section]; } else if (_asyncDataSourceFlags.tableViewNumberOfRowsInSection) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1489,7 +1531,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if (_asyncDataSourceFlags.numberOfSectionsInTableNode) { - return [_asyncDataSource numberOfSectionsInTableNode:self.tableNode]; + GET_TABLENODE_OR_RETURN(tableNode, 0); + return [_asyncDataSource numberOfSectionsInTableNode:tableNode]; } else if (_asyncDataSourceFlags.numberOfSectionsInTableView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" diff --git a/AsyncDisplayKit/ASViewController.mm b/AsyncDisplayKit/ASViewController.mm index 606f667c10..de8b5dc3ac 100644 --- a/AsyncDisplayKit/ASViewController.mm +++ b/AsyncDisplayKit/ASViewController.mm @@ -19,6 +19,7 @@ #import "ASTraitCollection.h" #import "ASEnvironmentInternal.h" #import "ASRangeControllerUpdateRangeProtocol+Beta.h" +#import "ASInternalHelpers.h" #define AS_LOG_VISIBILITY_CHANGES 0 @@ -61,6 +62,11 @@ return self; } +- (void)dealloc +{ + ASPerformBackgroundDeallocation(_node); +} + - (void)loadView { ASDisplayNodeAssertTrue(!_node.layerBacked); diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 21f8b62f6f..00af36fde6 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -11,7 +11,6 @@ #pragma once #import -#import #import #import #import @@ -114,7 +113,7 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; * For each data updating, the corresponding methods in delegate will be called. */ @protocol ASFlowLayoutControllerDataSource; -@interface ASDataController : ASDealloc2MainObject +@interface ASDataController : NSObject - (instancetype)initWithDataSource:(id)dataSource eventLog:(nullable ASEventLog *)eventLog NS_DESIGNATED_INITIALIZER; diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 9c1b0913ef..db4a499b2d 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -1057,26 +1057,6 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; ASMoveElementInTwoDimensionalArray(_completedNodes[ASDataControllerRowNodeKind], indexPath, newIndexPath); } -#pragma mark - Dealloc - -- (void)dealloc -{ - ASDisplayNodeAssertMainThread(); - for (NSMutableArray *sections in [_completedNodes objectEnumerator]) { - for (NSArray *section in sections) { - for (ASCellNode *node in section) { - if (node.isNodeLoaded) { - if (node.layerBacked) { - [node.layer removeFromSuperlayer]; - } else { - [node.view removeFromSuperview]; - } - } - } - } - } -} - @end #if AS_MEASURE_AVOIDED_DATACONTROLLER_WORK diff --git a/AsyncDisplayKit/Details/ASDealloc2MainObject.h b/AsyncDisplayKit/Details/ASDealloc2MainObject.h deleted file mode 100644 index 652d42aa3a..0000000000 --- a/AsyncDisplayKit/Details/ASDealloc2MainObject.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// ASDealloc2MainObject.h -// AsyncDisplayKit -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * _OBJC_SUPPORTED_INLINE_REFCNT_WITH_DEALLOC2MAIN permits classes to implement their own reference counting and enforce - * deallocation on the main thread, but requires manual reference counting. This superclass exposes such functionality - * to ARC-enabled classes. - */ -@interface ASDealloc2MainObject : NSObject - -- (BOOL)_isDeallocating; - -@end - -NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Details/ASDealloc2MainObject.m b/AsyncDisplayKit/Details/ASDealloc2MainObject.m deleted file mode 100644 index 243e9b17d0..0000000000 --- a/AsyncDisplayKit/Details/ASDealloc2MainObject.m +++ /dev/null @@ -1,34 +0,0 @@ -// -// ASDealloc2MainObject.m -// AsyncDisplayKit -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. -// - -#import "ASDealloc2MainObject.h" - -#import - -#import "_AS-objc-internal.h" - -#if __has_feature(objc_arc) -#error This file must be compiled without ARC. Use -fno-objc-arc. -#endif - -@interface ASDealloc2MainObject () -{ - @private - int _retainCount; -} -@end - -@implementation ASDealloc2MainObject - -#if !__has_feature(objc_arc) -_OBJC_SUPPORTED_INLINE_REFCNT_WITH_DEALLOC2MAIN(_retainCount); -#endif - -@end diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index dc312e0286..5d3ebc9484 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -30,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN * "working ranges" to trigger network calls and rendering, and is responsible for driving asynchronous layout of cells. * This includes cancelling those asynchronous operations as cells fall outside of the working ranges. */ -@interface ASRangeController : ASDealloc2MainObject +@interface ASRangeController : NSObject { id _layoutController; __weak id _dataSource; diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index a7025a2db2..bbad40d6f0 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -13,7 +13,6 @@ // These methods must never be called or overridden by other classes. // -#import "_AS-objc-internal.h" #import "ASDisplayNode.h" #import "ASThread.h" diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 67bb2cd35c..16720a6020 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -14,7 +14,6 @@ // #import -#import "_AS-objc-internal.h" #import "ASDisplayNode.h" #import "ASThread.h" #import "_ASTransitionContext.h" @@ -103,6 +102,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo unsigned isExitingHierarchy:1; unsigned isInHierarchy:1; unsigned visibilityNotificationsDisabled:VISIBILITY_NOTIFICATIONS_DISABLED_BITS; + unsigned isDeallocating:1; } _flags; @protected diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index 7c565f1cbe..ec650bbadc 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -12,6 +12,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + ASDISPLAYNODE_EXTERN_C_BEGIN BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector); @@ -41,6 +43,10 @@ CGFloat ASCeilPixelValue(CGFloat f); CGFloat ASRoundPixelValue(CGFloat f); +BOOL ASClassRequiresMainThreadDeallocation(Class _Nullable c); + +Class _Nullable ASGetClassFromType(const char *type); + ASDISPLAYNODE_EXTERN_C_END ASDISPLAYNODE_INLINE BOOL ASImageAlphaInfoIsOpaque(CGImageAlphaInfo info) { @@ -81,3 +87,5 @@ ASDISPLAYNODE_INLINE void ASBoundsAndPositionForFrame(CGRect rect, CGPoint origi @interface NSIndexPath (ASInverseComparison) - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath; @end + +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.m b/AsyncDisplayKit/Private/ASInternalHelpers.m index 04c9972b62..4a78f14d65 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.m +++ b/AsyncDisplayKit/Private/ASInternalHelpers.m @@ -84,6 +84,48 @@ void ASPerformBackgroundDeallocation(id object) [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object]; } +BOOL ASClassRequiresMainThreadDeallocation(Class class) +{ + if (class == [UIImage class] || class == [UIColor class]) { + return NO; + } + + if ([class isSubclassOfClass:[UIResponder class]] + || [class isSubclassOfClass:[CALayer class]] + || [class isSubclassOfClass:[UIGestureRecognizer class]]) { + return YES; + } + + const char *name = class_getName(class); + if (strncmp(name, "UI", 2) == 0 || strncmp(name, "AV", 2) == 0 || strncmp(name, "CA", 2) == 0) { + return YES; + } + + return NO; +} + +Class _Nullable ASGetClassFromType(const char *type) +{ + // Class types all start with @" + if (strncmp(type, "@\"", 2) != 0) { + return nil; + } + + // Ensure length >= 3 + size_t typeLength = strlen(type); + if (typeLength < 3) { + ASDisplayNodeCFailAssert(@"Got invalid type-encoding: %s", type); + return nil; + } + + // Copy type[2..(end-1)]. So @"UIImage" -> UIImage + size_t resultLength = typeLength - 3; + char className[resultLength + 1]; + strncpy(className, type + 2, resultLength); + className[resultLength] = '\0'; + return objc_getClass(className); +} + CGFloat ASScreenScale() { static CGFloat __scale = 0.0; diff --git a/AsyncDisplayKit/Private/_AS-objc-internal.h b/AsyncDisplayKit/Private/_AS-objc-internal.h deleted file mode 100644 index 535c212e5e..0000000000 --- a/AsyncDisplayKit/Private/_AS-objc-internal.h +++ /dev/null @@ -1,473 +0,0 @@ -// -// _AS-objc-internal.h -// AsyncDisplayKit -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. -// - -/* - * Copyright (c) 2009 Apple Inc. All Rights Reserved. - * - * @APPLE_LICENSE_HEADER_START@ - * - * This file contains Original Code and/or Modifications of Original Code - * as defined in and that are subject to the Apple Public Source License - * Version 2.0 (the 'License'). You may not use this file except in - * compliance with the License. Please obtain a copy of the License at - * http://www.opensource.apple.com/apsl/ and read it before using this - * file. - * - * The Original Code and all software distributed under the License are - * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER - * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, - * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. - * Please see the License for the specific language governing rights and - * limitations under the License. - * - * @APPLE_LICENSE_HEADER_END@ - */ - -#ifndef _OBJC_INTERNAL_H -#define _OBJC_INTERNAL_H - -/* - * WARNING DANGER HAZARD BEWARE EEK - * - * Everything in this file is for Apple Internal use only. - * These will change in arbitrary OS updates and in unpredictable ways. - * When your program breaks, you get to keep both pieces. - */ - -/* - * objc-internal.h: Private SPI for use by other system frameworks. - */ - -#include -#include -#include -#include - -__BEGIN_DECLS - -// In-place construction of an Objective-C class. -OBJC_EXPORT Class objc_initializeClassPair(Class superclass_gen, const char *name, Class cls_gen, Class meta_gen) - __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_0); - -#if __OBJC2__ && __LP64__ -// Register a tagged pointer class. -OBJC_EXPORT void _objc_insert_tagged_isa(unsigned char slotNumber, Class isa) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); -#endif - -// Batch object allocation using malloc_zone_batch_malloc(). -OBJC_EXPORT unsigned class_createInstances(Class cls, size_t extraBytes, - id *results, unsigned num_requested) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3) - OBJC_ARC_UNAVAILABLE; - -// Get the isa pointer written into objects just before being freed. -OBJC_EXPORT Class _objc_getFreedObjectClass(void) - __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0); - -// Substitute receiver for messages to nil. -// Not supported for all messages to nil. -OBJC_EXPORT id _objc_setNilReceiver(id newNilReceiver) - __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_NA); -OBJC_EXPORT id _objc_getNilReceiver(void) - __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_NA); - -// Return NO if no instance of `cls` has ever owned an associative reference. -OBJC_EXPORT BOOL class_instancesHaveAssociatedObjects(Class cls) - __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_0); - -// Return YES if GC is on and `object` is a GC allocation. -OBJC_EXPORT BOOL objc_isAuto(id object) - __OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_NA); - -// env NSObjCMessageLoggingEnabled -OBJC_EXPORT void instrumentObjcMessageSends(BOOL flag) - __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0); - -// Initializer called by libSystem -#if __OBJC2__ -OBJC_EXPORT void _objc_init(void) - __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); -#endif - -// GC startup callback from Foundation -OBJC_EXPORT malloc_zone_t *objc_collect_init(int (*callback)(void)) - __OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_NA); - -// Plainly-implemented GC barriers. Rosetta used to use these. -OBJC_EXPORT id objc_assign_strongCast_generic(id value, id *dest) - UNAVAILABLE_ATTRIBUTE; -OBJC_EXPORT id objc_assign_global_generic(id value, id *dest) - UNAVAILABLE_ATTRIBUTE; -OBJC_EXPORT id objc_assign_threadlocal_generic(id value, id *dest) - UNAVAILABLE_ATTRIBUTE; -OBJC_EXPORT id objc_assign_ivar_generic(id value, id dest, ptrdiff_t offset) - UNAVAILABLE_ATTRIBUTE; - -// Install missing-class callback. Used by the late unlamented ZeroLink. -OBJC_EXPORT void _objc_setClassLoader(BOOL (*newClassLoader)(const char *)) OBJC2_UNAVAILABLE; - -// Install handler for allocation failures. -// Handler may abort, or throw, or provide an object to return. -OBJC_EXPORT void _objc_setBadAllocHandler(id (*newHandler)(Class isa)) - __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); - -// This can go away when AppKit stops calling it (rdar://7811851) -#if __OBJC2__ -OBJC_EXPORT void objc_setMultithreaded (BOOL flag) - __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_0,__MAC_10_5, __IPHONE_NA,__IPHONE_NA); -#endif - -// Used by ExceptionHandling.framework -#if !__OBJC2__ -OBJC_EXPORT void _objc_error(id rcv, const char *fmt, va_list args) - __attribute__((noreturn)) - __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_0,__MAC_10_5, __IPHONE_NA,__IPHONE_NA); - -#endif - -// External Reference support. Used to support compaction. - -enum { - OBJC_XREF_STRONG = 1, - OBJC_XREF_WEAK = 2 -}; -typedef uintptr_t objc_xref_type_t; -typedef uintptr_t objc_xref_t; - -OBJC_EXPORT objc_xref_t _object_addExternalReference(id object, objc_xref_type_t type) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); -OBJC_EXPORT void _object_removeExternalReference(objc_xref_t xref) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); -OBJC_EXPORT id _object_readExternalReference(objc_xref_t xref) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); - -OBJC_EXPORT uintptr_t _object_getExternalHash(id object) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -// Instance-specific instance variable layout. - -OBJC_EXPORT void _class_setIvarLayoutAccessor(Class cls_gen, const uint8_t* (*accessor) (id object)) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_NA); -OBJC_EXPORT const uint8_t *_object_getIvarLayout(Class cls_gen, id object) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_NA); - -OBJC_EXPORT BOOL _class_usesAutomaticRetainRelease(Class cls) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -// Obsolete ARC conversions. - -// hack - remove and reinstate objc.h's definitions -#undef objc_retainedObject -#undef objc_unretainedObject -#undef objc_unretainedPointer -OBJC_EXPORT id objc_retainedObject(objc_objectptr_t pointer) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); -OBJC_EXPORT id objc_unretainedObject(objc_objectptr_t pointer) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); -OBJC_EXPORT objc_objectptr_t objc_unretainedPointer(id object) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); -#if __has_feature(objc_arc) -# define objc_retainedObject(o) ((__bridge_transfer id)(objc_objectptr_t)(o)) -# define objc_unretainedObject(o) ((__bridge id)(objc_objectptr_t)(o)) -# define objc_unretainedPointer(o) ((__bridge objc_objectptr_t)(id)(o)) -#else -# define objc_retainedObject(o) ((id)(objc_objectptr_t)(o)) -# define objc_unretainedObject(o) ((id)(objc_objectptr_t)(o)) -# define objc_unretainedPointer(o) ((objc_objectptr_t)(id)(o)) -#endif - -// API to only be called by root classes like NSObject or NSProxy - -OBJC_EXPORT -id -_objc_rootRetain(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -_objc_rootRelease(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -bool -_objc_rootReleaseWasZero(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -bool -_objc_rootTryRetain(id obj) -__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -bool -_objc_rootIsDeallocating(id obj) -__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -id -_objc_rootAutorelease(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -uintptr_t -_objc_rootRetainCount(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -id -_objc_rootInit(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -id -_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -id -_objc_rootAlloc(Class cls) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -_objc_rootDealloc(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -_objc_rootFinalize(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -malloc_zone_t * -_objc_rootZone(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -uintptr_t -_objc_rootHash(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void * -objc_autoreleasePoolPush(void) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -objc_autoreleasePoolPop(void *context) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - - -OBJC_EXPORT id objc_retain(id obj) - __asm__("_objc_retain") - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT void objc_release(id obj) - __asm__("_objc_release") - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT id objc_autorelease(id obj) - __asm__("_objc_autorelease") - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -// wraps objc_autorelease(obj) in a useful way when used with return values -OBJC_EXPORT -id -objc_autoreleaseReturnValue(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -// wraps objc_autorelease(objc_retain(obj)) in a useful way when used with return values -OBJC_EXPORT -id -objc_retainAutoreleaseReturnValue(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -// called ONLY by ARR by callers to undo the autorelease (if possible), otherwise objc_retain -OBJC_EXPORT -id -objc_retainAutoreleasedReturnValue(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -objc_storeStrong(id *location, id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -id -objc_retainAutorelease(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -// obsolete. -OBJC_EXPORT id objc_retain_autorelease(id obj) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -id -objc_loadWeakRetained(id *location) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -id -objc_initWeak(id *addr, id val) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -objc_destroyWeak(id *addr) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -objc_copyWeak(id *to, id *from) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -objc_moveWeak(id *to, id *from) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - - -OBJC_EXPORT -void -_objc_autoreleasePoolPrint(void) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT BOOL objc_should_deallocate(id object) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT void objc_clear_deallocating(id object) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - - -// to make CF link for now - -OBJC_EXPORT -void * -_objc_autoreleasePoolPush(void) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -OBJC_EXPORT -void -_objc_autoreleasePoolPop(void *context) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - - -// Extra @encode data for XPC, or NULL -OBJC_EXPORT const char *_protocol_getMethodTypeEncoding(Protocol *p, SEL sel, BOOL isRequiredMethod, BOOL isInstanceMethod) - __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); - - -// API to only be called by classes that provide their own reference count storage - -OBJC_EXPORT -void -_objc_deallocOnMainThreadHelper(void *context) - __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); - -// On async versus sync deallocation and the _dealloc2main flag -// -// Theory: -// -// If order matters, then code must always: [self dealloc]. -// If order doesn't matter, then always async should be safe. -// -// Practice: -// -// The _dealloc2main bit is set for GUI objects that may be retained by other -// threads. Once deallocation begins on the main thread, doing more async -// deallocation will at best cause extra UI latency and at worst cause -// use-after-free bugs in unretained delegate style patterns. Yes, this is -// extremely fragile. Yes, in the long run, developers should switch to weak -// references. -// -// Note is NOT safe to do any equality check against the result of -// dispatch_get_current_queue(). The main thread can and does drain more than -// one dispatch queue. That is why we call pthread_main_np(). -// - -typedef enum { - _OBJC_DEALLOC_OBJECT_NOW = 1, /* call [self dealloc] immediately. */ - _OBJC_DEALLOC_OBJECT_LATER = 2 /* call [self dealloc] on the main queue. */ -} _objc_object_disposition_t; - -#define _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC_STATEMENT(_rc_ivar, _logicStatement) \ - -(id)retain { \ - /* this will fail to compile if _rc_ivar is an unsigned type */ \ - int _retain_count_ivar_must_not_be_unsigned[0L - (__typeof__(_rc_ivar))-1] __attribute__((unused)); \ - __typeof__(_rc_ivar) _prev = __sync_fetch_and_add(&_rc_ivar, 2); \ - if (_prev < 0) { \ - __builtin_trap(); /* BUG: retain of over-released ref */ \ - } \ - if (_prev & 1) { \ - __builtin_trap(); /* BUG: retain of over-released ref (dealloc) */ \ - } \ - return self; \ - } \ - -(oneway void)release { \ - __typeof__(_rc_ivar) _prev = __sync_fetch_and_sub(&_rc_ivar, 2); \ - if (_prev > 0) { \ - return; \ - } else if (_prev < 0) { \ - __builtin_trap(); /* BUG: over-release */ \ - } \ - /* mark the object as deallocating. */ \ - if (!__sync_bool_compare_and_swap(&_rc_ivar, -2, 1)) { \ - __builtin_trap(); /* BUG: dangling ref did a retain */ \ - } \ - _objc_object_disposition_t fate = _logicStatement; \ - if (fate == _OBJC_DEALLOC_OBJECT_NOW) { \ - [self dealloc]; \ - } else if (fate == _OBJC_DEALLOC_OBJECT_LATER) { \ - dispatch_barrier_async_f(dispatch_get_main_queue(), self, \ - _objc_deallocOnMainThreadHelper); \ - } else { \ - __builtin_trap(); /* BUG: bogus fate value */ \ - } \ - } \ - -(NSUInteger)retainCount { \ - return (__atomic_load_n(&_rc_ivar, __ATOMIC_SEQ_CST) + 2) >> 1; \ - } \ - -(BOOL)_tryRetain { \ - __typeof__(_rc_ivar) _prev; \ - do { \ - _prev = __atomic_load_n(&_rc_ivar, __ATOMIC_SEQ_CST); \ - if (_prev & 1) { \ - return 0; \ - } else if (_prev == -2) { \ - return 0; \ - } else if (_prev < -2) { \ - __builtin_trap(); /* BUG: over-release elsewhere */ \ - } \ - } while ( ! __sync_bool_compare_and_swap(&_rc_ivar, _prev, _prev + 2)); \ - return 1; \ - } \ - -(BOOL)_isDeallocating { \ - __typeof__(_rc_ivar) _prev = __atomic_load_n(&_rc_ivar, __ATOMIC_SEQ_CST); \ - if (_prev == -2) { \ - return 1; \ - } else if (_prev < -2) { \ - __builtin_trap(); /* BUG: over-release elsewhere */ \ - } \ - return _prev & 1; \ - } - -#define _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, _dealloc2main) \ - _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC_STATEMENT(_rc_ivar, ( (_dealloc2main && !pthread_main_np()) ? _OBJC_DEALLOC_OBJECT_LATER : _OBJC_DEALLOC_OBJECT_NOW )) - -#define _OBJC_SUPPORTED_INLINE_REFCNT(_rc_ivar) _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, 0) -#define _OBJC_SUPPORTED_INLINE_REFCNT_WITH_DEALLOC2MAIN(_rc_ivar) _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, 1) - -__END_DECLS - -#endif diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index fcfa42c138..6f9c5c08af 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -26,6 +26,7 @@ #import "ASInsetLayoutSpec.h" #import "ASCenterLayoutSpec.h" #import "ASBackgroundLayoutSpec.h" +#import "ASInternalHelpers.h" // Conveniences for making nodes named a certain way #define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.debugName = @#n @@ -90,6 +91,11 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @property (nonatomic, copy) CGSize(^calculateSizeBlock)(ASTestDisplayNode *node, CGSize size); @property (nonatomic) BOOL hasFetchedData; +@property (nonatomic, nullable) UIGestureRecognizer *gestureRecognizer; +@property (nonatomic, nullable) id idGestureRecognizer; +@property (nonatomic, nullable) UIImage *bigImage; +@property (nonatomic, nullable) NSArray *randomProperty; + @property (nonatomic) BOOL displayRangeStateChangedToYES; @property (nonatomic) BOOL displayRangeStateChangedToNO; @@ -1046,23 +1052,47 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point XCTAssertNil(weakSubnode); } -- (void)testMainThreadDealloc +- (void)testThatUIKitDeallocationTrampoliningWorks { - __block BOOL didDealloc = NO; + NS_VALID_UNTIL_END_OF_SCOPE __weak UIGestureRecognizer *weakRecognizer = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak UIGestureRecognizer *weakIdRecognizer = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak UIView *weakView = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak CALayer *weakLayer = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak UIImage *weakImage = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak NSArray *weakArray = nil; + __block NS_VALID_UNTIL_END_OF_SCOPE ASTestDisplayNode *node = nil; + @autoreleasepool { + node = [[ASTestDisplayNode alloc] init]; + node.gestureRecognizer = [[UIGestureRecognizer alloc] init]; + node.idGestureRecognizer = [[UIGestureRecognizer alloc] init]; + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1000, 1000), YES, 1); + node.bigImage = UIGraphicsGetImageFromCurrentImageContext(); + node.randomProperty = @[ @"Hello, world!" ]; + UIGraphicsEndImageContext(); + weakImage = node.bigImage; + weakView = node.view; + weakLayer = node.layer; + weakArray = node.randomProperty; + weakIdRecognizer = node.idGestureRecognizer; + weakRecognizer = node.gestureRecognizer; + } [self executeOffThread:^{ - @autoreleasepool { - ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; - node.willDeallocBlock = ^(__unsafe_unretained ASDisplayNode *n){ - XCTAssertTrue([NSThread isMainThread], @"unexpected node dealloc %@ %@", n, [NSThread currentThread]); - didDealloc = YES; - }; - } + node = nil; }]; - // deallocation should be queued on the main runloop; give it a chance - ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return didDealloc; }); - XCTAssertTrue(didDealloc, @"unexpected node lifetime"); + XCTAssertNotNil(weakRecognizer, @"UIGestureRecognizer ivars should be deallocated on main."); + XCTAssertNotNil(weakIdRecognizer, @"UIGestureRecognizer-backed 'id' ivars should be deallocated on main."); + XCTAssertNotNil(weakView, @"UIView ivars should be deallocated on main."); + XCTAssertNotNil(weakLayer, @"CALayer ivars should be deallocated on main."); + XCTAssertNil(weakImage, @"UIImage ivars should be deallocated normally."); + XCTAssertNil(weakArray, @"NSArray ivars should be deallocated normally."); + XCTAssertNil(node); + + [self expectationForPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nonnull evaluatedObject, NSDictionary * _Nullable bindings) { + return (weakRecognizer == nil && weakIdRecognizer == nil && weakView == nil); + }] evaluatedWithObject:(id)kCFNull handler:nil]; + [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testSubnodes From 887f48cbdabed351c232aa51af9001a9e3fe0a1b Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sun, 20 Nov 2016 15:55:18 +0900 Subject: [PATCH 2/2] Add support for run loop queues with no handler, optimize --- AsyncDisplayKit/ASDisplayNode.mm | 2 +- AsyncDisplayKit/ASRunLoopQueue.h | 13 ++++++++++++- AsyncDisplayKit/ASRunLoopQueue.mm | 30 +++++++++++++++++------------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index d22bd060c9..2ab5cebd9f 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -464,7 +464,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) static ASRunLoopQueue *queue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { }]; + queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:nil]; queue.batchSize = 10; }); diff --git a/AsyncDisplayKit/ASRunLoopQueue.h b/AsyncDisplayKit/ASRunLoopQueue.h index 4eacc232da..cd9a572997 100644 --- a/AsyncDisplayKit/ASRunLoopQueue.h +++ b/AsyncDisplayKit/ASRunLoopQueue.h @@ -16,8 +16,19 @@ NS_ASSUME_NONNULL_BEGIN @interface ASRunLoopQueue : NSObject +/** + * Create a new queue with the given run loop and handler. + * + * @param runloop The run loop that will drive this queue. + * @param handlerBlock An optional block to be run for each enqueued object. + * + * @discussion You may pass @c nil for the handler if you simply want the objects to + * be retained at enqueue time, and released during the run loop step. This is useful + * for creating a "main deallocation queue", as @c ASDeallocQueue creates its own + * worker thread with its own run loop. + */ - (instancetype)initWithRunLoop:(CFRunLoopRef)runloop - andHandler:(void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; + andHandler:(nullable void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; - (void)enqueue:(ObjectType)object; diff --git a/AsyncDisplayKit/ASRunLoopQueue.mm b/AsyncDisplayKit/ASRunLoopQueue.mm index aac7ad1d17..7e50424a59 100644 --- a/AsyncDisplayKit/ASRunLoopQueue.mm +++ b/AsyncDisplayKit/ASRunLoopQueue.mm @@ -16,6 +16,7 @@ #import #import +#import #define ASRunLoopQueueLoggingEnabled 0 @@ -222,7 +223,12 @@ static void runLoopSourceCallback(void *info) { - (void)processQueue { - std::deque itemsToProcess = std::deque(); + BOOL hasExecutionBlock = (_queueConsumer != nil); + + // If we have an execution block, this vector will be populated, otherwise remains empty. + // This is to avoid needlessly retaining/releasing the objects if we don't have a block. + std::vector itemsToProcess; + BOOL isQueueDrained = NO; { ASDN::MutexLocker l(_internalQueueLock); @@ -235,25 +241,23 @@ static void runLoopSourceCallback(void *info) { ASProfilingSignpostStart(0, self); // Snatch the next batch of items. - NSUInteger totalNodeCount = _internalQueue.size(); - for (int i = 0; i < MIN(self.batchSize, totalNodeCount); i++) { - id node = _internalQueue[0]; - itemsToProcess.push_back(node); - _internalQueue.pop_front(); + auto firstItemToProcess = _internalQueue.cbegin(); + auto lastItemToProcess = MIN(_internalQueue.cend(), firstItemToProcess + self.batchSize); + + if (hasExecutionBlock) { + itemsToProcess = std::vector(firstItemToProcess, lastItemToProcess); } + _internalQueue.erase(firstItemToProcess, lastItemToProcess); if (_internalQueue.empty()) { isQueueDrained = YES; } } - unsigned long numberOfItems = itemsToProcess.size(); - for (int i = 0; i < numberOfItems; i++) { - if (isQueueDrained && i == numberOfItems - 1) { - _queueConsumer(itemsToProcess[i], YES); - } else { - _queueConsumer(itemsToProcess[i], isQueueDrained); - } + // itemsToProcess will be empty if _queueConsumer == nil so no need to check again. + auto itemsEnd = itemsToProcess.cend(); + for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) { + _queueConsumer(*iterator, isQueueDrained && iterator == itemsEnd - 1); } // If the queue is not fully drained yet force another run loop to process next batch of items