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

Commit a2d4dc5

Browse files
author
Adlai Holler
authored
Merge pull request #1839 from maicki/MSAsyncMeasure
[ASDisplayNode] Allow measure always be off the main thread
2 parents 0906958 + 9de014f commit a2d4dc5

File tree

7 files changed

+209
-23
lines changed

7 files changed

+209
-23
lines changed

AsyncDisplayKit/ASDisplayNode.mm

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#import <objc/runtime.h>
1717
#import <deque>
18+
#import <queue>
1819

1920
#import "_ASAsyncTransaction.h"
2021
#import "_ASAsyncTransactionContainer+Private.h"
@@ -631,7 +632,7 @@ - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
631632
if (! [self shouldMeasureWithSizeRange:constrainedSize]) {
632633
return _layout;
633634
}
634-
635+
635636
[self cancelLayoutTransitionsInProgress];
636637

637638
ASLayout *previousLayout = _layout;
@@ -642,13 +643,14 @@ - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
642643
pendingLayout:newLayout
643644
previousLayout:previousLayout];
644645
} else {
645-
ASLayoutTransition *layoutContext;
646+
ASLayoutTransition *layoutTransition = nil;
646647
if (self.usesImplicitHierarchyManagement) {
647-
layoutContext = [[ASLayoutTransition alloc] initWithNode:self
648-
pendingLayout:newLayout
649-
previousLayout:previousLayout];
648+
layoutTransition = [[ASLayoutTransition alloc] initWithNode:self
649+
pendingLayout:newLayout
650+
previousLayout:previousLayout];
650651
}
651-
[self applyLayout:newLayout layoutContext:layoutContext];
652+
653+
[self _applyLayout:newLayout layoutTransition:layoutTransition];
652654
[self _completeLayoutCalculation];
653655
}
654656

@@ -685,6 +687,11 @@ - (ASLayoutableType)layoutableType
685687
return ASLayoutableTypeDisplayNode;
686688
}
687689

690+
- (BOOL)canLayoutAsynchronous
691+
{
692+
return !self.isNodeLoaded;
693+
}
694+
688695
#pragma mark - Layout Transition
689696

690697
- (void)transitionLayoutWithAnimation:(BOOL)animated
@@ -716,7 +723,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
716723
int32_t transitionID = [self _startNewTransition];
717724

718725
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
719-
ASDisplayNodeAssert([node _hasTransitionInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one.");
726+
ASDisplayNodeAssert([node _isTransitionInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one.");
720727
node.hierarchyState |= ASHierarchyStateLayoutPending;
721728
node.pendingTransitionID = transitionID;
722729
});
@@ -755,10 +762,10 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
755762
}
756763

757764
ASLayout *previousLayout = _layout;
758-
[self applyLayout:newLayout layoutContext:nil];
765+
[self _applyLayout:newLayout layoutTransition:nil];
759766

760767
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
761-
[node applyPendingLayoutContext];
768+
[node _applyPendingLayoutContext];
762769
[node _completeLayoutCalculation];
763770
node.hierarchyState &= (~ASHierarchyStateLayoutPending);
764771
});
@@ -781,6 +788,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
781788
});
782789
};
783790

791+
// TODO ihm: Can we always push the measure to the background thread and remove the parameter from the API?
784792
if (shouldMeasureAsync) {
785793
ASPerformBlockOnBackgroundThread(transitionBlock);
786794
} else {
@@ -818,7 +826,7 @@ - (void)calculatedLayoutDidChange
818826
- (void)cancelLayoutTransitionsInProgress
819827
{
820828
ASDN::MutexLocker l(_propertyLock);
821-
if ([self _hasTransitionInProgress]) {
829+
if ([self _isTransitionInProgress]) {
822830
// Cancel transition in progress
823831
[self _finishOrCancelTransition];
824832

@@ -841,10 +849,10 @@ - (void)setUsesImplicitHierarchyManagement:(BOOL)value
841849
_usesImplicitHierarchyManagement = value;
842850
}
843851

844-
- (BOOL)_hasTransitionInProgress
852+
- (BOOL)_isTransitionInProgress
845853
{
846-
ASDN::MutexLocker l(_propertyLock);
847-
return _transitionInProgress;
854+
ASDN::MutexLocker l(_propertyLock);
855+
return _transitionInProgress;
848856
}
849857

850858
/// Starts a new transition and returns the transition id
@@ -2412,16 +2420,16 @@ - (void)exitHierarchyState:(ASHierarchyState)hierarchyState
24122420
});
24132421
}
24142422

