diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index ed7b097097..1ad6732d64 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -197,6 +197,29 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return [_ASDisplayLayer class]; } ++ (void)scheduleNodeForDisplay:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + static NSMutableSet *nodesToDisplay = nil; + static BOOL displayScheduled = NO; + if (!nodesToDisplay) { + nodesToDisplay = [[NSMutableSet alloc] init]; + } + [nodesToDisplay addObject:node]; + if (!displayScheduled) { + displayScheduled = YES; + // It's essenital that any layout pass that is scheduled during the current + // runloop has a chance to be applied / scheduled, so always perform this after the current runloop. + dispatch_async(dispatch_get_main_queue(), ^{ + displayScheduled = NO; + for (ASDisplayNode *node in nodesToDisplay) { + [node __recursivelyTriggerDisplayAndBlock:NO]; + } + nodesToDisplay = nil; + }); + } +} + #pragma mark - Lifecycle - (void)_staticInitialize @@ -710,6 +733,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (void)recursivelyDisplayImmediately { ASDN::MutexLocker l(_propertyLock); + for (ASDisplayNode *child in _subnodes) { [child recursivelyDisplayImmediately]; } @@ -1453,7 +1477,7 @@ static NSInteger incrementIfFound(NSInteger i) { [_placeholderLayer removeFromSuperlayer]; } -void recursivelyEnsureDisplayForLayer(CALayer *layer) +void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) { // This recursion must handle layers in various states: // 1. Just added to hierarchy, CA hasn't yet called -display @@ -1472,26 +1496,26 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) // Kick off the recursion first, so that all necessary display calls are sent and the displayQueue is full of parallelizable work. for (CALayer *sublayer in layer.sublayers) { - recursivelyEnsureDisplayForLayer(sublayer); + recursivelyTriggerDisplayForLayer(sublayer, shouldBlock); } - // As the recursion unwinds, verify each transaction is complete and block if it is not. - // While blocking on one transaction, others may be completing concurrently, so it doesn't matter which blocks first. - BOOL waitUntilComplete = (!node.shouldBypassEnsureDisplay); - if (waitUntilComplete) { - for (_ASAsyncTransaction *transaction in [layer.asyncdisplaykit_asyncLayerTransactions copy]) { - // Even if none of the layers have had a chance to start display earlier, they will still be allowed to saturate a multicore CPU while blocking main. - // This significantly reduces time on the main thread relative to UIKit. - [transaction waitUntilComplete]; + if (shouldBlock) { + // As the recursion unwinds, verify each transaction is complete and block if it is not. + // While blocking on one transaction, others may be completing concurrently, so it doesn't matter which blocks first. + BOOL waitUntilComplete = (!node.shouldBypassEnsureDisplay); + if (waitUntilComplete) { + for (_ASAsyncTransaction *transaction in [layer.asyncdisplaykit_asyncLayerTransactions copy]) { + // Even if none of the layers have had a chance to start display earlier, they will still be allowed to saturate a multicore CPU while blocking main. + // This significantly reduces time on the main thread relative to UIKit. + [transaction waitUntilComplete]; + } } } } -- (void)recursivelyEnsureDisplay +- (void)__recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(self.isNodeLoaded, @"Node must have layer or view loaded to use -recursivelyEnsureDisplay"); - ASDisplayNodeAssert(self.inHierarchy && (self.isLayerBacked || self.view.window != nil), @"Node must be in a hierarchy to use -recursivelyEnsureDisplay"); CALayer *layer = self.layer; // -layoutIfNeeded is recursive, and even walks up to superlayers to check if they need layout, @@ -1500,7 +1524,12 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) if ([layer needsLayout]) { [layer layoutIfNeeded]; } - recursivelyEnsureDisplayForLayer(layer); + recursivelyTriggerDisplayForLayer(layer, shouldBlock); +} + +- (void)recursivelyEnsureDisplay +{ + [self __recursivelyTriggerDisplayAndBlock:YES]; } - (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay @@ -2127,6 +2156,9 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, return _replaceAsyncSentinel != nil; } +// FIXME: This method doesn't appear to be called, and could be removed. +// However, it may be useful for an API similar to what Paper used to create a new node hierarchy, +// trigger asynchronous measurement and display on it, and have it swap out and replace an old hierarchy. - (ASSentinel *)_asyncReplaceSentinel { ASDN::MutexLocker l(_propertyLock); diff --git a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm index 206b7d7e62..4872707e7e 100644 --- a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm +++ b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm @@ -17,8 +17,9 @@ @end @implementation ASRangeHandlerRender -@synthesize workingWindow = _workingWindow; +#if USE_WORKING_WINDOW +@synthesize workingWindow = _workingWindow; - (UIWindow *)workingWindow { ASDisplayNodeAssertMainThread(); @@ -45,6 +46,7 @@ [self node:node exitedRangeOfType:ASLayoutRangeTypeRender]; } } +#endif - (void)node:(ASDisplayNode *)node enteredRangeOfType:(ASLayoutRangeType)rangeType { @@ -60,12 +62,17 @@ // The node un-suspends display. [node enterInterfaceState:ASInterfaceStateDisplay]; + +#if USE_WORKING_WINDOW // Add the node's layer to an off-screen window to trigger display and mark its contents as non-volatile. // Use the layer directly to avoid the substantial overhead of UIView heirarchy manipulations. // Any view-backed nodes will still create their views in order to assemble the layer heirarchy, and they will // also assemble a view subtree for the node, but we avoid the much more significant expense triggered by a view // being added or removed from an onscreen window (responder chain setup, will/DidMoveToWindow: recursive calls, etc) [[[self workingWindow] layer] addSublayer:node.layer]; +#else + [node recursivelyEnsureDisplay]; // Need to do this without waiting +#endif } - (void)node:(ASDisplayNode *)node exitedRangeOfType:(ASLayoutRangeType)rangeType @@ -93,6 +100,7 @@ // The node calls clearCurrentContents and suspends display [node exitInterfaceState:ASInterfaceStateDisplay]; +#if USE_WORKING_WINDOW if (node.layer.superlayer != [[self workingWindow] layer]) { // In this case, the node has previously passed through the working range (or it is zero), and it has now fallen outside the working range. if (![node isLayerBacked]) { @@ -104,6 +112,13 @@ // At this point, the node's layer may validly be present either in the workingWindow, or in the contentsView of a cell. [node.layer removeFromSuperlayer]; +#else + if (![node isLayerBacked]) { + [node.view removeFromSuperview]; + } else { + [node.layer removeFromSuperlayer]; + } +#endif } @end diff --git a/AsyncDisplayKit/Details/_ASDisplayLayer.mm b/AsyncDisplayKit/Details/_ASDisplayLayer.mm index 12622d3bb6..3152e4d7ce 100644 --- a/AsyncDisplayKit/Details/_ASDisplayLayer.mm +++ b/AsyncDisplayKit/Details/_ASDisplayLayer.mm @@ -147,11 +147,9 @@ - (void)displayImmediately { - // REVIEW: Should this respect isDisplaySuspended? If so, we'd probably want to synchronously display when - // setDisplaySuspended:No is called, rather than just scheduling. The thread affinity for the displayImmediately - // call will be tricky if we need to support this, though. It probably should just execute if displayImmediately is - // called directly. The caller should be responsible for not calling displayImmediately if it wants to obey the - // suspended state. + // This method is a low-level bypass that avoids touching CA, including any reset of the + // needsDisplay flag, until the .contents property is set with the result. + // It is designed to be able to block the thread of any caller and fully execute the display. ASDisplayNodeAssertMainThread(); [self display:NO]; diff --git a/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m b/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m index 9aa3bd8fdb..5f57b5aa86 100644 --- a/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m +++ b/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m @@ -111,7 +111,7 @@ typedef NS_ENUM(NSUInteger, PIDebugBoxPaddingLocation) [paddedLines addObject:paddedLine]; } concatenatedLines = paddedLines; - totalLineLength += difference; + // totalLineLength += difference; } concatenatedLines = [self appendTopAndBottomToBoxString:concatenatedLines parent:parent]; return [concatenatedLines componentsJoinedByString:@"\n"]; diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index 4242881a5b..525d1b4bf8 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -304,10 +304,9 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, // for async display, capture the current displaySentinel value to bail early when the job is executed if another is // enqueued // for sync display, just use nil for the displaySentinel and go - // - // REVIEW: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing - // from the displayQueue? do we want to put in some kind of timer to not cancel early fails from displaySentinel - // changes? + + // FIXME: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing + // from the displayQueue? Need to not cancel early fails from displaySentinel changes. ASSentinel *displaySentinel = (asynchronously ? _displaySentinel : nil); int64_t displaySentinelValue = [displaySentinel increment]; diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 58cc12c35c..0ad88aafcb 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -19,6 +19,10 @@ #import "ASThread.h" #import "ASLayoutOptions.h" +// Project-wide control for whether the offscreen UIWindow is used for display, or if +// ASDK's internal system for coalescing and triggering display events is used. +#define USE_WORKING_WINDOW 1 + /** 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. diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index b23b994f8c..2239cc038a 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -221,6 +221,8 @@ - (void)setNeedsDisplay { + _bridge_prologue; + 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 @@ -238,7 +240,19 @@ [rasterizedContainerNode setNeedsDisplay]; }); } else { - [_layer setNeedsDisplay]; + // If not rasterized (and therefore we certainly have a view or layer), + // Send the message to the view/layer first, as scheduleNodeForDisplay may call -displayIfNeeded. + // Wrapped / synchronous nodes created with initWithView/LayerBlock: do not need scheduleNodeForDisplay, + // as they don't need to display in the working range at all - since at all times onscreen, one + // -setNeedsDisplay to the CALayer will result in a synchronous display in the next frame. + + _messageToViewOrLayer(setNeedsDisplay); + +#if !USE_WORKING_WINDOW + if (_layer && !self.isSynchronous) { + [ASDisplayNode scheduleNodeForDisplay:self]; + } +#endif } } diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index f30361a3ab..830c400dfb 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -108,6 +108,8 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) } ++ (void)scheduleNodeForDisplay:(ASDisplayNode *)node; + // The _ASDisplayLayer backing the node, if any. @property (nonatomic, readonly, retain) _ASDisplayLayer *asyncLayer;