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

Commit 603e955

Browse files
Adlai Hollergarrettmoon
Adlai Holler
authored andcommitted
Improve Handling of Rasterized Node Interface & Hierarchy States (#2731)
* Improve handling of rasterize node interface states and testing * Fix harder * Finish fixes and move rasterization flag into beta header * Re-enable rasterization in ASDKgram * Re-enable working test * Only do it in debug
1 parent 081686b commit 603e955

10 files changed

+190
-102
lines changed

AsyncDisplayKit/ASDisplayNode+Beta.h

+22
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,28 @@ typedef struct {
123123
*/
124124
+ (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode;
125125

126+
/**
127+
* @abstract Whether to draw all descendant nodes' layers/views into this node's layer/view's backing store.
128+
*
129+
* @discussion
130+
* When set to YES, causes all descendant nodes' layers/views to be drawn directly into this node's layer/view's backing
131+
* store. Defaults to NO.
132+
*
133+
* If a node's descendants are static (never animated or never change attributes after creation) then that node is a
134+
* good candidate for rasterization. Rasterizing descendants has two main benefits:
135+
* 1) Backing stores for descendant layers are not created. Instead the layers are drawn directly into the rasterized
136+
* container. This can save a great deal of memory.
137+
* 2) Since the entire subtree is drawn into one backing store, compositing and blending are eliminated in that subtree
138+
* which can help improve animation/scrolling/etc performance.
139+
*
140+
* Rasterization does not currently support descendants with transform, sublayerTransform, or alpha. Those properties
141+
* will be ignored when rasterizing descendants.
142+
*
143+
* Note: this has nothing to do with -[CALayer shouldRasterize], which doesn't work with ASDisplayNode's asynchronous
144+
* rendering model.
145+
*/
146+
@property (nonatomic, assign) BOOL shouldRasterizeDescendants;
147+
126148
@end
127149

128150
NS_ASSUME_NONNULL_END

AsyncDisplayKit/ASDisplayNode.h

-22
Original file line numberDiff line numberDiff line change
@@ -416,28 +416,6 @@ extern NSInteger const ASDefaultDrawingPriority;
416416
*/
417417
@property (nonatomic, assign) BOOL displaysAsynchronously;
418418

419-
/**
420-
* @abstract Whether to draw all descendant nodes' layers/views into this node's layer/view's backing store.
421-
*
422-
* @discussion
423-
* When set to YES, causes all descendant nodes' layers/views to be drawn directly into this node's layer/view's backing
424-
* store. Defaults to NO.
425-
*
426-
* If a node's descendants are static (never animated or never change attributes after creation) then that node is a
427-
* good candidate for rasterization. Rasterizing descendants has two main benefits:
428-
* 1) Backing stores for descendant layers are not created. Instead the layers are drawn directly into the rasterized
429-
* container. This can save a great deal of memory.
430-
* 2) Since the entire subtree is drawn into one backing store, compositing and blending are eliminated in that subtree
431-
* which can help improve animation/scrolling/etc performance.
432-
*
433-
* Rasterization does not currently support descendants with transform, sublayerTransform, or alpha. Those properties
434-
* will be ignored when rasterizing descendants.
435-
*
436-
* Note: this has nothing to do with -[CALayer shouldRasterize], which doesn't work with ASDisplayNode's asynchronous
437-
* rendering model.
438-
*/
439-
@property (nonatomic, assign) BOOL shouldRasterizeDescendants;
440-
441419
/**
442420
* @abstract Prevent the node's layer from displaying.
443421
*

AsyncDisplayKit/ASDisplayNode.mm

+55-63
Original file line numberDiff line numberDiff line change
@@ -1422,13 +1422,15 @@ - (BOOL)shouldRasterizeDescendants
14221422
- (void)setShouldRasterizeDescendants:(BOOL)shouldRasterize
14231423
{
14241424
ASDisplayNodeAssertThreadAffinity(self);
1425+
BOOL rasterizedFromSelfOrAncestor = NO;
14251426
{
14261427
ASDN::MutexLocker l(__instanceLock__);
14271428

14281429
if (_flags.shouldRasterizeDescendants == shouldRasterize)
14291430
return;
14301431

14311432
_flags.shouldRasterizeDescendants = shouldRasterize;
1433+
rasterizedFromSelfOrAncestor = shouldRasterize || ASHierarchyStateIncludesRasterized(_hierarchyState);
14321434
}
14331435

14341436
if (self.isNodeLoaded) {
@@ -1438,18 +1440,18 @@ - (void)setShouldRasterizeDescendants:(BOOL)shouldRasterize
14381440
[self recursivelyClearContents];
14391441

14401442
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode *node) {
1441-
if (shouldRasterize) {
1443+
if (rasterizedFromSelfOrAncestor) {
14421444
[node enterHierarchyState:ASHierarchyStateRasterized];
1443-
[node __unloadNode];
1445+
if (node.isNodeLoaded) {
1446+
[node __unloadNode];
1447+
}
14441448
} else {
14451449
[node exitHierarchyState:ASHierarchyStateRasterized];
1446-
[node __loadNode];
1450+
// We can avoid eagerly loading this node. We will load it on-demand as usual.
14471451
}
14481452
});
1449-
if (!shouldRasterize) {
1450-
// At this point all of our subnodes have their layers or views recreated, but we haven't added
1451-
// them to ours yet. This is because our node is already loaded, and the above recursion
1452-
// is only performed on our subnodes -- not self.
1453+
if (!rasterizedFromSelfOrAncestor) {
1454+
// If we are not going to rasterize at all, go ahead and set up our view hierarchy.
14531455
[self _addSubnodeViewsAndLayers];
14541456
}
14551457

@@ -1460,7 +1462,7 @@ - (void)setShouldRasterizeDescendants:(BOOL)shouldRasterize
14601462
}
14611463
} else {
14621464
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode *node) {
1463-
if (shouldRasterize) {
1465+
if (rasterizedFromSelfOrAncestor) {
14641466
[node enterHierarchyState:ASHierarchyStateRasterized];
14651467
} else {
14661468
[node exitHierarchyState:ASHierarchyStateRasterized];
@@ -1887,6 +1889,7 @@ ASDISPLAYNODE_INLINE BOOL nodeIsInRasterizedTree(ASDisplayNode *node) {
18871889
/*
18881890
* Central private helper method that should eventually be called if submethods add, insert or replace subnodes
18891891
* You must hold __instanceLock__ to call this.
1892+
* This method is called with thread affinity.
18901893
*
18911894
* @param subnode The subnode to insert
18921895
* @param subnodeIndex The index in _subnodes to insert it
@@ -1930,15 +1933,22 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnod
19301933
// This call will apply our .hierarchyState to the new subnode.
19311934
// If we are a managed hierarchy, as in ASCellNode trees, it will also apply our .interfaceState.
19321935
[subnode __setSupernode:self];
1933-
1934-
// Don't bother inserting the view/layer if in a rasterized subtree, because there are no layers in the hierarchy and
1935-
// none of this could possibly work.
1936-
if (nodeIsInRasterizedTree(self) == NO && self.nodeLoaded) {
1937-
// If node is loaded insert the subnode otherwise wait until the node get's loaded
1938-
ASPerformBlockOnMainThread(^{
1939-
[self _insertSubnodeSubviewOrSublayer:subnode atIndex:sublayerIndex];
1936+
1937+
// If this subnode will be rasterized, update its hierarchy state & enter hierarchy if needed
1938+
if (nodeIsInRasterizedTree(self)) {
1939+
ASDisplayNodePerformBlockOnEveryNodeBFS(subnode, ^(ASDisplayNode * _Nonnull node) {
1940+
[node enterHierarchyState:ASHierarchyStateRasterized];
1941+
if (node.isNodeLoaded) {
1942+
[node __unloadNode];
1943+
}
19401944
});
1941-
}
1945+
if (_flags.isInHierarchy) {
1946+
[subnode __enterHierarchy];
1947+
}
1948+
} else if (self.nodeLoaded) {
1949+
// If not rasterizing, and node is loaded insert the subview/sublayer now.
1950+
[self _insertSubnodeSubviewOrSublayer:subnode atIndex:sublayerIndex];
1951+
} // Otherwise we will insert subview/sublayer when we get loaded
19421952

19431953
ASDisplayNodeAssert(disableNotifications == shouldDisableNotificationsForMovingBetweenParents(oldParent, self), @"Invariant violated");
19441954
if (disableNotifications) {
@@ -1968,10 +1978,7 @@ - (void)_insertSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode atIndex:(NSInte
19681978
if (canUseViewAPI(self, subnode)) {
19691979
[_view insertSubview:subnode.view atIndex:idx];
19701980
} else {
1971-
#pragma clang diagnostic push
1972-
#pragma clang diagnostic ignored "-Wconversion"
1973-
[_layer insertSublayer:subnode.layer atIndex:idx];
1974-
#pragma clang diagnostic pop
1981+
[_layer insertSublayer:subnode.layer atIndex:(unsigned int)idx];
19751982
}
19761983
}
19771984

@@ -2032,7 +2039,7 @@ - (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *
20322039
return;
20332040
}
20342041

2035-
if ([oldSubnode _deallocSafeSupernode] != self) {
2042+
if (oldSubnode.supernode != self) {
20362043
ASDisplayNodeFailAssert(@"Old Subnode to replace must be a subnode");
20372044
return;
20382045
}
@@ -2076,7 +2083,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)be
20762083
return;
20772084
}
20782085

2079-
if ([below _deallocSafeSupernode] != self) {
2086+
if (below.supernode != self) {
20802087
ASDisplayNodeFailAssert(@"Node to insert below must be a subnode");
20812088
return;
20822089
}
@@ -2101,7 +2108,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)be
21012108

21022109
// If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to
21032110
// insert it will mess up our calculation
2104-
if ([subnode _deallocSafeSupernode] == self) {
2111+
if (subnode.supernode == self) {
21052112
NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode];
21062113
if (currentIndexInSubnodes < belowSubnodeIndex) {
21072114
belowSubnodeIndex--;
@@ -2138,7 +2145,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)ab
21382145
return;
21392146
}
21402147

2141-
if ([above _deallocSafeSupernode] != self) {
2148+
if (above.supernode != self) {
21422149
ASDisplayNodeFailAssert(@"Node to insert above must be a subnode");
21432150
return;
21442151
}
@@ -2162,7 +2169,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)ab
21622169

21632170
// If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to
21642171
// insert it will mess up our calculation
2165-
if ([subnode _deallocSafeSupernode] == self) {
2172+
if (subnode.supernode == self) {
21662173
NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode];
21672174
if (currentIndexInSubnodes <= aboveSubnodeIndex) {
21682175
aboveSubnodeIndex--;
@@ -2228,7 +2235,7 @@ - (void)_removeSubnode:(ASDisplayNode *)subnode
22282235

22292236
// Don't call self.supernode here because that will retain/autorelease the supernode. This method -_removeSupernode: is often called while tearing down a node hierarchy, and the supernode in question might be in the middle of its -dealloc. The supernode is never messaged, only compared by value, so this is safe.
22302237
// The particular issue that triggers this edge case is when a node calls -removeFromSupernode on a subnode from within its own -dealloc method.
2231-
if (!subnode || [subnode _deallocSafeSupernode] != self) {
2238+
if (!subnode || subnode.supernode != self) {
22322239
return;
22332240
}
22342241

@@ -2253,23 +2260,17 @@ - (void)_removeFromSupernode
22532260
__weak ASDisplayNode *supernode = _supernode;
22542261
__weak UIView *view = _view;
22552262
__weak CALayer *layer = _layer;
2256-
BOOL layerBacked = _flags.layerBacked;
2257-
BOOL isNodeLoaded = (layer != nil || view != nil);
22582263
__instanceLock__.unlock();
22592264

22602265
// Clear supernode's reference to us before removing the view from the hierarchy, as _ASDisplayView
22612266
// will trigger us to clear our _supernode pointer in willMoveToSuperview:nil.
22622267
// This may result in removing the last strong reference, triggering deallocation after this method.
22632268
[supernode _removeSubnode:self];
22642269

2265-
if (isNodeLoaded && (supernode == nil || supernode.isNodeLoaded)) {
2266-
ASPerformBlockOnMainThread(^{
2267-
if (layerBacked || supernode.layerBacked) {
2268-
[layer removeFromSuperlayer];
2269-
} else {
2270-
[view removeFromSuperview];
2271-
}
2272-
});
2270+
if (view != nil) {
2271+
[view removeFromSuperview];
2272+
} else if (layer != nil) {
2273+
[layer removeFromSuperlayer];
22732274
}
22742275
}
22752276

@@ -2328,12 +2329,10 @@ - (void)__enterHierarchy
23282329
if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
23292330
_flags.isEnteringHierarchy = YES;
23302331
_flags.isInHierarchy = YES;
2331-
2332-
if (_flags.shouldRasterizeDescendants) {
2333-
// Nodes that are descendants of a rasterized container do not have views or layers, and so cannot receive visibility notifications directly via orderIn/orderOut CALayer actions. Manually send visibility notifications to rasterized descendants.
2334-
[self _recursiveWillEnterHierarchy];
2335-
} else {
2336-
[self willEnterHierarchy];
2332+
2333+
[self willEnterHierarchy];
2334+
for (ASDisplayNode *subnode in self.subnodes) {
2335+
[subnode __enterHierarchy];
23372336
}
23382337
_flags.isEnteringHierarchy = NO;
23392338

@@ -2369,13 +2368,10 @@ - (void)__exitHierarchy
23692368

23702369
[self.asyncLayer cancelAsyncDisplay];
23712370

2372-
if (_flags.shouldRasterizeDescendants) {
2373-
// Nodes that are descendants of a rasterized container do not have views or layers, and so cannot receive visibility notifications directly via orderIn/orderOut CALayer actions. Manually send visibility notifications to rasterized descendants.
2374-
[self _recursiveDidExitHierarchy];
2375-
} else {
2376-
[self didExitHierarchy];
2371+
[self didExitHierarchy];
2372+
for (ASDisplayNode *subnode in self.subnodes) {
2373+
[subnode __exitHierarchy];
23772374
}
2378-
23792375
_flags.isExitingHierarchy = NO;
23802376
}
23812377
}
@@ -2416,19 +2412,13 @@ - (NSArray *)subnodes
24162412
return ([_subnodes copy] ?: @[]);
24172413
}
24182414

2415+
// NOTE: This method must be dealloc-safe (should not retain self).
24192416
- (ASDisplayNode *)supernode
24202417
{
24212418
ASDN::MutexLocker l(__instanceLock__);
24222419
return _supernode;
24232420
}
24242421

2425-
// This is a thread-method to return the supernode without causing it to be retained autoreleased. See -_removeSubnode: for details.
2426-
- (ASDisplayNode *)_deallocSafeSupernode
2427-
{
2428-
ASDN::MutexLocker l(__instanceLock__);
2429-
return _supernode;
2430-
}
2431-
24322422
- (void)__setSupernode:(ASDisplayNode *)newSupernode
24332423
{
24342424
BOOL supernodeDidChange = NO;
@@ -2471,7 +2461,7 @@ - (void)__setSupernode:(ASDisplayNode *)newSupernode
24712461

24722462
// Propagate down the new pending transition id
24732463
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) {
2474-
node.pendingTransitionID = _pendingTransitionID;
2464+
node.pendingTransitionID = pendingTransitionId;
24752465
});
24762466
}
24772467
}
@@ -2485,6 +2475,14 @@ - (void)__setSupernode:(ASDisplayNode *)newSupernode
24852475
stateToEnterOrExit |= ASHierarchyStateLayoutPending;
24862476

24872477
[self exitHierarchyState:stateToEnterOrExit];
2478+
2479+
// We only need to explicitly exit hierarchy here if we were rasterized.
2480+
// Otherwise we will exit the hierarchy when our view/layer does so
2481+
// which has some nice carry-over machinery to handle cases where we are removed from a hierarchy
2482+
// and then added into it again shortly after.
2483+
if (parentWasOrIsRasterized && _flags.isInHierarchy) {
2484+
[self __exitHierarchy];
2485+
}
24882486
}
24892487
}
24902488
}
@@ -3559,12 +3557,6 @@ - (BOOL)isInHierarchy
35593557
return _flags.isInHierarchy;
35603558
}
35613559

3562-
- (void)setInHierarchy:(BOOL)inHierarchy
3563-
{
3564-
ASDN::MutexLocker l(__instanceLock__);
3565-
_flags.isInHierarchy = inHierarchy;
3566-
}
3567-
35683560
- (id<ASLayoutElement>)finalLayoutElement
35693561
{
35703562
return self;
@@ -3610,7 +3602,7 @@ - (ASEventLog *)eventLog
36103602
}
36113603

36123604
// Check supernode so that if we are cell node we don't find self.
3613-
ASCellNode *cellNode = ASDisplayNodeFindFirstSupernodeOfClass([self _deallocSafeSupernode], [ASCellNode class]);
3605+
ASCellNode *cellNode = ASDisplayNodeFindFirstSupernodeOfClass(self.supernode, [ASCellNode class]);
36143606
if (cellNode != nil) {
36153607
[result addObject:@{ @"cellNode" : ASObjectDescriptionMakeTiny(cellNode) }];
36163608
}

AsyncDisplayKit/ASDisplayNodeExtras.h

+14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@
1414
#import <AsyncDisplayKit/ASBaseDefines.h>
1515
#import <AsyncDisplayKit/ASDisplayNode.h>
1616

17+
/**
18+
* Sets the debugName field for these nodes to the given symbol names, within the domain of "self.class"
19+
* For instance, in `MYButtonNode` if you call `ASSetDebugNames(self.titleNode, _countNode)` the debug names
20+
* for the nodes will be set to `MYButtonNode.titleNode` and `MYButtonNode.countNode`.
21+
*/
22+
#if DEBUG
23+
#define ASSetDebugNames(...) _ASSetDebugNames(self.class, @"" # __VA_ARGS__, __VA_ARGS__, nil)
24+
#else
25+
#define ASSetDebugNames(...)
26+
#endif
27+
1728
/// For deallocation of objects on the main thread across multiple run loops.
1829
extern void ASPerformMainThreadDeallocation(_Nullable id object);
1930

@@ -163,6 +174,9 @@ extern UIColor *ASDisplayNodeDefaultTintColor() AS_WARN_UNUSED_RESULT;
163174
extern void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node);
164175
extern void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node);
165176

177+
// Not to be called directly.
178+
extern void _ASSetDebugNames(Class _Nonnull owningClass, NSString * _Nonnull names, ASDisplayNode * _Nullable object, ...);
179+
166180
ASDISPLAYNODE_EXTERN_C_END
167181

168182
NS_ASSUME_NONNULL_END

AsyncDisplayKit/ASDisplayNodeExtras.mm

+18
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ extern void ASPerformMainThreadDeallocation(_Nullable id object)
3232
}
3333
}
3434

