From 840884272d4c448951c0186a8b832e0e7d0ffde1 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 5 Dec 2015 22:20:16 -0800 Subject: [PATCH] Introduced ASHierarchyState. Created ASDisplayNode+FrameworkPrivate.h. Fixed deadlock. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 6 + AsyncDisplayKit/ASCellNode.m | 2 +- AsyncDisplayKit/ASCollectionView.mm | 19 ++- AsyncDisplayKit/ASDisplayNode+Subclasses.h | 44 ------ AsyncDisplayKit/ASDisplayNode.mm | 140 +++++++++++------- AsyncDisplayKit/ASDisplayNodeExtras.h | 6 + AsyncDisplayKit/ASDisplayNodeExtras.mm | 9 +- AsyncDisplayKit/ASMultiplexImageNode.mm | 1 + AsyncDisplayKit/ASNetworkImageNode.mm | 5 +- AsyncDisplayKit/ASTableView.mm | 12 +- AsyncDisplayKit/ASViewController.m | 4 +- .../Details/ASRangeHandlerPreload.mm | 2 +- .../Details/ASRangeHandlerRender.mm | 2 +- .../Details/ASRangeHandlerVisible.mm | 2 +- AsyncDisplayKit/Details/_ASDisplayLayer.mm | 1 + AsyncDisplayKit/Details/_ASDisplayView.mm | 1 + .../Private/ASDisplayNode+AsyncDisplay.mm | 7 +- .../Private/ASDisplayNode+FrameworkPrivate.h | 109 ++++++++++++++ .../Private/ASDisplayNode+UIViewBridge.mm | 23 ++- .../Private/ASDisplayNodeInternal.h | 37 ++--- .../TextKit/ASTextNodeWordKerner.m | 2 +- 21 files changed, 281 insertions(+), 153 deletions(-) create mode 100644 AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 8708d2a031..a769a8fc6d 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -441,6 +441,8 @@ D785F6621A74327E00291744 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; D785F6631A74327E00291744 /* ASScrollNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.m */; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; + DE6EA3221C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; + DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; DECBD6E71BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6E91BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; @@ -725,6 +727,7 @@ D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = ""; }; + DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+FrameworkPrivate.h"; sourceTree = ""; }; DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1045,6 +1048,7 @@ 058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */, 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */, 058D0A0B195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm */, + DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */, 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */, @@ -1231,6 +1235,7 @@ 257754B11BEE44CD00737CA5 /* ASTextKitShadower.h in Headers */, 058D0A7B195D05F900B7D73C /* ASDisplayNodeInternal.h in Headers */, 0587F9BD1A7309ED00AFF0BA /* ASEditableTextNode.h in Headers */, + DE6EA3221C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */, 1950C4491A3BB5C1005C8279 /* ASEqualityHelpers.h in Headers */, 257754A81BEE44CD00737CA5 /* ASTextKitContext.h in Headers */, 464052221A3F83C40061C0BA /* ASFlowLayoutController.h in Headers */, @@ -1340,6 +1345,7 @@ 254C6B791BF94DF4003EC431 /* ASTextKitEntityAttribute.h in Headers */, 509E68631B3AEDB4009B9150 /* ASCollectionViewLayoutController.h in Headers */, B35061F71B010EFD0018CF92 /* ASCollectionViewProtocols.h in Headers */, + DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */, B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */, B35061F81B010EFD0018CF92 /* ASControlNode.h in Headers */, B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 50ad035702..a4357479f9 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -25,7 +25,7 @@ if (!(self = [super init])) return nil; - // use UITableViewCell defaults + // Use UITableViewCell defaults _selectionStyle = UITableViewCellSelectionStyleDefault; self.clipsToBounds = YES; diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index aac9507693..7c5e5fabac 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -6,19 +6,16 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "ASCollectionView.h" - #import "ASAssert.h" -#import "ASCollectionViewLayoutController.h" -#import "ASRangeController.h" -#import "ASCollectionDataController.h" #import "ASBatchFetching.h" -#import "UICollectionViewLayout+ASConvenience.h" -#import "ASInternalHelpers.h" +#import "ASCollectionView.h" +#import "ASCollectionDataController.h" +#import "ASCollectionViewLayoutController.h" #import "ASCollectionViewFlowLayoutInspector.h" - -// FIXME: Temporary nonsense import until method names are finalized and exposed -#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNode+FrameworkPrivate.h" +#import "ASInternalHelpers.h" +#import "ASRangeController.h" +#import "UICollectionViewLayout+ASConvenience.h" static const NSUInteger kASCollectionViewAnimationNone = UITableViewRowAnimationNone; static const ASSizeRange kInvalidSizeRange = {CGSizeZero, CGSizeZero}; @@ -661,6 +658,8 @@ static BOOL _isInterceptedSelector(SEL sel) - (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPath:(NSIndexPath *)indexPath { ASCellNode *node = [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath]; + [node enterHierarchyState:ASHierarchyStateRangeManaged]; + ASDisplayNodeAssert([node isKindOfClass:ASCellNode.class], @"invalid node class, expected ASCellNode"); if (node.layoutDelegate == nil) { node.layoutDelegate = self; diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 3af5c45254..b0bb3b0e0f 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -412,10 +412,8 @@ */ - (UIImage *)placeholderImage; - /** @name Description */ - /** * @abstract Return a description of the node * @@ -425,47 +423,5 @@ @end -@interface ASDisplayNode (ASDisplayNodePrivate) -/** - * This method has proven helpful in a few rare scenarios, similar to a category extension on UIView, - * but it's considered private API for now and its use should not be encouraged. - * @param checkViewHierarchy If YES, and no supernode can be found, method will walk up from `self.view` to find a supernode. - * If YES, this method must be called on the main thread and the node must not be layer-backed. - */ -- (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy; - -// The two methods below will eventually be exposed, but their names are subject to change. -/** - * @abstract Ensure that all rendering is complete for this node and its descendents. - * - * @discussion Calling this method on the main thread after a node is added to the view heirarchy will ensure that - * placeholder states are never visible to the user. It is used by ASTableView, ASCollectionView, and ASViewController - * to implement their respective ".neverShowPlaceholders" option. - * - * If all nodes have layer.contents set and/or their layer does not have -needsDisplay set, the method will return immediately. - * - * This method is capable of handling a mixed set of nodes, with some not having started display, some in progress on an - * asynchronous display operation, and some already finished. - * - * In order to guarantee against deadlocks, this method should only be called on the main thread. - * It may block on the private queue, [_ASDisplayLayer displayQueue] - */ -- (void)recursivelyEnsureDisplay; - -/** - * @abstract Allows a node to bypass all ensureDisplay passes. Defaults to NO. - * - * @discussion Nodes that are expensive to draw and expected to have placeholder even with - * .neverShowPlaceholders enabled should set this to YES. - * - * ASImageNode uses the default of NO, as it is often used for UI images that are expected to synchronize with ensureDisplay. - * - * ASNetworkImageNode and ASMultiplexImageNode set this to YES, because they load data from a database or server, - * and are expected to support a placeholder state given that display is often blocked on slow data fetching. - */ -@property (nonatomic, assign) BOOL shouldBypassEnsureDisplay; - -@end - #define ASDisplayNodeAssertThreadAffinity(viewNode) ASDisplayNodeAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created") #define ASDisplayNodeCAssertThreadAffinity(viewNode) ASDisplayNodeCAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created") diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 2c68b3b7e4..0edc15136a 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -7,8 +7,9 @@ */ #import "ASDisplayNode.h" -#import "ASDisplayNode+Subclasses.h" #import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASLayoutOptionsPrivate.h" #import @@ -38,6 +39,9 @@ @end +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + // Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) #if TIME_DISPLAYNODE_OPS #define TIME_SCOPED(outVar) ASDN::ScopeTimer t(outVar) @@ -323,17 +327,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) #pragma mark - Core -- (void)__tearDown:(BOOL)tearDown subnodesOfNode:(ASDisplayNode *)node -{ - for (ASDisplayNode *subnode in node.subnodes) { - if (tearDown) { - [subnode __unloadNode]; - } else { - [subnode __loadNode]; - } - } -} - - (void)__unloadNode { ASDisplayNodeAssertThreadAffinity(self); @@ -357,22 +350,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [self layer]; } -- (ASDisplayNode *)__rasterizedContainerNode -{ - ASDisplayNode *node = self.supernode; - while (node) { - if (node.shouldRasterizeDescendants) { - return node; - } - node = node.supernode; - } - - return nil; -} - - (BOOL)__shouldLoadViewOrLayer { - return ![self __rasterizedContainerNode]; + return !(_hierarchyState & ASHierarchyStateRasterized); } - (BOOL)__shouldSize @@ -645,28 +625,42 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) { ASDisplayNodeAssertThreadAffinity(self); ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssert(!((_hierarchyState & ASHierarchyStateRasterized) && _flags.shouldRasterizeDescendants), + @"Subnode of a rasterized node should not have redundant shouldRasterizeDescendants enabled"); return _flags.shouldRasterizeDescendants; } -- (void)setShouldRasterizeDescendants:(BOOL)flag +- (void)setShouldRasterizeDescendants:(BOOL)shouldRasterize { ASDisplayNodeAssertThreadAffinity(self); ASDN::MutexLocker l(_propertyLock); - if (_flags.shouldRasterizeDescendants == flag) + if (_flags.shouldRasterizeDescendants == shouldRasterize) return; - _flags.shouldRasterizeDescendants = flag; + _flags.shouldRasterizeDescendants = shouldRasterize; if (self.isNodeLoaded) { - //recursively tear down or build up subnodes + // Recursively tear down or build up subnodes. + // TODO: When disabling rasterization, preserve rasterized backing store as placeholderImage + // while the newly materialized subtree finishes rendering. Then destroy placeholderImage to save memory. [self recursivelyClearContents]; - [self __tearDown:flag subnodesOfNode:self]; - if (flag == NO) { - [self _addSubnodeViewsAndLayers]; - } - [self recursivelyDisplayImmediately]; + ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode *node) { + if (shouldRasterize) { + [node enterHierarchyState:ASHierarchyStateRasterized]; + [node __unloadNode]; + } else { + [node exitHierarchyState:ASHierarchyStateRasterized]; + [node __loadNode]; + } + }); + + if (self.interfaceState & ASInterfaceStateVisible) { + // TODO: Change this to recursivelyEnsureDisplay - but need a variant that does not skip + // nodes that have shouldBypassEnsureDisplay set (such as image nodes) so they are rasterized. + [self recursivelyDisplayImmediately]; + } } } @@ -969,7 +963,7 @@ static bool disableNotificationsForMovingBetweenParents(ASDisplayNode *from, ASD [_subnodes insertObject:subnode atIndex:subnodeIndex]; // Don't bother inserting the view/layer if in a rasterized subtree, becuase there are no layers in the hierarchy and none of this could possibly work. - if (!_flags.shouldRasterizeDescendants && ![self __rasterizedContainerNode]) { + if (!_flags.shouldRasterizeDescendants && [self __shouldLoadViewOrLayer]) { if (_layer) { ASDisplayNodeCAssertMainThread(); @@ -1090,7 +1084,7 @@ static NSInteger incrementIfFound(NSInteger i) { NSInteger aboveSublayerIndex = NSNotFound; // Don't bother figuring out the sublayerIndex if in a rasterized subtree, becuase there are no layers in the hierarchy and none of this could possibly work. - if (!_flags.shouldRasterizeDescendants && ![self __rasterizedContainerNode]) { + if (!_flags.shouldRasterizeDescendants && [self __shouldLoadViewOrLayer]) { if (_layer) { aboveSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:above.layer]; ASDisplayNodeAssert(aboveSublayerIndex != NSNotFound, @"Somehow above's supernode is self, yet we could not find it in our layers to replace"); @@ -1345,7 +1339,15 @@ static NSInteger incrementIfFound(NSInteger i) { - (void)__setSupernode:(ASDisplayNode *)supernode { ASDN::MutexLocker l(_propertyLock); - _supernode = supernode; + if (_supernode != supernode) { + ASHierarchyState oldHierarchyState = _supernode.hierarchyState; + _supernode = supernode; + if (_supernode) { + [self enterHierarchyState:_supernode.hierarchyState]; + } else { + [self exitHierarchyState:oldHierarchyState]; + } + } } // Track that a node will be displayed as part of the current node hierarchy. @@ -1653,7 +1655,8 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) * @see https://github.com/facebook/AsyncDisplayKit/issues/900 * Possible solution is to push `isInCellNode` state downward on `addSubnode`/`removeFromSupernode`. */ -- (BOOL)supportsInterfaceState { +- (BOOL)supportsInterfaceState +{ return ([self isKindOfClass:ASCellNode.class] || [self _supernodeWithClass:ASCellNode.class checkViewHierarchy:NO] != nil); } @@ -1664,23 +1667,23 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) return _interfaceState; } -- (void)setInterfaceState:(ASInterfaceState)interfaceState +- (void)setInterfaceState:(ASInterfaceState)newState { - ASInterfaceState oldValue; + ASInterfaceState oldState; { ASDN::MutexLocker l(_propertyLock); - oldValue = _interfaceState; - _interfaceState = interfaceState; + oldState = _interfaceState; + _interfaceState = newState; } - if (interfaceState != oldValue) { - if ((interfaceState & ASInterfaceStateMeasureLayout) != (oldValue & ASInterfaceStateMeasureLayout)) { + if (newState != oldState) { + if ((newState & ASInterfaceStateMeasureLayout) != (oldState & ASInterfaceStateMeasureLayout)) { // Trigger asynchronous measurement if it is not already cached or being calculated. } // Entered or exited data loading state. - if ((interfaceState & ASInterfaceStateFetchData) != (oldValue & ASInterfaceStateFetchData)) { - if (interfaceState & ASInterfaceStateFetchData) { + if ((newState & ASInterfaceStateFetchData) != (oldState & ASInterfaceStateFetchData)) { + if (newState & ASInterfaceStateFetchData) { [self fetchData]; } else { [self clearFetchedData]; @@ -1688,8 +1691,8 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) } // Entered or exited contents rendering state. - if ((interfaceState & ASInterfaceStateDisplay) != (oldValue & ASInterfaceStateDisplay)) { - if (interfaceState & ASInterfaceStateDisplay) { + if ((newState & ASInterfaceStateDisplay) != (oldState & ASInterfaceStateDisplay)) { + if (newState & ASInterfaceStateDisplay) { // Once the working window is eliminated (ASRangeHandlerRender), trigger display directly here. [self setDisplaySuspended:NO]; } else { @@ -1699,14 +1702,13 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) } // Entered or exited data loading state. - if ((interfaceState & ASInterfaceStateVisible) != (oldValue & ASInterfaceStateVisible)) { - if (interfaceState & ASInterfaceStateVisible) { + if ((newState & ASInterfaceStateVisible) != (oldState & ASInterfaceStateVisible)) { + if (newState & ASInterfaceStateVisible) { // Consider providing a -didBecomeVisible. } else { // Consider providing a -didBecomeInvisible. } } - } } @@ -1724,6 +1726,40 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) }); } +- (ASHierarchyState)hierarchyState +{ + ASDN::MutexLocker l(_propertyLock); + return _hierarchyState; +} + +- (void)setHierarchyState:(ASHierarchyState)newState +{ + ASHierarchyState oldState; + { + ASDN::MutexLocker l(_propertyLock); + oldState = _hierarchyState; + _hierarchyState = newState; + } + + if (newState != oldState) { + LOG(@"setHierarchyState: oldState = %lu, newState = %lu", (unsigned long)oldState, (unsigned long)newState); + } +} + +- (void)enterHierarchyState:(ASHierarchyState)hierarchyState +{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, ^(ASDisplayNode *node) { + node.hierarchyState |= hierarchyState; + }); +} + +- (void)exitHierarchyState:(ASHierarchyState)hierarchyState +{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, ^(ASDisplayNode *node) { + node.hierarchyState &= (~hierarchyState); + }); +} + - (void)layout { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 4ccf0b48ca..22dc2f2921 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -37,6 +37,12 @@ extern ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node); */ extern void ASDisplayNodePerformBlockOnEveryNode(CALayer *layer, ASDisplayNode *node, void(^block)(ASDisplayNode *node)); +/** + Identical to ASDisplayNodePerformBlockOnEveryNode, except it does not run the block on the + node provided directly to the function call - only on all descendants. + */ +extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^block)(ASDisplayNode *node)); + /** Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block. */ diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index 681640ffa3..7665bee955 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -7,8 +7,8 @@ */ #import "ASDisplayNodeExtras.h" - #import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+FrameworkPrivate.h" extern ASDisplayNode *ASLayerToDisplayNode(CALayer *layer) { @@ -46,6 +46,13 @@ extern void ASDisplayNodePerformBlockOnEveryNode(CALayer *layer, ASDisplayNode * } } +extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^block)(ASDisplayNode *node)) +{ + for (ASDisplayNode *subnode in node.subnodes) { + ASDisplayNodePerformBlockOnEveryNode(nil, subnode, block); + } +} + id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) { CALayer *layer = node.layer; diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index f72fbdce39..c51e2bb2ba 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -17,6 +17,7 @@ #import "ASAvailability.h" #import "ASBaseDefines.h" #import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASLog.h" #import "ASPhotosFrameworkImageRequest.h" #import "ASEqualityHelpers.h" diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index e7905bebf2..c1437ef814 100644 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -10,8 +10,9 @@ #import "ASBasicImageDownloader.h" #import "ASDisplayNode+Subclasses.h" -#import "ASThread.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASEqualityHelpers.h" +#import "ASThread.h" @interface ASNetworkImageNode () { @@ -30,10 +31,8 @@ BOOL _imageLoaded; } - @end - @implementation ASNetworkImageNode - (instancetype)initWithCache:(id)cache downloader:(id)downloader diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 530a26f638..751247d7a0 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -10,16 +10,14 @@ #import "ASTableViewInternal.h" #import "ASAssert.h" +#import "ASBatchFetching.h" #import "ASChangeSetDataController.h" #import "ASCollectionViewLayoutController.h" -#import "ASLayoutController.h" -#import "ASRangeController.h" -#import "ASBatchFetching.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASInternalHelpers.h" #import "ASLayout.h" - -// FIXME: Temporary nonsense import until method names are finalized and exposed -#import "ASDisplayNode+Subclasses.h" +#import "ASLayoutController.h" +#import "ASRangeController.h" static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; @@ -829,6 +827,8 @@ static BOOL _isInterceptedSelector(SEL sel) - (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPath:(NSIndexPath *)indexPath { ASCellNode *node = [_asyncDataSource tableView:self nodeForRowAtIndexPath:indexPath]; + [node enterHierarchyState:ASHierarchyStateRangeManaged]; + ASDisplayNodeAssert([node isKindOfClass:ASCellNode.class], @"invalid node class, expected ASCellNode"); if (node.layoutDelegate == nil) { node.layoutDelegate = self; diff --git a/AsyncDisplayKit/ASViewController.m b/AsyncDisplayKit/ASViewController.m index 3509ccbe85..ab2c6741e6 100644 --- a/AsyncDisplayKit/ASViewController.m +++ b/AsyncDisplayKit/ASViewController.m @@ -9,9 +9,7 @@ #import "ASViewController.h" #import "ASAssert.h" #import "ASDimension.h" - -// FIXME: Temporary nonsense import until method names are finalized and exposed -#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNode+FrameworkPrivate.h" @implementation ASViewController { diff --git a/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm b/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm index adbbf5ecfb..09a3623a26 100644 --- a/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm +++ b/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm @@ -8,7 +8,7 @@ #import "ASRangeHandlerPreload.h" #import "ASDisplayNode.h" -#import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+FrameworkPrivate.h" @implementation ASRangeHandlerPreload diff --git a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm index b2a751a8bd..206b7d7e62 100644 --- a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm +++ b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm @@ -10,7 +10,7 @@ #import "ASDisplayNode.h" #import "ASDisplayNode+Subclasses.h" -#import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+FrameworkPrivate.h" @interface ASRangeHandlerRender () @property (nonatomic,readonly) UIWindow *workingWindow; diff --git a/AsyncDisplayKit/Details/ASRangeHandlerVisible.mm b/AsyncDisplayKit/Details/ASRangeHandlerVisible.mm index d8daf51889..f17bc1cad3 100644 --- a/AsyncDisplayKit/Details/ASRangeHandlerVisible.mm +++ b/AsyncDisplayKit/Details/ASRangeHandlerVisible.mm @@ -8,7 +8,7 @@ #import "ASRangeHandlerVisible.h" #import "ASDisplayNode.h" -#import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+FrameworkPrivate.h" @implementation ASRangeHandlerVisible diff --git a/AsyncDisplayKit/Details/_ASDisplayLayer.mm b/AsyncDisplayKit/Details/_ASDisplayLayer.mm index 99e4bf68e8..12622d3bb6 100644 --- a/AsyncDisplayKit/Details/_ASDisplayLayer.mm +++ b/AsyncDisplayKit/Details/_ASDisplayLayer.mm @@ -14,6 +14,7 @@ #import "ASAssert.h" #import "ASDisplayNode.h" #import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+FrameworkPrivate.h" @implementation _ASDisplayLayer { diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm index 8b5300269d..027a312733 100644 --- a/AsyncDisplayKit/Details/_ASDisplayView.mm +++ b/AsyncDisplayKit/Details/_ASDisplayView.mm @@ -15,6 +15,7 @@ #import "ASAssert.h" #import "ASDisplayNodeExtras.h" #import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+Subclasses.h" @interface _ASDisplayView () diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index e74cd00e35..bdfb9b2729 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -10,6 +10,7 @@ #import "_ASAsyncTransaction.h" #import "ASAssert.h" #import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+FrameworkPrivate.h" @implementation ASDisplayNode (AsyncDisplay) @@ -84,7 +85,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, return; } - BOOL rasterizingFromAscendent = [self __rasterizedContainerNode] != nil; + BOOL rasterizingFromAscendent = (_hierarchyState & ASHierarchyStateRasterized); // if super node is rasterizing descendents, subnodes will not have had layout calls becase they don't have layers if (rasterizingFromAscendent) { @@ -178,7 +179,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; - ASDisplayNodeAssert(rasterizing || ![self __rasterizedContainerNode], @"Rasterized descendants should never display unless being drawn into the rasterized container."); + ASDisplayNodeAssert(rasterizing || !(_hierarchyState & ASHierarchyStateRasterized), @"Rasterized descendants should never display unless being drawn into the rasterized container."); if (!rasterizing && self.shouldRasterizeDescendants) { CGRect bounds = self.bounds; @@ -296,7 +297,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, ASDN::MutexLocker l(_propertyLock); - if ([self __rasterizedContainerNode]) { + if (_hierarchyState & ASHierarchyStateRasterized) { return; } diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h new file mode 100644 index 0000000000..58cc12c35c --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -0,0 +1,109 @@ +/* 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. + */ + +// +// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. +// These methods must never be called or overridden by other classes. +// + +#import "_ASDisplayLayer.h" +#import "_AS-objc-internal.h" +#import "ASDisplayNodeExtraIvars.h" +#import "ASDisplayNode.h" +#import "ASSentinel.h" +#import "ASThread.h" +#import "ASLayoutOptions.h" + +/** + Hierarchy state is propogated from nodes to all of their children when certain behaviors are required from the subtree. + Examples include rasterization and external driving of the .interfaceState property. + By passing this information explicitly, performance is optimized by avoiding iteration up the supernode chain. + Lastly, this avoidance of supernode traversal protects against the possibility of deadlocks when a supernode is + simultaneously attempting to materialize views / layers for its subtree (as many related methods require property locking) + + Note: as the hierarchy deepens, more state properties may be enabled. However, state properties may never be disabled / + cancelled below the point they are enabled. They continue to the leaves of the hierarchy. + */ + +typedef NS_OPTIONS(NSUInteger, ASHierarchyState) +{ + /** The node may or may not have a supernode, but no supernode has a special hierarchy-influencing option enabled. */ + ASHierarchyStateNormal = 0, + /** The node has a supernode with .shouldRasterizeDescendants = YES. + Note: the root node of the rasterized subtree (the one with the property set on it) will NOT have this state set. */ + ASHierarchyStateRasterized = 1 << 0, + /** The node or one of its supernodes is managed by a class like ASRangeController. Most commonly, these nodes are + ASCellNode objects or a subnode of one, and are used in ASTableView or ASCollectionView. + These nodes also recieve regular updates to the .interfaceState property with more detailed status information. */ + ASHierarchyStateRangeManaged = 1 << 1, +}; + +@interface ASDisplayNode () <_ASDisplayLayerDelegate> +{ +@protected + ASInterfaceState _interfaceState; + ASHierarchyState _hierarchyState; +} + +// These methods are recursive, and either union or remove the provided interfaceState to all sub-elements. +- (void)enterInterfaceState:(ASInterfaceState)interfaceState; +- (void)exitInterfaceState:(ASInterfaceState)interfaceState; + +// These methods are recursive, and either union or remove the provided hierarchyState to all sub-elements. +- (void)enterHierarchyState:(ASHierarchyState)hierarchyState; +- (void)exitHierarchyState:(ASHierarchyState)hierarchyState; + +/** + * @abstract Returns the Hierarchy State of the node. + * + * @return The current ASHierarchyState of the node, indicating whether it is rasterized or managed by a range controller. + * + * @see ASInterfaceState + */ +@property (nonatomic, readwrite) ASHierarchyState hierarchyState; + +// The two methods below will eventually be exposed, but their names are subject to change. +/** + * @abstract Ensure that all rendering is complete for this node and its descendents. + * + * @discussion Calling this method on the main thread after a node is added to the view heirarchy will ensure that + * placeholder states are never visible to the user. It is used by ASTableView, ASCollectionView, and ASViewController + * to implement their respective ".neverShowPlaceholders" option. + * + * If all nodes have layer.contents set and/or their layer does not have -needsDisplay set, the method will return immediately. + * + * This method is capable of handling a mixed set of nodes, with some not having started display, some in progress on an + * asynchronous display operation, and some already finished. + * + * In order to guarantee against deadlocks, this method should only be called on the main thread. + * It may block on the private queue, [_ASDisplayLayer displayQueue] + */ +- (void)recursivelyEnsureDisplay; + +/** + * @abstract Allows a node to bypass all ensureDisplay passes. Defaults to NO. + * + * @discussion Nodes that are expensive to draw and expected to have placeholder even with + * .neverShowPlaceholders enabled should set this to YES. + * + * ASImageNode uses the default of NO, as it is often used for UI images that are expected to synchronize with ensureDisplay. + * + * ASNetworkImageNode and ASMultiplexImageNode set this to YES, because they load data from a database or server, + * and are expected to support a placeholder state given that display is often blocked on slow data fetching. + */ +@property (nonatomic, assign) BOOL shouldBypassEnsureDisplay; + +@end + +@interface UIView (ASDisplayNodeInternal) +@property (nonatomic, assign, readwrite) ASDisplayNode *asyncdisplaykit_node; +@end + +@interface CALayer (ASDisplayNodeInternal) +@property (nonatomic, assign, readwrite) ASDisplayNode *asyncdisplaykit_node; +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index e9ec7d72e3..b23b994f8c 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -8,9 +8,11 @@ #import "_ASCoreAnimationExtras.h" #import "_ASPendingState.h" +#import "ASInternalHelpers.h" #import "ASAssert.h" -#import "ASDisplayNode+Subclasses.h" #import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASEqualityHelpers.h" /** @@ -219,9 +221,22 @@ - (void)setNeedsDisplay { - ASDisplayNode *rasterizedContainerNode = [self __rasterizedContainerNode]; - if (rasterizedContainerNode) { - [rasterizedContainerNode setNeedsDisplay]; + if (_hierarchyState & ASHierarchyStateRasterized) { + ASPerformBlockOnMainThread(^{ + // The below operation must be performed on the main thread to ensure against an extremely rare deadlock, where a parent node + // begins materializing the view / layer heirarchy (locking itself or a descendant) while this node walks up + // the tree and requires locking that node to access .shouldRasterizeDescendants. + // For this reason, this method should be avoided when possible. Use _hierarchyState & ASHierarchyStateRasterized. + ASDisplayNodeAssertMainThread(); + ASDisplayNode *rasterizedContainerNode = self.supernode; + while (rasterizedContainerNode) { + if (rasterizedContainerNode.shouldRasterizeDescendants) { + break; + } + rasterizedContainerNode = rasterizedContainerNode.supernode; + } + [rasterizedContainerNode setNeedsDisplay]; + }); } else { [_layer setNeedsDisplay]; } diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 0d3ab7efe3..f30361a3ab 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -22,13 +22,14 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); void ASDisplayNodeRespectThreadAffinityOfNode(ASDisplayNode *node, void (^block)()); -typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { - ASDisplayNodeMethodOverrideNone = 0, - ASDisplayNodeMethodOverrideTouchesBegan = 1 << 0, - ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1, - ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2, - ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3, - ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4 +typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) +{ + ASDisplayNodeMethodOverrideNone = 0, + ASDisplayNodeMethodOverrideTouchesBegan = 1 << 0, + ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1, + ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2, + ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3, + ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4 }; @class _ASPendingState; @@ -73,8 +74,6 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { _ASPendingState *_pendingViewState; - ASInterfaceState _interfaceState; - struct ASDisplayNodeFlags { // public properties unsigned synchronous:1; @@ -118,9 +117,6 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { // Bitmask to check which methods an object overrides. @property (nonatomic, assign, readonly) ASDisplayNodeMethodOverrides methodOverrides; -// These methods are recursive, and either union or remove the provided interfaceState to all sub-elements. -- (void)enterInterfaceState:(ASInterfaceState)interfaceState; -- (void)exitInterfaceState:(ASInterfaceState)interfaceState; // Swizzle to extend the builtin functionality with custom logic - (BOOL)__shouldLoadViewOrLayer; @@ -149,9 +145,6 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { // Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. - (void)displayImmediately; -// Returns the ancestor node that rasterizes descendants, or nil if none. -- (ASDisplayNode *)__rasterizedContainerNode; - // Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. - (id)initWithViewClass:(Class)viewClass; @@ -160,12 +153,12 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { @property (nonatomic, assign) CGFloat contentsScaleForDisplay; -@end +/** + * This method has proven helpful in a few rare scenarios, similar to a category extension on UIView, + * but it's considered private API for now and its use should not be encouraged. + * @param checkViewHierarchy If YES, and no supernode can be found, method will walk up from `self.view` to find a supernode. + * If YES, this method must be called on the main thread and the node must not be layer-backed. + */ +- (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy; -@interface UIView (ASDisplayNodeInternal) -@property (nonatomic, assign, readwrite) ASDisplayNode *asyncdisplaykit_node; -@end - -@interface CALayer (ASDisplayNodeInternal) -@property (nonatomic, assign, readwrite) ASDisplayNode *asyncdisplaykit_node; @end diff --git a/AsyncDisplayKit/TextKit/ASTextNodeWordKerner.m b/AsyncDisplayKit/TextKit/ASTextNodeWordKerner.m index ffd9655a91..f0ae6eea7f 100644 --- a/AsyncDisplayKit/TextKit/ASTextNodeWordKerner.m +++ b/AsyncDisplayKit/TextKit/ASTextNodeWordKerner.m @@ -68,7 +68,7 @@ { // If it's a space character and we have custom word kerning, use the whitespace action control character. if ([layoutManager.textStorage.string characterAtIndex:characterIndex] == ' ') - return NSControlCharacterWhitespaceAction; + return NSControlCharacterActionWhitespace; return defaultAction; }