mirror of
https://github.com/HackPlan/AsyncDisplayKit.git
synced 2026-03-27 22:46:35 +08:00
Merge pull request #1839 from maicki/MSAsyncMeasure
[ASDisplayNode] Allow measure always be off the main thread
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
#import <objc/runtime.h>
|
||||
#import <deque>
|
||||
#import <queue>
|
||||
|
||||
#import "_ASAsyncTransaction.h"
|
||||
#import "_ASAsyncTransactionContainer+Private.h"
|
||||
@@ -631,7 +632,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
if (! [self shouldMeasureWithSizeRange:constrainedSize]) {
|
||||
return _layout;
|
||||
}
|
||||
|
||||
|
||||
[self cancelLayoutTransitionsInProgress];
|
||||
|
||||
ASLayout *previousLayout = _layout;
|
||||
@@ -642,13 +643,14 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
pendingLayout:newLayout
|
||||
previousLayout:previousLayout];
|
||||
} else {
|
||||
ASLayoutTransition *layoutContext;
|
||||
ASLayoutTransition *layoutTransition = nil;
|
||||
if (self.usesImplicitHierarchyManagement) {
|
||||
layoutContext = [[ASLayoutTransition alloc] initWithNode:self
|
||||
pendingLayout:newLayout
|
||||
previousLayout:previousLayout];
|
||||
layoutTransition = [[ASLayoutTransition alloc] initWithNode:self
|
||||
pendingLayout:newLayout
|
||||
previousLayout:previousLayout];
|
||||
}
|
||||
[self applyLayout:newLayout layoutContext:layoutContext];
|
||||
|
||||
[self _applyLayout:newLayout layoutTransition:layoutTransition];
|
||||
[self _completeLayoutCalculation];
|
||||
}
|
||||
|
||||
@@ -685,6 +687,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
return ASLayoutableTypeDisplayNode;
|
||||
}
|
||||
|
||||
- (BOOL)canLayoutAsynchronous
|
||||
{
|
||||
return !self.isNodeLoaded;
|
||||
}
|
||||
|
||||
#pragma mark - Layout Transition
|
||||
|
||||
- (void)transitionLayoutWithAnimation:(BOOL)animated
|
||||
@@ -716,7 +723,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
int32_t transitionID = [self _startNewTransition];
|
||||
|
||||
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
|
||||
ASDisplayNodeAssert([node _hasTransitionInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one.");
|
||||
ASDisplayNodeAssert([node _isTransitionInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one.");
|
||||
node.hierarchyState |= ASHierarchyStateLayoutPending;
|
||||
node.pendingTransitionID = transitionID;
|
||||
});
|
||||
@@ -755,10 +762,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
}
|
||||
|
||||
ASLayout *previousLayout = _layout;
|
||||
[self applyLayout:newLayout layoutContext:nil];
|
||||
[self _applyLayout:newLayout layoutTransition:nil];
|
||||
|
||||
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
|
||||
[node applyPendingLayoutContext];
|
||||
[node _applyPendingLayoutContext];
|
||||
[node _completeLayoutCalculation];
|
||||
node.hierarchyState &= (~ASHierarchyStateLayoutPending);
|
||||
});
|
||||
@@ -781,6 +788,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
});
|
||||
};
|
||||
|
||||
// TODO ihm: Can we always push the measure to the background thread and remove the parameter from the API?
|
||||
if (shouldMeasureAsync) {
|
||||
ASPerformBlockOnBackgroundThread(transitionBlock);
|
||||
} else {
|
||||
@@ -818,7 +826,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
- (void)cancelLayoutTransitionsInProgress
|
||||
{
|
||||
ASDN::MutexLocker l(_propertyLock);
|
||||
if ([self _hasTransitionInProgress]) {
|
||||
if ([self _isTransitionInProgress]) {
|
||||
// Cancel transition in progress
|
||||
[self _finishOrCancelTransition];
|
||||
|
||||
@@ -841,10 +849,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
|
||||
_usesImplicitHierarchyManagement = value;
|
||||
}
|
||||
|
||||
- (BOOL)_hasTransitionInProgress
|
||||
- (BOOL)_isTransitionInProgress
|
||||
{
|
||||
ASDN::MutexLocker l(_propertyLock);
|
||||
return _transitionInProgress;
|
||||
ASDN::MutexLocker l(_propertyLock);
|
||||
return _transitionInProgress;
|
||||
}
|
||||
|
||||
/// Starts a new transition and returns the transition id
|
||||
@@ -2412,16 +2420,16 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
|
||||
});
|
||||
}
|
||||
|
||||
- (void)applyPendingLayoutContext
|
||||
- (void)_applyPendingLayoutContext
|
||||
{
|
||||
ASDN::MutexLocker l(_propertyLock);
|
||||
if (_pendingLayoutTransition) {
|
||||
[self applyLayout:_pendingLayoutTransition.pendingLayout layoutContext:_pendingLayoutTransition];
|
||||
[self _applyLayout:_pendingLayoutTransition.pendingLayout layoutTransition:_pendingLayoutTransition];
|
||||
_pendingLayoutTransition = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applyLayout:(ASLayout *)layout layoutContext:(ASLayoutTransition *)layoutContext
|
||||
- (void)_applyLayout:(ASLayout *)layout layoutTransition:(ASLayoutTransition *)layoutTransition
|
||||
{
|
||||
ASDN::MutexLocker l(_propertyLock);
|
||||
_layout = layout;
|
||||
@@ -2430,10 +2438,22 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
|
||||
ASDisplayNodeAssertTrue(layout.size.width >= 0.0);
|
||||
ASDisplayNodeAssertTrue(layout.size.height >= 0.0);
|
||||
|
||||
if (self.usesImplicitHierarchyManagement && layoutContext != nil) {
|
||||
[layoutContext applySubnodeInsertions];
|
||||
[layoutContext applySubnodeRemovals];
|
||||
if (layoutTransition == nil || self.usesImplicitHierarchyManagement == NO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trampoline to the main thread if necessary
|
||||
if (ASDisplayNodeThreadIsMain() == NO && layoutTransition.isSynchronous == NO) {
|
||||
|
||||
// Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded
|
||||
ASPerformBlockOnMainThread(^{
|
||||
[layoutTransition startTransition];
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[layoutTransition startTransition];
|
||||
}
|
||||
|
||||
- (void)layout
|
||||
|
||||
@@ -54,6 +54,11 @@ typedef std::map<unsigned long, id<ASLayoutable>, std::less<unsigned long>> ASCh
|
||||
return ASLayoutableTypeLayoutSpec;
|
||||
}
|
||||
|
||||
- (BOOL)canLayoutAsynchronous
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Layout
|
||||
|
||||
- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
|
||||
|
||||
@@ -46,8 +46,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@protocol ASLayoutable <ASEnvironment, ASStackLayoutable, ASStaticLayoutable, ASLayoutablePrivate, ASLayoutableExtensibility>
|
||||
|
||||
/**
|
||||
* @abstract Returns type of layoutable
|
||||
*/
|
||||
@property (nonatomic, readonly) ASLayoutableType layoutableType;
|
||||
|
||||
/**
|
||||
* @abstract Returns if the layoutable can be used to layout in an asynchronous way on a background thread.
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL canLayoutAsynchronous;
|
||||
|
||||
/**
|
||||
* @abstract Calculate a layout based on given size range.
|
||||
*
|
||||
|
||||
@@ -118,6 +118,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
|
||||
ASEnvironmentState _environmentState;
|
||||
ASLayout *_layout;
|
||||
|
||||
|
||||
UIEdgeInsets _hitTestSlop;
|
||||
NSMutableArray *_subnodes;
|
||||
|
||||
|
||||
@@ -18,16 +18,45 @@
|
||||
|
||||
@interface ASLayoutTransition : NSObject <_ASTransitionContextLayoutDelegate>
|
||||
|
||||
/**
|
||||
* Node to apply layout transition on
|
||||
*/
|
||||
@property (nonatomic, readonly, weak) ASDisplayNode *node;
|
||||
@property (nonatomic, readonly, strong) ASLayout *pendingLayout;
|
||||
|
||||
/**
|
||||
* Previous layout to transition from
|
||||
*/
|
||||
@property (nonatomic, readonly, strong) ASLayout *previousLayout;
|
||||
|
||||
- (instancetype)initWithNode:(ASDisplayNode *)node
|
||||
pendingLayout:(ASLayout *)pendingLayout
|
||||
previousLayout:(ASLayout *)previousLayout;
|
||||
/**
|
||||
* Pending layout to transition to
|
||||
*/
|
||||
@property (nonatomic, readonly, strong) ASLayout *pendingLayout;
|
||||
|
||||
/**
|
||||
* Returns if the layout transition can happen asynchronously
|
||||
*/
|
||||
@property (nonatomic, readonly, assign) BOOL isSynchronous;
|
||||
|
||||
/**
|
||||
* Returns a newly initialized layout transition
|
||||
*/
|
||||
- (instancetype)initWithNode:(ASDisplayNode *)node pendingLayout:(ASLayout *)pendingLayout previousLayout:(ASLayout *)previousLayout NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* Insert and remove subnodes that where added or removed between the previousLayout and the pendingLayout
|
||||
*/
|
||||
- (void)startTransition;
|
||||
|
||||
/**
|
||||
* Insert all new subnodes that where added between the previous layout and the pending layout
|
||||
*/
|
||||
- (void)applySubnodeInsertions;
|
||||
|
||||
/**
|
||||
* Remove all subnodes that are removed between the previous layout and the pending layout
|
||||
*/
|
||||
- (void)applySubnodeRemovals;
|
||||
|
||||
@end
|
||||
|
||||
@@ -18,10 +18,37 @@
|
||||
#import "ASLayout.h"
|
||||
|
||||
#import <vector>
|
||||
#import <queue>
|
||||
|
||||
#import "NSArray+Diffing.h"
|
||||
#import "ASEqualityHelpers.h"
|
||||
|
||||
/**
|
||||
* Search the whole layout stack if at least one layout has a layoutable object that can not be layed out asynchronous.
|
||||
* This can be the case for example if a node was already loaded
|
||||
*/
|
||||
static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) {
|
||||
// Queue used to keep track of sublayouts while traversing this layout in a BFS fashion.
|
||||
std::queue<ASLayout *> queue;
|
||||
queue.push(layout);
|
||||
|
||||
while (!queue.empty()) {
|
||||
layout = queue.front();
|
||||
queue.pop();
|
||||
|
||||
if (layout.layoutableObject.canLayoutAsynchronous == NO) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Add all sublayouts to process in next step
|
||||
for (int i = 0; i < layout.sublayouts.count; i++) {
|
||||
queue.push(layout.sublayouts[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@implementation ASLayoutTransition {
|
||||
ASDN::RecursiveMutex _propertyLock;
|
||||
BOOL _calculatedSubnodeOperations;
|
||||
@@ -44,6 +71,18 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isSynchronous
|
||||
{
|
||||
ASDN::MutexLocker l(_propertyLock);
|
||||
return ASLayoutCanTransitionAsynchronous(_pendingLayout);
|
||||
}
|
||||
|
||||
- (void)startTransition
|
||||
{
|
||||
[self applySubnodeInsertions];
|
||||
[self applySubnodeRemovals];
|
||||
}
|
||||
|
||||
- (void)applySubnodeInsertions
|
||||
{
|
||||
ASDN::MutexLocker l(_propertyLock);
|
||||
|
||||
@@ -130,4 +130,88 @@
|
||||
XCTAssertEqual(node.subnodes[2], node2);
|
||||
}
|
||||
|
||||
- (void)testMeasurementInBackgroundThreadWithLoadedNode
|
||||
{
|
||||
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
|
||||
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
|
||||
|
||||
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
|
||||
node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) {
|
||||
ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode;
|
||||
if ([strongNode.layoutState isEqualToNumber:@1]) {
|
||||
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1]];
|
||||
} else {
|
||||
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node2]];
|
||||
}
|
||||
};
|
||||
|
||||
// Intentionally trigger view creation
|
||||
[node2 view];
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout also if one node is already loaded"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
|
||||
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
|
||||
XCTAssertEqual(node.subnodes[0], node1);
|
||||
|
||||
node.layoutState = @2;
|
||||
[node invalidateCalculatedLayout];
|
||||
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
|
||||
|
||||
// Dispatch back to the main thread to let the insertion / deletion of subnodes happening
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
XCTAssertEqual(node.subnodes[0], node2);
|
||||
[expectation fulfill];
|
||||
});
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
|
||||
if (error) {
|
||||
NSLog(@"Timeout Error: %@", error);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testTransitionLayoutWithAnimationWithLoadedNodes
|
||||
{
|
||||
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
|
||||
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
|
||||
|
||||
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
|
||||
|
||||
node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) {
|
||||
ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode;
|
||||
if ([strongNode.layoutState isEqualToNumber:@1]) {
|
||||
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1]];
|
||||
} else {
|
||||
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node2]];
|
||||
}
|
||||
};
|
||||
|
||||
// Intentionally trigger view creation
|
||||
[node2 view];
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout transition also if one node is already loaded"];
|
||||
|
||||
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
|
||||
XCTAssertEqual(node.subnodes[0], node1);
|
||||
|
||||
node.layoutState = @2;
|
||||
[node invalidateCalculatedLayout];
|
||||
[node transitionLayoutWithAnimation:YES shouldMeasureAsync:YES measurementCompletion:^{
|
||||
// Push this to the next runloop to let async insertion / removing of nodes finished before checking
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
XCTAssertEqual(node.subnodes[0], node2);
|
||||
[expectation fulfill];
|
||||
});
|
||||
}];
|
||||
|
||||
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
|
||||
if (error) {
|
||||
NSLog(@"Timeout Error: %@", error);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user