diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index f40aa0c382..3ef5d094c0 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -2066,7 +2066,8 @@ NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"AS static ASRunLoopQueue *renderQueue; dispatch_once(&onceToken, ^{ renderQueue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() - andHandler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { + retainObjects:NO + handler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { [dequeuedItem _recursivelyTriggerDisplayAndBlock:NO]; if (isQueueDrained) { CFTimeInterval timestamp = CACurrentMediaTime(); diff --git a/Source/ASDisplayNodeExtras.mm b/Source/ASDisplayNodeExtras.mm index 094179c024..13f8c7b797 100644 --- a/Source/ASDisplayNodeExtras.mm +++ b/Source/ASDisplayNodeExtras.mm @@ -24,7 +24,7 @@ extern void ASPerformMainThreadDeallocation(_Nullable id object) static ASRunLoopQueue *queue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:nil]; + queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil]; queue.batchSize = 10; }); if (object != nil) { diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index 0d1e49bc88..2e50444f3d 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -22,6 +22,7 @@ AS_SUBCLASSING_RESTRICTED * Create a new queue with the given run loop and handler. * * @param runloop The run loop that will drive this queue. + * @param retainsObjects Whether the queue should retain its objects. * @param handlerBlock An optional block to be run for each enqueued object. * * @discussion You may pass @c nil for the handler if you simply want the objects to @@ -30,7 +31,8 @@ AS_SUBCLASSING_RESTRICTED * worker thread with its own run loop. */ - (instancetype)initWithRunLoop:(CFRunLoopRef)runloop - andHandler:(nullable void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; + retainObjects:(BOOL)retainsObjects + handler:(nullable void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; - (void)enqueue:(ObjectType)object; diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 99e0878d6e..1374e79bf9 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -155,7 +155,7 @@ static void runLoopSourceCallback(void *info) { CFRunLoopRef _runLoop; CFRunLoopSourceRef _runLoopSource; CFRunLoopObserverRef _runLoopObserver; - std::deque _internalQueue; + NSPointerArray *_internalQueue; // Use NSPointerArray so we can decide __strong or __weak per-instance. ASDN::RecursiveMutex _internalQueueLock; #if ASRunLoopQueueLoggingEnabled @@ -169,11 +169,12 @@ static void runLoopSourceCallback(void *info) { @implementation ASRunLoopQueue -- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop andHandler:(void(^)(id dequeuedItem, BOOL isQueueDrained))handlerBlock +- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop retainObjects:(BOOL)retainsObjects handler:(void (^)(id _Nullable, BOOL))handlerBlock { if (self = [super init]) { _runLoop = runloop; - _internalQueue = std::deque(); + NSPointerFunctionsOptions options = retainsObjects ? NSPointerFunctionsStrongMemory : NSPointerFunctionsWeakMemory; + _internalQueue = [[NSPointerArray alloc] initWithOptions:options]; _queueConsumer = handlerBlock; _batchSize = 1; _ensureExclusiveMembership = YES; @@ -240,22 +241,41 @@ static void runLoopSourceCallback(void *info) { ASDN::MutexLocker l(_internalQueueLock); // Early-exit if the queue is empty. - if (_internalQueue.empty()) { + NSInteger internalQueueCount = _internalQueue.count; + if (internalQueueCount == 0) { return; } ASProfilingSignpostStart(0, self); // Snatch the next batch of items. - auto firstItemToProcess = _internalQueue.cbegin(); - auto lastItemToProcess = MIN(_internalQueue.cend(), firstItemToProcess + self.batchSize); + NSInteger maxCountToProcess = MIN(internalQueueCount, self.batchSize); - if (hasExecutionBlock) { - itemsToProcess = std::vector(firstItemToProcess, lastItemToProcess); + /** + * For each item in the next batch, if it's non-nil then NULL it out + * and if we have an execution block then add it in. + * This could be written a bunch of different ways but + * this particular one nicely balances readability, safety, and efficiency. + */ + NSInteger foundItemCount = 0; + for (NSInteger i = 0; i < internalQueueCount && foundItemCount < maxCountToProcess; i++) { + /** + * It is safe to use unsafe_unretained here. If the queue is weak, the + * object will be added to the autorelease pool. If the queue is strong, + * it will retain the object until we transfer it (retain it) in itemsToProcess. + */ + __unsafe_unretained id ptr = (__bridge id)[_internalQueue pointerAtIndex:i]; + if (ptr != nil) { + foundItemCount++; + if (hasExecutionBlock) { + itemsToProcess.push_back(ptr); + } + [_internalQueue replacePointerAtIndex:i withPointer:NULL]; + } } - _internalQueue.erase(firstItemToProcess, lastItemToProcess); - if (_internalQueue.empty()) { + [_internalQueue compact]; + if (_internalQueue.count == 0) { isQueueDrained = YES; } } @@ -295,7 +315,7 @@ static void runLoopSourceCallback(void *info) { BOOL foundObject = NO; if (_ensureExclusiveMembership) { - for (id currentObject : _internalQueue) { + for (id currentObject in _internalQueue) { if (currentObject == object) { foundObject = YES; break; @@ -304,7 +324,7 @@ static void runLoopSourceCallback(void *info) { } if (!foundObject) { - _internalQueue.push_back(object); + [_internalQueue addPointer:(__bridge void *)object]; CFRunLoopSourceSignal(_runLoopSource); CFRunLoopWakeUp(_runLoop);