diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 685ed93aef..77f175350e 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -66,6 +66,11 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState) ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay | ASInterfaceStateVisible, }; +/** + * Default drawing priority for display node + */ +extern NSUInteger const ASDefaultDrawingPriority; + /** * An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view * hierarchy off the main thread, and could do rendering off the main thread as well. @@ -475,6 +480,13 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) NSTimeInterval placeholderFadeDuration; +/** + * @abstract Determines drawing priority of the node. Nodes with higher priority will be drawn earlier. + * + * @discussion Defaults to ASDefaultDrawingPriority. There may be multiple drawing threads, and some of them may + * decide to perform operations in queued order (regardless of drawingPriority) + */ +@property (nonatomic, assign) NSUInteger drawingPriority; /** @name Hit Testing */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 79cd80d1dc..5b6b87e5e7 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -26,6 +26,8 @@ #import "ASLayoutSpec.h" #import "ASCellNode.h" +NSUInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; + @interface ASDisplayNode () /** @@ -2236,6 +2238,31 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, } } +static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; + +- (void)setDrawingPriority:(NSUInteger)drawingPriority +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + if (drawingPriority == ASDefaultDrawingPriority) { + _flags.hasCustomDrawingPriority = NO; + objc_setAssociatedObject(self, ASDisplayNodeDrawingPriorityKey, nil, OBJC_ASSOCIATION_ASSIGN); + } else { + _flags.hasCustomDrawingPriority = YES; + objc_setAssociatedObject(self, ASDisplayNodeDrawingPriorityKey, [NSNumber numberWithUnsignedInteger:drawingPriority], OBJC_ASSOCIATION_RETAIN); + } +} + +-(NSUInteger)drawingPriority +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + if (!_flags.hasCustomDrawingPriority) + return ASDefaultDrawingPriority; + else + return [objc_getAssociatedObject(self, ASDisplayNodeDrawingPriorityKey) unsignedIntegerValue]; +} + - (BOOL)isInHierarchy { ASDisplayNodeAssertThreadAffinity(self); diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h index ca792f9fec..973a2da0ec 100644 --- a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h @@ -29,6 +29,8 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) { ASAsyncTransactionStateComplete }; +extern NSUInteger const ASDefaultTransactionPriority; + /** @summary ASAsyncTransaction provides lightweight transaction semantics for asynchronous operations. @@ -94,6 +96,25 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) { queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion; +/** + @summary Adds a synchronous operation to the transaction. The execution block will be executed immediately. + + @desc The block will be executed on the specified queue and is expected to complete synchronously. The async + transaction will wait for all operations to execute on their appropriate queues, so the blocks may still be executing + async if they are running on a concurrent queue, even though the work for this block is synchronous. + + @param block The execution block that will be executed on a background queue. This is where the expensive work goes. + @param priority Execution priority; Tasks with higher priority will be executed sooner + @param queue The dispatch queue on which to execute the block. + @param completion The completion block that will be executed with the output of the execution block when all of the + operations in the transaction are completed. Executed and released on callbackQueue. + */ +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block + priority:(NSUInteger)priority + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion; + + /** @summary Adds an async operation to the transaction. The execution block will be executed immediately. @@ -112,6 +133,27 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) { queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion; +/** + @summary Adds an async operation to the transaction. The execution block will be executed immediately. + + @desc The block will be executed on the specified queue and is expected to complete asynchronously. The block will be + supplied with a completion block that can be executed once its async operation is completed. This is useful for + network downloads and other operations that have an async API. + + WARNING: Consumers MUST call the completeOperationBlock passed into the work block, or objects will be leaked! + + @param block The execution block that will be executed on a background queue. This is where the expensive work goes. + @param priority Execution priority; Tasks with higher priority will be executed sooner + @param queue The dispatch queue on which to execute the block. + @param completion The completion block that will be executed with the output of the execution block when all of the + operations in the transaction are completed. Executed and released on callbackQueue. + */ +- (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block + priority:(NSUInteger)priority + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion; + + /** @summary Adds a block to run on the completion of the async transaction. diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm index 07bb4667f2..c606f3b6fb 100644 --- a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm @@ -13,6 +13,8 @@ #import #import +NSUInteger const ASDefaultTransactionPriority = 128; + @interface ASDisplayNodeAsyncTransactionOperation : NSObject - (id)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock; @property (nonatomic, copy) asyncdisplaykit_async_transaction_operation_completion_block_t operationCompletionBlock; @@ -63,7 +65,7 @@ public: virtual void release() = 0; // schedule block on given queue - virtual void schedule(dispatch_queue_t queue, dispatch_block_t block) = 0; + virtual void schedule(NSUInteger priority, dispatch_queue_t queue, dispatch_block_t block) = 0; // dispatch block on given queue when all previously scheduled blocks finished executing virtual void notify(dispatch_queue_t queue, dispatch_block_t block) = 0; @@ -103,7 +105,7 @@ private: } virtual void release(); - virtual void schedule(dispatch_queue_t queue, dispatch_block_t block); + virtual void schedule(NSUInteger priority, dispatch_queue_t queue, dispatch_block_t block); virtual void notify(dispatch_queue_t queue, dispatch_block_t block); virtual void enter(); virtual void leave(); @@ -120,12 +122,21 @@ private: { dispatch_block_t _block; GroupImpl *_group; + NSUInteger _priority; }; - - struct DispatchEntry // entry for each + + struct DispatchEntry // entry for each dispatch queue { - std::list _operations; + typedef std::list OperationQueue; + typedef std::list OperationIteratorList; // each item points to operation queue + typedef std::map OperationPriorityMap; // sorted by priority + + OperationQueue _operationQueue; + OperationPriorityMap _operationPriorityMap; int _threadCount; + + Operation popNextOperation(bool respectPriority); // assumes locked mutex + void pushOperation(Operation operation); // assumes locked mutex }; std::map _entries; @@ -149,7 +160,44 @@ void ASAsyncTransactionQueue::GroupImpl::release() } } -void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispatch_block_t block) +ASAsyncTransactionQueue::Operation ASAsyncTransactionQueue::DispatchEntry::popNextOperation(bool respectPriority) +{ + NSCAssert(!_operationQueue.empty() && !_operationPriorityMap.empty(), @"No scheduled operations available"); + + OperationQueue::iterator queueIterator; + OperationPriorityMap::iterator mapIterator; + + if (respectPriority) { + mapIterator = --_operationPriorityMap.end(); // highest priority "bucket" + queueIterator = *mapIterator->second.begin(); + } else { + queueIterator = _operationQueue.begin(); + mapIterator = _operationPriorityMap.find(queueIterator->_priority); + } + + // no matter what, first item in "bucket" must match item in queue + NSCAssert(mapIterator->second.front() == queueIterator, @"Queue inconsistency"); + + Operation res = *queueIterator; + _operationQueue.erase(queueIterator); + + mapIterator->second.pop_front(); + if (mapIterator->second.empty()) { + _operationPriorityMap.erase(mapIterator); + } + + return res; +} + +void ASAsyncTransactionQueue::DispatchEntry::pushOperation(ASAsyncTransactionQueue::Operation operation) +{ + _operationQueue.push_back(operation); + + OperationIteratorList &list = _operationPriorityMap[operation._priority]; + list.push_back(--_operationQueue.end()); +} + +void ASAsyncTransactionQueue::GroupImpl::schedule(NSUInteger priority, dispatch_queue_t queue, dispatch_block_t block) { ASAsyncTransactionQueue &q = _queue; ASDN::MutexLocker locker(q._mutex); @@ -159,7 +207,8 @@ void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispat Operation operation; operation._block = block; operation._group = this; - entry._operations.push_back(operation); + operation._priority = priority; + entry.pushOperation(operation); ++_pendingOperations; // enter group @@ -171,15 +220,16 @@ void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispat if (entry._threadCount < maxThreads) { // we need to spawn another thread + // first thread will take operations in queue order (regardless of priority), other threads will respect priority + bool respectPriority = entry._threadCount > 0; ++entry._threadCount; dispatch_async(queue, ^{ ASDN::MutexLocker lock(q._mutex); // go until there are no more pending operations - while (!entry._operations.empty()) { - Operation operation = entry._operations.front(); - entry._operations.pop_front(); + while (!entry._operationQueue.empty()) { + Operation operation = entry.popNextOperation(respectPriority); { ASDN::MutexUnlocker unlock(q._mutex); if (operation._block) { @@ -192,7 +242,7 @@ void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispat --entry._threadCount; if (entry._threadCount == 0) { - NSCAssert(entry._operations.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen + NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen q._entries.erase(queue); } }); @@ -295,6 +345,17 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() - (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion +{ + [self addAsyncOperationWithBlock:block + priority:ASDefaultTransactionPriority + queue:queue + completion:completion]; +} + +- (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block + priority:(NSUInteger)priority + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); @@ -303,7 +364,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; [_operations addObject:operation]; - _group->schedule(queue, ^{ + _group->schedule(priority, queue, ^{ @autoreleasepool { if (_state != ASAsyncTransactionStateCanceled) { _group->enter(); @@ -319,6 +380,17 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() - (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion +{ + [self addOperationWithBlock:block + priority:ASDefaultTransactionPriority + queue:queue + completion:completion]; +} + +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block + priority:(NSUInteger)priority + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); @@ -327,7 +399,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; [_operations addObject:operation]; - _group->schedule(queue, ^{ + _group->schedule(priority, queue, ^{ @autoreleasepool { if (_state != ASAsyncTransactionStateCanceled) { operation.value = block(); diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index 2566d0e281..a642275305 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -358,7 +358,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, // Adding this displayBlock operation to the transaction will start it IMMEDIATELY. // The only function of the transaction commit is to gate the calling of the completionBlock. - [transaction addOperationWithBlock:displayBlock queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; } else { UIImage *contents = (UIImage *)displayBlock(); completionBlock(contents, NO); diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 42627d39c0..f1ffb377e9 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -84,6 +84,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) unsigned shouldRasterizeDescendants:1; unsigned shouldBypassEnsureDisplay:1; unsigned displaySuspended:1; + unsigned hasCustomDrawingPriority:1; // whether custom drawing is enabled unsigned implementsDrawRect:1;