Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AsyncDisplayKit/ASDisplayNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState)
*/
@property (atomic, readonly, assign, getter=isNodeLoaded) BOOL nodeLoaded;

/**
* @abstract Reflects the fact of node and it's subnodes being thread agnostic.
*
* @return YES if node and it's subnodes hierarchy is safe to manipulate on any thread; NO otherwise.
*/
@property (atomic, readonly, assign, getter=isRecursivelyDetachedFromMainThread) BOOL recursivelyDetachedFromMainThread;

/**
* @abstract Returns whether the node rely on a layer instead of a view.
*
Expand Down
51 changes: 51 additions & 0 deletions AsyncDisplayKit/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *i

flags.isInHierarchy = NO;
flags.displaysAsynchronously = YES;
flags.isRecursivelyDetachedFromMainThread = YES;
flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0);
flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0);
if (instance) {
Expand Down Expand Up @@ -444,6 +445,8 @@ - (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked
if (self.placeholderEnabled) {
[self _setupPlaceholderLayer];
}

[self _setRecursivelyDetachedFromMainThread:NO];
}

- (UIView *)view
Expand Down Expand Up @@ -491,6 +494,30 @@ - (BOOL)isNodeLoaded
return (_view != nil || (_flags.layerBacked && _layer != nil));
}

- (BOOL)isRecursivelyDetachedFromMainThread {
ASDN::MutexLocker l(_propertyLock);
return _flags.isRecursivelyDetachedFromMainThread;
}

- (void)setRecursivelyDetachedFromMainThread:(BOOL)recursivelyDetachedFromMainThread {
ASDN::MutexLocker l(_propertyLock);

[self _setRecursivelyDetachedFromMainThread:recursivelyDetachedFromMainThread];
}

- (void)_setRecursivelyDetachedFromMainThread:(BOOL)recursivelyDetachedFromMainThread {
if (_flags.isRecursivelyDetachedFromMainThread == recursivelyDetachedFromMainThread)
return;

_flags.isRecursivelyDetachedFromMainThread = recursivelyDetachedFromMainThread;

if (recursivelyDetachedFromMainThread) {
[self.supernode __updateMainThreadDetachment];
} else {
self.supernode.recursivelyDetachedFromMainThread = recursivelyDetachedFromMainThread;
}
}

- (NSString *)name
{
ASDN::MutexLocker l(_propertyLock);
Expand Down Expand Up @@ -1166,6 +1193,10 @@ - (void)_addSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode
ASDisplayNodeAssert(subnode.isLayerBacked, @"Cannot add a subview to a layer-backed node; only sublayers permitted.");
[_layer addSublayer:subnode.layer];
}

if (! subnode.recursivelyDetachedFromMainThread){
[self _setRecursivelyDetachedFromMainThread:NO];
}
}

- (void)_addSubnodeViewsAndLayers
Expand Down Expand Up @@ -1380,7 +1411,27 @@ - (void)__setSupernode:(ASDisplayNode *)newSupernode
} else {
[self exitHierarchyState:stateToEnterOrExit];
}
{
ASDN::MutexLocker l(_propertyLock);
if (!_flags.isRecursivelyDetachedFromMainThread) {
newSupernode.recursivelyDetachedFromMainThread = NO;
[oldSupernode __updateMainThreadDetachment];
}
}
}
}

// Check if we still have any detached subnode and update detachment accordingly
- (void)__updateMainThreadDetachment {
BOOL allSubnodesAreDetached = YES;
for (ASDisplayNode *subnode in _subnodes) {
if (!subnode.recursivelyDetachedFromMainThread) {
allSubnodesAreDetached = NO;
break;
}
}

[self _setRecursivelyDetachedFromMainThread:allSubnodesAreDetached];
}

// Track that a node will be displayed as part of the current node hierarchy.
Expand Down
16 changes: 8 additions & 8 deletions AsyncDisplayKit/Details/ASCollectionDataController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ - (void)prepareForReloadData
[self _populateSupplementaryNodesOfKind:kind withMutableNodes:nodes mutableIndexPaths:indexPaths];
_pendingNodes[kind] = nodes;
_pendingIndexPaths[kind] = indexPaths;
// Measure loaded nodes before leaving the main thread
[self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths];

// Measure main thread attached nodes before leaving the main thread
[self layoutMainThreadAttachedNodes:nodes ofKind:kind atIndexPaths:indexPaths];
}
}

Expand Down Expand Up @@ -89,9 +89,9 @@ - (void)prepareForInsertSections:(NSIndexSet *)sections
[self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths];
_pendingNodes[kind] = nodes;
_pendingIndexPaths[kind] = indexPaths;
// Measure loaded nodes before leaving the main thread
[self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths];

// Measure main thread attached nodes before leaving the main thread
[self layoutMainThreadAttachedNodes:nodes ofKind:kind atIndexPaths:indexPaths];
}
}

Expand Down Expand Up @@ -131,8 +131,8 @@ - (void)prepareForReloadSections:(NSIndexSet *)sections
_pendingNodes[kind] = nodes;
_pendingIndexPaths[kind] = indexPaths;

// Measure loaded nodes before leaving the main thread
[self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths];
// Measure main thread attached nodes before leaving the main thread
[self layoutMainThreadAttachedNodes:nodes ofKind:kind atIndexPaths:indexPaths];
}
}

Expand Down
6 changes: 3 additions & 3 deletions AsyncDisplayKit/Details/ASDataController+Subclasses.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock;

/*
* Perform measurement and layout of loaded nodes on the main thread, skipping unloaded nodes.
* Perform measurement and layout of affined nodes on the main thread, skipping thread agnostic nodes.
*
* @discussion Once nodes have loaded their views, we can't layout in the background so this is a chance
* @discussion Once nodes or any subnodes have loaded their views, we can't layout in the background so this is a chance
* to do so immediately on the main thread.
*/
- (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths;
- (void)layoutMainThreadAttachedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths;

/**
* Provides the size range for a specific node during the layout process.
Expand Down
22 changes: 11 additions & 11 deletions AsyncDisplayKit/Details/ASDataController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,12 @@ - (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(
}
}

- (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths {
- (void)layoutMainThreadAttachedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths {
NSAssert(NSThread.isMainThread, @"Main thread layout must be on the main thread.");

[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, __unused BOOL * stop) {
ASCellNode *node = nodes[idx];
if (node.isNodeLoaded) {
if (!node.isRecursivelyDetachedFromMainThread) {
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath];
[self _layoutNode:node withConstrainedSize:constrainedSize];
}
Expand Down Expand Up @@ -166,17 +166,17 @@ - (void)_layoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSAr

for (NSUInteger k = j; k < j + batchCount; k++) {
ASCellNode *node = nodes[k];
if (!node.isNodeLoaded) {
if (node.isRecursivelyDetachedFromMainThread) {
nodeBoundSizes[k] = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPaths[k]];
}
}

dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSUInteger k = j; k < j + batchCount; k++) {
ASCellNode *node = nodes[k];
// Only measure nodes whose views aren't loaded, since we're in the background.
// We should already have measured loaded nodes before we left the main thread, using layoutLoadedNodes:ofKind:atIndexPaths:
if (!node.isNodeLoaded) {
// Only measure nodes with thread affinity, since we're in the background.
// We should already have measured affined nodes before we left the main thread, using layoutMainThreadAttachedNodes:ofKind:atIndexPaths:
if (node.isRecursivelyDetachedFromMainThread) {
[self _layoutNode:node withConstrainedSize:nodeBoundSizes[k]];
}
}
Expand Down Expand Up @@ -383,7 +383,7 @@ - (void)_reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animat
[self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];

// Measure nodes whose views are loaded before we leave the main thread
[self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths];
[self layoutMainThreadAttachedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths];

// Allow subclasses to perform setup before going into the edit transaction
[self prepareForReloadData];
Expand Down Expand Up @@ -578,7 +578,7 @@ - (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataContro
[self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];

// Measure nodes whose views are loaded before we leave the main thread
[self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths];
[self layoutMainThreadAttachedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths];

[self prepareForInsertSections:sections];

Expand Down Expand Up @@ -636,7 +636,7 @@ - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataContro
// at this time. Thus _editingNodes could be empty and crash in ASIndexPathsForMultidimensional[...]

// Measure nodes whose views are loaded before we leave the main thread
[self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths];
[self layoutMainThreadAttachedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths];

[self prepareForReloadSections:sections];

Expand Down Expand Up @@ -750,7 +750,7 @@ - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDat
}

// Measure nodes whose views are loaded before we leave the main thread
[self layoutLoadedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths];
[self layoutMainThreadAttachedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths];

[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - insertRows: %@", indexPaths);
Expand Down Expand Up @@ -800,7 +800,7 @@ - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDat
}

// Measure nodes whose views are loaded before we leave the main thread
[self layoutLoadedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths];
[self layoutMainThreadAttachedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths];

[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - reloadRows: %@", indexPaths);
Expand Down
4 changes: 4 additions & 0 deletions AsyncDisplayKit/Private/ASDisplayNodeInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
unsigned shouldRasterizeDescendants:1;
unsigned shouldBypassEnsureDisplay:1;
unsigned displaySuspended:1;
unsigned isRecursivelyDetachedFromMainThread:1;

// whether custom drawing is enabled
unsigned implementsDrawRect:1;
Expand Down Expand Up @@ -132,6 +133,9 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
// Changed before calling willEnterHierarchy / didExitHierarchy.
@property (nonatomic, readwrite, assign, getter = isInHierarchy) BOOL inHierarchy;

// Used to track the fact of being thread agnostic over subnodes hierarchy
@property (atomic, readwrite, assign, getter=isRecursivelyDetachedFromMainThread) BOOL recursivelyDetachedFromMainThread;

// Private API for helper functions / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this.
- (BOOL)__visibilityNotificationsDisabled;
- (void)__incrementVisibilityNotificationsDisabled;
Expand Down
27 changes: 27 additions & 0 deletions AsyncDisplayKitTests/ASDisplayNodeTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,33 @@ - (void)testMainThreadDealloc
XCTAssertTrue(didDealloc, @"unexpected node lifetime");
}

- (void)testAttachedNodeEnteringLeavingHierarchy {
ASDisplayNode *parent = [[ASDisplayNode alloc] init];
XCTAssertTrue(parent.isRecursivelyDetachedFromMainThread);

ASDisplayNode *subnode = [[ASDisplayNode alloc] init];
[subnode view];
XCTAssertFalse(subnode.isRecursivelyDetachedFromMainThread);

[parent addSubnode:subnode];
XCTAssertFalse(parent.isRecursivelyDetachedFromMainThread);

[subnode removeFromSupernode];
XCTAssertTrue(parent.isRecursivelyDetachedFromMainThread);
}

- (void)testAttachingSubnode {
ASDisplayNode *parent = [[ASDisplayNode alloc] init];
ASDisplayNode *subnode1 = [[ASDisplayNode alloc] init];
ASDisplayNode *subnode2 = [[ASDisplayNode alloc] init];
[parent addSubnode:subnode1];
[subnode1 addSubnode:subnode2];

XCTAssertTrue(parent.isRecursivelyDetachedFromMainThread);
[subnode2 view];
XCTAssertFalse(parent.isRecursivelyDetachedFromMainThread);
}

- (void)testSubnodes
{
ASDisplayNode *parent = [[ASDisplayNode alloc] init];
Expand Down