35+
extern void _ASSetDebugNames(Class _Nonnull owningClass, NSString * _Nonnull names, ASDisplayNode * _Nullable object, ...)
36+
{
37+
NSString *owningClassName = NSStringFromClass(owningClass);
38+
NSArray *nameArray = [names componentsSeparatedByString:@", "];
39+
va_list args;
40+
va_start(args, object);
41+
NSInteger i = 0;
42+
for (ASDisplayNode *node = object; node != nil; node = va_arg(args, id), i++) {
43+
NSMutableString *symbolName = [nameArray[i] mutableCopy];
44+
// Remove any `self.` or `_` prefix
45+
[symbolName replaceOccurrencesOfString:@"self." withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)];
46+
[symbolName replaceOccurrencesOfString:@"_" withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)];
47+
node.debugName = [NSString stringWithFormat:@"%@.%@", owningClassName, symbolName];
48+
}
49+
ASDisplayNodeCAssert(nameArray.count == i, @"Malformed call to ASSetDebugNames: %@", names);
50+
va_end(args);
51+
}
52+
3553
extern ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNode, UIWindow *window)
3654
{
3755
ASDisplayNodeCAssert(![displayNode isLayerBacked], @"displayNode must not be layer backed as it may have a nil window");

0 commit comments

Comments
 (0)