Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit dcf858e

Browse files
nguyenhuyAdlai Holler
authored andcommitted
[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
1 parent 3164d8d commit dcf858e

File tree

8 files changed

+124
-44
lines changed

8 files changed

+124
-44
lines changed

Source/ASCollectionNode.mm

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,20 @@ - (void)clearContents
193193
[self.rangeController clearContents];
194194
}
195195

196-
- (void)didExitPreloadState
197-
{
198-
[super didExitPreloadState];
199-
[self.rangeController clearPreloadedData];
200-
}
201-
202196
- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState
203197
{
204198
[super interfaceStateDidChange:newState fromState:oldState];
205199
[ASRangeController layoutDebugOverlayIfNeeded];
206200
}
207201

202+
- (void)didEnterPreloadState
203+
{
204+
// Intentionally allocate the view here so that super will trigger a layout pass on it which in turn will trigger the intial data load.
205+
// We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view.
206+
[self view];
207+
[super didEnterPreloadState];
208+
}
209+
208210
#if ASRangeControllerLoggingEnabled
209211
- (void)didEnterVisibleState
210212
{
@@ -219,6 +221,12 @@ - (void)didExitVisibleState
219221
}
220222
#endif
221223

224+
- (void)didExitPreloadState
225+
{
226+
[super didExitPreloadState];
227+
[self.rangeController clearPreloadedData];
228+
}
229+
222230
#pragma mark Setter / Getter
223231

224232
// TODO: Implement this without the view. Then revisit ASLayoutElementCollectionTableSetTraitCollection

Source/ASDisplayNode.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,11 @@ extern NSInteger const ASDefaultDrawingPriority;
639639
*/
640640
- (void)setNeedsLayout;
641641

642+
/**
643+
* 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.
644+
*/
645+
- (void)layoutIfNeeded;
646+
642647
@property (nonatomic, strong, nullable) id contents; // default=nil
643648
@property (nonatomic, assign) BOOL clipsToBounds; // default==NO
644649
@property (nonatomic, getter=isOpaque) BOOL opaque; // default==YES

Source/ASDisplayNode.mm

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ - (void)invalidateCalculatedLayout
985985

986986
- (void)__layout
987987
{
988-
ASDisplayNodeAssertMainThread();
988+
ASDisplayNodeAssertThreadAffinity(self);
989989
ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
990990

991991
{
@@ -1014,8 +1014,12 @@ - (void)__layout
10141014
[self _locked_layoutPlaceholderIfNecessary];
10151015
}
10161016

1017-
[self layout];
1018-
[self layoutDidFinish];
1017+
[self _layoutSublayouts];
1018+
1019+
ASPerformBlockOnMainThread(^{
1020+
[self layout];
1021+
[self layoutDidFinish];
1022+
});
10191023
}
10201024

