diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index ec5d4d482e..881cc251e1 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 */; }; @@ -383,8 +382,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, ); }; }; @@ -409,7 +406,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 */; }; @@ -546,7 +542,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 */; }; @@ -722,7 +717,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 */, @@ -889,7 +883,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 = ""; }; @@ -914,8 +907,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 = ""; }; @@ -1497,8 +1488,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 */, @@ -1572,7 +1561,7 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( - 058D0A02195D050800B7D73C /* _AS-objc-internal.h */, + 69C4CAF51DA3147000B1EC9B /* ASLayoutElementStylePrivate.h */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, @@ -1812,7 +1801,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 */, @@ -1872,7 +1860,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 */, @@ -2226,7 +2213,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 */, @@ -2419,7 +2405,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..09006e9a59 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -226,7 +226,14 @@ _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. + ASCollectionView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDelegate = delegate; + }); } } @@ -245,7 +252,13 @@ _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. + ASCollectionView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDataSource = dataSource; + }); } } diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index f80f476c2c..27143e6bae 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -300,10 +300,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 - @@ -365,6 +370,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 @@ -412,6 +420,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 @@ -1351,6 +1362,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]; } diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index dddc908239..97a7b8c206 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 29a0a5011d..2561202238 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -426,7 +426,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); @@ -442,15 +443,119 @@ 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; + NSAssert(sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count), @"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; + NSAssert(sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count), @"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 @@ -538,7 +643,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..604f5ce8b1 100644 --- a/AsyncDisplayKit/ASTableNode.mm +++ b/AsyncDisplayKit/ASTableNode.mm @@ -195,7 +195,14 @@ _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. + ASTableView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDelegate = delegate; + }); } } @@ -214,7 +221,14 @@ _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. + ASTableView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDataSource = dataSource; + }); } } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index a3b75fb5b4..51bd965fe9 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -278,10 +278,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 - @@ -301,6 +306,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 @@ -340,6 +348,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 diff --git a/AsyncDisplayKit/ASViewController.mm b/AsyncDisplayKit/ASViewController.mm index dfb41bfc7f..e3def4c26d 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 620d976044..48481e1ef3 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 8aa400cb54..a84bb5c8bd 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 f145abac7c..e00c336a80 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); @@ -37,6 +39,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) { @@ -77,3 +83,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 c9dae927ef..66e7516f66 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 126e5c3806..914a3570d0 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