Merge pull request #1839 from maicki/MSAsyncMeasure

[ASDisplayNode] Allow measure always be off the main thread
This commit is contained in:
Adlai Holler
2016-07-08 15:41:05 -07:00
committed by GitHub
7 changed files with 209 additions and 23 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.
*

View File

@@ -118,6 +118,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
ASEnvironmentState _environmentState;
ASLayout *_layout;
UIEdgeInsets _hitTestSlop;
NSMutableArray *_subnodes;

View File

@@ -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

View File

@@ -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);

View File

@@ -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