10211025
/// Needs to be called with lock held
@@ -1054,7 +1058,7 @@ - (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds
10541058
std::shared_ptr<ASDisplayNodeLayout> nextLayout = _pendingDisplayNodeLayout;
10551059
#define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout->layout.size, boundsSizeForLayout)
10561060

1057-
// nextLayout was likely created by a call to layoutThatFits:, check if is valid and can be applied.
1061+
// nextLayout was likely created by a call to layoutThatFits:, check if it is valid and can be applied.
10581062
// If our bounds size is different than it, or invalid, recalculate. Use #define to avoid nullptr->
10591063
if (nextLayout == nullptr || nextLayout->isDirty() == YES || layoutSizeDifferentFromBounds) {
10601064
// Use the last known constrainedSize passed from a parent during layout (if never, use bounds).
@@ -1405,12 +1409,13 @@ - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size
14051409

14061410
- (void)layout
14071411
{
1408-
[self _layoutSublayouts];
1412+
ASDisplayNodeAssertMainThread();
1413+
// Subclass hook
14091414
}
14101415

14111416
- (void)_layoutSublayouts
14121417
{
1413-
ASDisplayNodeAssertMainThread();
1418+
ASDisplayNodeAssertThreadAffinity(self);
14141419
ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
14151420

14161421
ASLayout *layout;
@@ -3720,6 +3725,10 @@ - (void)didEnterPreloadState
37203725
ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
37213726
[_interfaceStateDelegate didEnterPreloadState];
37223727

3728+
// Trigger a layout pass to ensure all subnodes have the correct size to preload their content.
3729+
// This is important for image nodes, as well as collection and table nodes.
3730+
[self layoutIfNeeded];
3731+
37233732
if (_methodOverrides & ASDisplayNodeMethodOverrideFetchData) {
37243733
#pragma clang diagnostic push
37253734
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

Source/ASTableNode.mm

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,20 @@ - (void)clearContents
122122
[self.rangeController clearContents];
123123
}
124124

125-
- (void)didExitPreloadState
126-
{
127-
[super didExitPreloadState];
128-
[self.rangeController clearPreloadedData];
129-
}
130-
131125
- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState
132126
{
133127
[super interfaceStateDidChange:newState fromState:oldState];
134128
[ASRangeController layoutDebugOverlayIfNeeded];
135129
}
136130

131+
- (void)didEnterPreloadState
132+
{
133+
// Intentionally allocate the view here so that super will trigger a layout pass on it which in turn will trigger the intial data load.
134+
// We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view.
135+
[self view];
136+
[super didEnterPreloadState];
137+
}
138+
137139
#if ASRangeControllerLoggingEnabled
138140
- (void)didEnterVisibleState
139141
{
@@ -148,6 +150,12 @@ - (void)didExitVisibleState
148150
}
149151
#endif
150152

153+
- (void)didExitPreloadState
154+
{
155+
[super didExitPreloadState];
156+
[self.rangeController clearPreloadedData];
157+
}
158+
151159
#pragma mark Setter / Getter
152160

153161
// TODO: Implement this without the view. Then revisit ASLayoutElementCollectionTableSetTraitCollection

Source/Details/UIView+ASConvenience.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
4242

4343
- (void)setNeedsDisplay;
4444
- (void)setNeedsLayout;
45+
- (void)layoutIfNeeded;
4546

4647
@end
4748

Source/Details/_ASCollectionViewCell.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ - (void)prepareForReuse
5858
*/
5959
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
6060
{
61+
[super applyLayoutAttributes:layoutAttributes];
6162
self.layoutAttributes = layoutAttributes;
6263
}
6364

Source/Private/ASDisplayNode+UIViewBridge.mm

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
#if DISPLAYNODE_USE_LOCKS
4141
#define _bridge_prologue_read ASDN::MutexLocker l(__instanceLock__); ASDisplayNodeAssertThreadAffinity(self)
4242
#define _bridge_prologue_write ASDN::MutexLocker l(__instanceLock__)
43-
#define _bridge_prologue_write_unlock ASDN::MutexUnlocker u(__instanceLock__)
4443
#else
4544
#define _bridge_prologue_read ASDisplayNodeAssertThreadAffinity(self)
4645
#define _bridge_prologue_write
@@ -79,8 +78,6 @@ ASDISPLAYNODE_INLINE BOOL ASDisplayNodeShouldApplyBridgedWriteToView(ASDisplayNo
7978
#define _setToLayer(layerProperty, layerValueExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \
8079
if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNodeGetPendingState(self).layerProperty = (layerValueExpr); }
8180

82-
#define _messageToViewOrLayer(viewAndLayerSelector) (_view ? [_view viewAndLayerSelector] : [_layer viewAndLayerSelector])
83-
8481
/**
8582
* 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,
8683
* 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
301298

302299
- (void)setNeedsDisplay
303300
{
304-
_bridge_prologue_write;
305-
if (_hierarchyState & ASHierarchyStateRasterized) {
301+
BOOL isRasterized = NO;
302+
BOOL shouldApply = NO;
303+
id viewOrLayer = nil;
304+
{
305+
_bridge_prologue_write;
306+
isRasterized = _hierarchyState & ASHierarchyStateRasterized;
307+
shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);
308+
viewOrLayer = _view ?: _layer;
309+
}
310+
311+
if (isRasterized) {
306312
ASPerformBlockOnMainThread(^{
307313
// The below operation must be performed on the main thread to ensure against an extremely rare deadlock, where a parent node
308314
// begins materializing the view / layer hierarchy (locking itself or a descendant) while this node walks up
@@ -319,13 +325,13 @@ - (void)setNeedsDisplay
319325
[rasterizedContainerNode setNeedsDisplay];
320326
});
321327
} else {
322-
BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);
323328
if (shouldApply) {
324329
// If not rasterized, and the node is loaded (meaning we certainly have a view or layer), send a
325330
// message to the view/layer first. This is because __setNeedsDisplay calls as scheduleNodeForDisplay,
326331
// which may call -displayIfNeeded. We want to ensure the needsDisplay flag is set now, and then cleared.
327-
_messageToViewOrLayer(setNeedsDisplay);
332+
[viewOrLayer setNeedsDisplay];
328333
} else {
334+
_bridge_prologue_write;
329335
[ASDisplayNodeGetPendingState(self) setNeedsDisplay];
330336
}
331337
[self __setNeedsDisplay];
@@ -334,29 +340,59 @@ - (void)setNeedsDisplay
334340

335341
- (void)setNeedsLayout
336342
{
337-
_bridge_prologue_write;
338-
BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);
343+
BOOL shouldApply = NO;
344+
BOOL loaded = NO;
345+
id viewOrLayer = nil;
346+
{
347+
_bridge_prologue_write;
348+
shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);
349+
loaded = __loaded(self);
350+
viewOrLayer = _view ?: _layer;
351+
}
352+
339353
if (shouldApply) {
340354
// The node is loaded and we're on main.
341355
// Quite the opposite of setNeedsDisplay, we must call __setNeedsLayout before messaging
342356
// the view or layer to ensure that measurement and implicitly added subnodes have been handled.
343-
344-
// Calling __setNeedsLayout while holding the property lock can cause deadlocks
345-
_bridge_prologue_write_unlock;
346357
[self __setNeedsLayout];
347-
_bridge_prologue_write;
348-
_messageToViewOrLayer(setNeedsLayout);
349-
} else if (__loaded(self)) {
358+
[viewOrLayer setNeedsLayout];
359+
} else if (loaded) {
350360
// The node is loaded but we're not on main.
351-
// We will call [self __setNeedsLayout] when we apply
352-
// the pending state. We need to call it on main if the node is loaded
353-
// to support automatic subnode management.
361+
// We will call [self __setNeedsLayout] when we apply the pending state.
362+
// We need to call it on main if the node is loaded to support automatic subnode management.
363+
_bridge_prologue_write;
354364
[ASDisplayNodeGetPendingState(self) setNeedsLayout];
355365
} else {
356366
// The node is not loaded and we're not on main.
357-
_bridge_prologue_write_unlock;
358367
[self __setNeedsLayout];
368+
}
369+
}
370+
371+
- (void)layoutIfNeeded
372+
{
373+
BOOL shouldApply = NO;
374+
BOOL loaded = NO;
375+
id viewOrLayer = nil;
376+
{
377+
_bridge_prologue_write;
378+
shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);
379+
loaded = __loaded(self);
380+
viewOrLayer = _view ?: _layer;
381+
}
382+
383+
if (shouldApply) {
384+
// The node is loaded and we're on main.
385+
// Message the view or layer which in turn will call __layout on us (see -[_ASDisplayLayer layoutSublayers]).
386+
[viewOrLayer layoutIfNeeded];
387+
} else if (loaded) {
388+
// The node is loaded but we're not on main.
389+
// 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]).
390+
// We need to call it on main if the node is loaded to support automatic subnode management.
359391
_bridge_prologue_write;
392+
[ASDisplayNodeGetPendingState(self) layoutIfNeeded];
393+
} else {
394+
// The node is not loaded and we're not on main.
395+
[self __layout];
360396
}
361397
}
362398

Source/Private/_ASPendingState.mm

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
// Properties
2525
int needsDisplay:1;
2626
int needsLayout:1;
27-
27+
int layoutIfNeeded:1;
28+
2829
// Flags indicating that a given property should be applied to the view at creation
2930
int setClipsToBounds:1;
3031
int setOpaque:1;
@@ -272,6 +273,11 @@ - (void)setNeedsLayout
272273
_flags.needsLayout = YES;
273274
}
274275

276+
- (void)layoutIfNeeded
277+
{
278+
_flags.layoutIfNeeded = YES;
279+
}
280+
275281
- (void)setClipsToBounds:(BOOL)flag
276282
{
277283
clipsToBounds = flag;
@@ -761,16 +767,19 @@ - (void)applyToLayer:(CALayer *)layer
761767
if (flags.setEdgeAntialiasingMask)
762768
layer.edgeAntialiasingMask = edgeAntialiasingMask;
763769

764-
if (flags.needsLayout)
765-
[layer setNeedsLayout];
766-
767770
if (flags.setAsyncTransactionContainer)
768771
layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer;
769772

770773
if (flags.setOpaque)
771774
ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired");
772775

773776
ASPendingStateApplyMetricsToLayer(self, layer);
777+
778+
if (flags.needsLayout)
779+
[layer setNeedsLayout];
780+
781+
if (flags.layoutIfNeeded)
782+
[layer layoutIfNeeded];
774783
}
775784

776785
- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPropertiesHandling
@@ -889,9 +898,6 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr
889898
if (flags.setEdgeAntialiasingMask)
890899
layer.edgeAntialiasingMask = edgeAntialiasingMask;
891900

892-
if (flags.needsLayout)
893-
[view setNeedsLayout];
894-
895901
if (flags.setAsyncTransactionContainer)
896902
view.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer;
897903

@@ -955,6 +961,12 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr
955961
} else {
956962
ASPendingStateApplyMetricsToLayer(self, layer);
957963
}
964+
965+
if (flags.needsLayout)
966+
[view setNeedsLayout];
967+
968+
if (flags.layoutIfNeeded)
969+
[view layoutIfNeeded];
958970
}
959971

960972
// FIXME: Make this more efficient by tracking which properties are set rather than reading everything.

0 commit comments

Comments
 (0)