2415-
- (void)applyPendingLayoutContext
2423+
- (void)_applyPendingLayoutContext
24162424
{
24172425
ASDN::MutexLocker l(_propertyLock);
24182426
if (_pendingLayoutTransition) {
2419-
[self applyLayout:_pendingLayoutTransition.pendingLayout layoutContext:_pendingLayoutTransition];
2427+
[self _applyLayout:_pendingLayoutTransition.pendingLayout layoutTransition:_pendingLayoutTransition];
24202428
_pendingLayoutTransition = nil;
24212429
}
24222430
}
24232431

2424-
- (void)applyLayout:(ASLayout *)layout layoutContext:(ASLayoutTransition *)layoutContext
2432+
- (void)_applyLayout:(ASLayout *)layout layoutTransition:(ASLayoutTransition *)layoutTransition
24252433
{
24262434
ASDN::MutexLocker l(_propertyLock);
24272435
_layout = layout;
@@ -2430,10 +2438,22 @@ - (void)applyLayout:(ASLayout *)layout layoutContext:(ASLayoutTransition *)layou
24302438
ASDisplayNodeAssertTrue(layout.size.width >= 0.0);
24312439
ASDisplayNodeAssertTrue(layout.size.height >= 0.0);
24322440

2433-
if (self.usesImplicitHierarchyManagement && layoutContext != nil) {
2434-
[layoutContext applySubnodeInsertions];
2435-
[layoutContext applySubnodeRemovals];
2441+
if (layoutTransition == nil || self.usesImplicitHierarchyManagement == NO) {
2442+
return;
24362443
}
2444+
2445+
// Trampoline to the main thread if necessary
2446+
if (ASDisplayNodeThreadIsMain() == NO && layoutTransition.isSynchronous == NO) {
2447+
2448+
// Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded
2449+
ASPerformBlockOnMainThread(^{
2450+
[layoutTransition startTransition];
2451+
});
2452+
2453+
return;
2454+
}
2455+
2456+
[layoutTransition startTransition];
24372457
}
24382458

24392459
- (void)layout

AsyncDisplayKit/Layout/ASLayoutSpec.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ - (ASLayoutableType)layoutableType
5454
return ASLayoutableTypeLayoutSpec;
5555
}
5656

57+
- (BOOL)canLayoutAsynchronous
58+
{
59+
return YES;
60+
}
61+
5762
#pragma mark - Layout
5863

5964
- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize

AsyncDisplayKit/Layout/ASLayoutable.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ NS_ASSUME_NONNULL_BEGIN
4646
*/
4747
@protocol ASLayoutable <ASEnvironment, ASStackLayoutable, ASStaticLayoutable, ASLayoutablePrivate, ASLayoutableExtensibility>
4848

49+
/**
50+
* @abstract Returns type of layoutable
51+
*/
4952
@property (nonatomic, readonly) ASLayoutableType layoutableType;
5053

54+
/**
55+
* @abstract Returns if the layoutable can be used to layout in an asynchronous way on a background thread.
56+
*/
57+
@property (nonatomic, readonly) BOOL canLayoutAsynchronous;
58+
5159
/**
5260
* @abstract Calculate a layout based on given size range.
5361
*

AsyncDisplayKit/Private/ASDisplayNodeInternal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
118118
ASEnvironmentState _environmentState;
119119
ASLayout *_layout;
120120

121+
121122
UIEdgeInsets _hitTestSlop;
122123
NSMutableArray *_subnodes;
123124

AsyncDisplayKit/Private/ASLayoutTransition.h

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,45 @@
1818

1919
@interface ASLayoutTransition : NSObject <_ASTransitionContextLayoutDelegate>
2020

21+
/**
22+
* Node to apply layout transition on
23+
*/
2124
@property (nonatomic, readonly, weak) ASDisplayNode *node;
22-
@property (nonatomic, readonly, strong) ASLayout *pendingLayout;
25+
26+
/**
27+
* Previous layout to transition from
28+
*/
2329
@property (nonatomic, readonly, strong) ASLayout *previousLayout;
2430

25-
- (instancetype)initWithNode:(ASDisplayNode *)node
26-
pendingLayout:(ASLayout *)pendingLayout
27-
previousLayout:(ASLayout *)previousLayout;
31+
/**
32+
* Pending layout to transition to
33+
*/
34+
@property (nonatomic, readonly, strong) ASLayout *pendingLayout;
2835

36+
/**
37+
* Returns if the layout transition can happen asynchronously
38+
*/
39+
@property (nonatomic, readonly, assign) BOOL isSynchronous;
40+
41+
/**
42+
* Returns a newly initialized layout transition
43+
*/
44+
- (instancetype)initWithNode:(ASDisplayNode *)node pendingLayout:(ASLayout *)pendingLayout previousLayout:(ASLayout *)previousLayout NS_DESIGNATED_INITIALIZER;
45+
- (instancetype)init NS_UNAVAILABLE;
46+
47+
/**
48+
* Insert and remove subnodes that where added or removed between the previousLayout and the pendingLayout
49+
*/
50+
- (void)startTransition;
51+
52+
/**
53+
* Insert all new subnodes that where added between the previous layout and the pending layout
54+
*/
2955
- (void)applySubnodeInsertions;
3056

