From f0439c4ec985a3da584e8b75bd8e3b66c7713d16 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 14 Apr 2017 00:25:17 +0100 Subject: [PATCH] [ASDisplayNode] Trigger a layout pass whenever a node enters preload state (#3263) * Add a thread-safe layoutIfNeeded implementation to ASDisplayNode * Trigger a layout pass when a display node enters preload state - This ensures that all the subnodes have the correct size to preload their content. * ASCollectionNode to trigger its initial data load when it enters preload state * Minor change in _ASCollectionViewCell * Layout sublayouts before dispatch to main for subclass hooks * Update comments * Don't wait until updates are committed when the collection node enters display state * Same deal for table node * Explain the layout trigger in ASDisplayNode --- Source/ASCollectionNode.mm | 20 ++++-- Source/ASDisplayNode.h | 5 ++ Source/ASDisplayNode.mm | 21 ++++-- Source/ASTableNode.mm | 20 ++++-- Source/Details/UIView+ASConvenience.h | 1 + Source/Details/_ASCollectionViewCell.m | 1 + Source/Private/ASDisplayNode+UIViewBridge.mm | 74 +++++++++++++++----- Source/Private/_ASPendingState.mm | 26 +++++-- 8 files changed, 124 insertions(+), 44 deletions(-) diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 53da2eb6c2..aa320f495f 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -193,18 +193,20 @@ - (void)clearContents [self.rangeController clearContents]; } -- (void)didExitPreloadState -{ - [super didExitPreloadState]; - [self.rangeController clearPreloadedData]; -} - - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { [super interfaceStateDidChange:newState fromState:oldState]; [ASRangeController layoutDebugOverlayIfNeeded]; } +- (void)didEnterPreloadState +{ + // Intentionally allocate the view here so that super will trigger a layout pass on it which in turn will trigger the intial data load. + // We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view. + [self view]; + [super didEnterPreloadState]; +} + #if ASRangeControllerLoggingEnabled - (void)didEnterVisibleState { @@ -219,6 +221,12 @@ - (void)didExitVisibleState } #endif +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + [self.rangeController clearPreloadedData]; +} + #pragma mark Setter / Getter // TODO: Implement this without the view. Then revisit ASLayoutElementCollectionTableSetTraitCollection diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 9d716ed394..97fcd97568 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -639,6 +639,11 @@ extern NSInteger const ASDefaultDrawingPriority; */ - (void)setNeedsLayout; +/** + * Performs a layout pass on the node. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread. + */ +- (void)layoutIfNeeded; + @property (nonatomic, strong, nullable) id contents; // default=nil @property (nonatomic, assign) BOOL clipsToBounds; // default==NO @property (nonatomic, getter=isOpaque) BOOL opaque; // default==YES diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index e49d016ad6..8478e0a016 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -985,7 +985,7 @@ - (void)invalidateCalculatedLayout - (void)__layout { - ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssertThreadAffinity(self); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); { @@ -1014,8 +1014,12 @@ - (void)__layout [self _locked_layoutPlaceholderIfNecessary]; } - [self layout]; - [self layoutDidFinish]; + [self _layoutSublayouts]; + + ASPerformBlockOnMainThread(^{ + [self layout]; + [self layoutDidFinish]; + }); } /// Needs to be called with lock held @@ -1054,7 +1058,7 @@ - (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds std::shared_ptr nextLayout = _pendingDisplayNodeLayout; #define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout->layout.size, boundsSizeForLayout) - // nextLayout was likely created by a call to layoutThatFits:, check if is valid and can be applied. + // nextLayout was likely created by a call to layoutThatFits:, check if it is valid and can be applied. // If our bounds size is different than it, or invalid, recalculate. Use #define to avoid nullptr-> if (nextLayout == nullptr || nextLayout->isDirty() == YES || layoutSizeDifferentFromBounds) { // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). @@ -1405,12 +1409,13 @@ - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size - (void)layout { - [self _layoutSublayouts]; + ASDisplayNodeAssertMainThread(); + // Subclass hook } - (void)_layoutSublayouts { - ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssertThreadAffinity(self); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); ASLayout *layout; @@ -3720,6 +3725,10 @@ - (void)didEnterPreloadState ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); [_interfaceStateDelegate didEnterPreloadState]; + // Trigger a layout pass to ensure all subnodes have the correct size to preload their content. + // This is important for image nodes, as well as collection and table nodes. + [self layoutIfNeeded]; + if (_methodOverrides & ASDisplayNodeMethodOverrideFetchData) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" diff --git a/Source/ASTableNode.mm b/Source/ASTableNode.mm index 2835746aa0..92fb5b006d 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -122,18 +122,20 @@ - (void)clearContents [self.rangeController clearContents]; } -- (void)didExitPreloadState -{ - [super didExitPreloadState]; - [self.rangeController clearPreloadedData]; -} - - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { [super interfaceStateDidChange:newState fromState:oldState]; [ASRangeController layoutDebugOverlayIfNeeded]; } +- (void)didEnterPreloadState +{ + // Intentionally allocate the view here so that super will trigger a layout pass on it which in turn will trigger the intial data load. + // We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view. + [self view]; + [super didEnterPreloadState]; +} + #if ASRangeControllerLoggingEnabled - (void)didEnterVisibleState { @@ -148,6 +150,12 @@ - (void)didExitVisibleState } #endif +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + [self.rangeController clearPreloadedData]; +} + #pragma mark Setter / Getter // TODO: Implement this without the view. Then revisit ASLayoutElementCollectionTableSetTraitCollection diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 24a9021cfd..9c918689ec 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setNeedsDisplay; - (void)setNeedsLayout; +- (void)layoutIfNeeded; @end diff --git a/Source/Details/_ASCollectionViewCell.m b/Source/Details/_ASCollectionViewCell.m index a2f5bdda13..76709d0c31 100644 --- a/Source/Details/_ASCollectionViewCell.m +++ b/Source/Details/_ASCollectionViewCell.m @@ -58,6 +58,7 @@ - (void)prepareForReuse */ - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { + [super applyLayoutAttributes:layoutAttributes]; self.layoutAttributes = layoutAttributes; } diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index c26faba35c..a5139e0b7f 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -40,7 +40,6 @@ #if DISPLAYNODE_USE_LOCKS #define _bridge_prologue_read ASDN::MutexLocker l(__instanceLock__); ASDisplayNodeAssertThreadAffinity(self) #define _bridge_prologue_write ASDN::MutexLocker l(__instanceLock__) -#define _bridge_prologue_write_unlock ASDN::MutexUnlocker u(__instanceLock__) #else #define _bridge_prologue_read ASDisplayNodeAssertThreadAffinity(self) #define _bridge_prologue_write @@ -79,8 +78,6 @@ ASDISPLAYNODE_INLINE BOOL ASDisplayNodeShouldApplyBridgedWriteToView(ASDisplayNo #define _setToLayer(layerProperty, layerValueExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNodeGetPendingState(self).layerProperty = (layerValueExpr); } -#define _messageToViewOrLayer(viewAndLayerSelector) (_view ? [_view viewAndLayerSelector] : [_layer viewAndLayerSelector]) - /** * This category implements certain frequently-used properties and methods of UIView and CALayer so that ASDisplayNode clients can just call the view/layer methods on the node, * with minimal loss in performance. Unlike UIView and CALayer methods, these can be called from a non-main thread until the view or layer is created. @@ -301,8 +298,17 @@ - (void)setFrame:(CGRect)rect - (void)setNeedsDisplay { - _bridge_prologue_write; - if (_hierarchyState & ASHierarchyStateRasterized) { + BOOL isRasterized = NO; + BOOL shouldApply = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + isRasterized = _hierarchyState & ASHierarchyStateRasterized; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + viewOrLayer = _view ?: _layer; + } + + if (isRasterized) { 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 hierarchy (locking itself or a descendant) while this node walks up @@ -319,13 +325,13 @@ - (void)setNeedsDisplay [rasterizedContainerNode setNeedsDisplay]; }); } else { - BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); if (shouldApply) { // If not rasterized, and the node is loaded (meaning we certainly have a view or layer), send a // message to the view/layer first. This is because __setNeedsDisplay calls as scheduleNodeForDisplay, // which may call -displayIfNeeded. We want to ensure the needsDisplay flag is set now, and then cleared. - _messageToViewOrLayer(setNeedsDisplay); + [viewOrLayer setNeedsDisplay]; } else { + _bridge_prologue_write; [ASDisplayNodeGetPendingState(self) setNeedsDisplay]; } [self __setNeedsDisplay]; @@ -334,29 +340,59 @@ - (void)setNeedsDisplay - (void)setNeedsLayout { - _bridge_prologue_write; - BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + BOOL shouldApply = NO; + BOOL loaded = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + loaded = __loaded(self); + viewOrLayer = _view ?: _layer; + } + if (shouldApply) { // The node is loaded and we're on main. // Quite the opposite of setNeedsDisplay, we must call __setNeedsLayout before messaging // the view or layer to ensure that measurement and implicitly added subnodes have been handled. - - // Calling __setNeedsLayout while holding the property lock can cause deadlocks - _bridge_prologue_write_unlock; [self __setNeedsLayout]; - _bridge_prologue_write; - _messageToViewOrLayer(setNeedsLayout); - } else if (__loaded(self)) { + [viewOrLayer setNeedsLayout]; + } else if (loaded) { // The node is loaded but we're not on main. - // We will call [self __setNeedsLayout] when we apply - // the pending state. We need to call it on main if the node is loaded - // to support automatic subnode management. + // We will call [self __setNeedsLayout] when we apply the pending state. + // We need to call it on main if the node is loaded to support automatic subnode management. + _bridge_prologue_write; [ASDisplayNodeGetPendingState(self) setNeedsLayout]; } else { // The node is not loaded and we're not on main. - _bridge_prologue_write_unlock; [self __setNeedsLayout]; + } +} + +- (void)layoutIfNeeded +{ + BOOL shouldApply = NO; + BOOL loaded = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + loaded = __loaded(self); + viewOrLayer = _view ?: _layer; + } + + if (shouldApply) { + // The node is loaded and we're on main. + // Message the view or layer which in turn will call __layout on us (see -[_ASDisplayLayer layoutSublayers]). + [viewOrLayer layoutIfNeeded]; + } else if (loaded) { + // The node is loaded but we're not on main. + // We will call layoutIfNeeded on the view or layer when we apply the pending state. __layout will in turn be called on us (see -[_ASDisplayLayer layoutSublayers]). + // We need to call it on main if the node is loaded to support automatic subnode management. _bridge_prologue_write; + [ASDisplayNodeGetPendingState(self) layoutIfNeeded]; + } else { + // The node is not loaded and we're not on main. + [self __layout]; } } diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 6f0a5426f2..0baad11298 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -24,7 +24,8 @@ // Properties int needsDisplay:1; int needsLayout:1; - + int layoutIfNeeded:1; + // Flags indicating that a given property should be applied to the view at creation int setClipsToBounds:1; int setOpaque:1; @@ -272,6 +273,11 @@ - (void)setNeedsLayout _flags.needsLayout = YES; } +- (void)layoutIfNeeded +{ + _flags.layoutIfNeeded = YES; +} + - (void)setClipsToBounds:(BOOL)flag { clipsToBounds = flag; @@ -761,9 +767,6 @@ - (void)applyToLayer:(CALayer *)layer if (flags.setEdgeAntialiasingMask) layer.edgeAntialiasingMask = edgeAntialiasingMask; - if (flags.needsLayout) - [layer setNeedsLayout]; - if (flags.setAsyncTransactionContainer) layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; @@ -771,6 +774,12 @@ - (void)applyToLayer:(CALayer *)layer ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); ASPendingStateApplyMetricsToLayer(self, layer); + + if (flags.needsLayout) + [layer setNeedsLayout]; + + if (flags.layoutIfNeeded) + [layer layoutIfNeeded]; } - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPropertiesHandling @@ -889,9 +898,6 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr if (flags.setEdgeAntialiasingMask) layer.edgeAntialiasingMask = edgeAntialiasingMask; - if (flags.needsLayout) - [view setNeedsLayout]; - if (flags.setAsyncTransactionContainer) view.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; @@ -955,6 +961,12 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr } else { ASPendingStateApplyMetricsToLayer(self, layer); } + + if (flags.needsLayout) + [view setNeedsLayout]; + + if (flags.layoutIfNeeded) + [view layoutIfNeeded]; } // FIXME: Make this more efficient by tracking which properties are set rather than reading everything.