57+
/**
58+
* Remove all subnodes that are removed between the previous layout and the pending layout
59+
*/
3160
- (void)applySubnodeRemovals;
3261

3362
@end

AsyncDisplayKit/Private/ASLayoutTransition.mm

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,37 @@
1818
#import "ASLayout.h"
1919

2020
#import <vector>
21+
#import <queue>
2122

2223
#import "NSArray+Diffing.h"
2324
#import "ASEqualityHelpers.h"
2425

26+
/**
27+
* Search the whole layout stack if at least one layout has a layoutable object that can not be layed out asynchronous.
28+
* This can be the case for example if a node was already loaded
29+
*/
30+
static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) {
31+
// Queue used to keep track of sublayouts while traversing this layout in a BFS fashion.
32+
std::queue<ASLayout *> queue;
33+
queue.push(layout);
34+
35+
while (!queue.empty()) {
36+
layout = queue.front();
37+
queue.pop();
38+
39+
if (layout.layoutableObject.canLayoutAsynchronous == NO) {
40+
return NO;
41+
}
42+
43+
// Add all sublayouts to process in next step
44+
for (int i = 0; i < layout.sublayouts.count; i++) {
45+
queue.push(layout.sublayouts[0]);
46+
}
47+
}
48+
49+
return YES;
50+
}
51+
2552
@implementation ASLayoutTransition {
2653
ASDN::RecursiveMutex _propertyLock;
2754
BOOL _calculatedSubnodeOperations;
@@ -44,6 +71,18 @@ - (instancetype)initWithNode:(ASDisplayNode *)node
4471
return self;
4572
}
4673

74+
- (BOOL)isSynchronous
75+
{
76+
ASDN::MutexLocker l(_propertyLock);
77+
return ASLayoutCanTransitionAsynchronous(_pendingLayout);
78+
}
79+
80+
- (void)startTransition
81+
{
82+
[self applySubnodeInsertions];
83+
[self applySubnodeRemovals];
84+
}
85+
4786
- (void)applySubnodeInsertions
4887
{
4988
ASDN::MutexLocker l(_propertyLock);

AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,88 @@ - (void)testCalculatedLayoutHierarchyTransitions
130130
XCTAssertEqual(node.subnodes[2], node2);
131131
}
132132

133+
- (void)testMeasurementInBackgroundThreadWithLoadedNode
134+
{
135+
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
136+
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
137+
138+
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
139+
node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) {
140+
ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode;
141+
if ([strongNode.layoutState isEqualToNumber:@1]) {
142+
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1]];
143+
} else {
144+
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node2]];
145+
}
146+
};
147+
148+
// Intentionally trigger view creation
149+
[node2 view];
150+
151+
XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout also if one node is already loaded"];
152+
153+
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
154+
155+
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
156+
XCTAssertEqual(node.subnodes[0], node1);
157+
158+
node.layoutState = @2;
159+
[node invalidateCalculatedLayout];
160+
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
161+
162+
// Dispatch back to the main thread to let the insertion / deletion of subnodes happening
163+
dispatch_async(dispatch_get_main_queue(), ^{
164+
XCTAssertEqual(node.subnodes[0], node2);
165+
[expectation fulfill];
166+
});
167+
});
168+
169+
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
170+
if (error) {
171+
NSLog(@"Timeout Error: %@", error);
172+
}
173+
}];
174+
}
175+
176+
- (void)testTransitionLayoutWithAnimationWithLoadedNodes
177+
{
178+
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
179+
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
180+
181+
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
182+
183+
node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) {
184+
ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode;
185+
if ([strongNode.layoutState isEqualToNumber:@1]) {
186+
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1]];
187+
} else {
188+
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node2]];
189+
}
190+
};
191+
192+
// Intentionally trigger view creation
193+
[node2 view];
194+
195+
XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout transition also if one node is already loaded"];
196+
197+
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
198+
XCTAssertEqual(node.subnodes[0], node1);
199+
200+
node.layoutState = @2;
201+
[node invalidateCalculatedLayout];
202+
[node transitionLayoutWithAnimation:YES shouldMeasureAsync:YES measurementCompletion:^{
203+
// Push this to the next runloop to let async insertion / removing of nodes finished before checking
204+
dispatch_async(dispatch_get_main_queue(), ^{
205+
XCTAssertEqual(node.subnodes[0], node2);
206+
[expectation fulfill];
207+
});
208+
}];
209+
210+
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
211+
if (error) {
212+
NSLog(@"Timeout Error: %@", error);
213+
}
214+
}];
215+
}
216+
133217
@end

0 commit comments

Comments
 (0)