diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 612ce6479f..011071fe62 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -531,14 +531,12 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent // Destruction of bigger images on the main thread can be expensive // and can take some time, so we dispatch onto a bg queue to // actually dealloc. - __block UIImage *image = self.image; + UIImage *image = self.image; CGSize imageSize = image.size; BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || imageSize.height > kMinReleaseImageOnBackgroundSize.height; if (shouldReleaseImageOnBackgroundThread) { - ASPerformBlockOnDeallocationQueue(^{ - image = nil; - }); + ASPerformBackgroundDeallocation(image); } self.image = nil; } diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 497f70d0a0..73ccbebc03 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -405,14 +405,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; // Destruction of bigger images on the main thread can be expensive // and can take some time, so we dispatch onto a bg queue to // actually dealloc. - __block UIImage *image = self.image; + UIImage *image = self.image; CGSize imageSize = image.size; BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || imageSize.height > kMinReleaseImageOnBackgroundSize.height; if (shouldReleaseImageOnBackgroundThread) { - ASPerformBlockOnDeallocationQueue(^{ - image = nil; - }); + ASPerformBackgroundDeallocation(image); } self.animatedImage = nil; self.image = _defaultImage; diff --git a/AsyncDisplayKit/ASRunLoopQueue.h b/AsyncDisplayKit/ASRunLoopQueue.h index 32382ef957..4eacc232da 100644 --- a/AsyncDisplayKit/ASRunLoopQueue.h +++ b/AsyncDisplayKit/ASRunLoopQueue.h @@ -16,11 +16,21 @@ NS_ASSUME_NONNULL_BEGIN @interface ASRunLoopQueue : NSObject -- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop andHandler:(void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; +- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop + andHandler:(void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; - (void)enqueue:(ObjectType)object; -@property (nonatomic, assign) NSUInteger batchSize; +@property (nonatomic, assign) NSUInteger batchSize; // Default == 1. +@property (nonatomic, assign) BOOL ensureExclusiveMembership; // Default == YES. Set-like behavior. + +@end + +@interface ASDeallocQueue : NSObject + ++ (instancetype)sharedDeallocationQueue; + +- (void)releaseObjectInBackground:(id)object; @end diff --git a/AsyncDisplayKit/ASRunLoopQueue.mm b/AsyncDisplayKit/ASRunLoopQueue.mm index e9be82de0d..6d2bd9d2f6 100644 --- a/AsyncDisplayKit/ASRunLoopQueue.mm +++ b/AsyncDisplayKit/ASRunLoopQueue.mm @@ -12,6 +12,7 @@ #import "ASRunLoopQueue.h" #import "ASThread.h" +#import "ASLog.h" #import #import @@ -25,10 +26,118 @@ static void runLoopSourceCallback(void *info) { #endif } +#pragma mark - ASDeallocQueue + +@implementation ASDeallocQueue { + NSThread *_thread; + NSCondition *_condition; + std::deque _queue; + ASDN::RecursiveMutex _queueLock; +} + ++ (instancetype)sharedDeallocationQueue +{ + static ASDeallocQueue *deallocQueue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + deallocQueue = [[ASDeallocQueue alloc] init]; + }); + return deallocQueue; +} + +- (void)releaseObjectInBackground:(id)object +{ + _queueLock.lock(); + _queue.push_back(object); + _queueLock.unlock(); +} + +- (void)threadMain +{ + @autoreleasepool { + __unsafe_unretained __typeof__(self) weakSelf = self; + // 100ms timer. No resources are wasted in between, as the thread sleeps, and each check is fast. + // This time is fast enough for most use cases without excessive churn. + CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(NULL, -1, 0.1, 0, 0, ^(CFRunLoopTimerRef timer) { +#if ASRunLoopQueueLoggingEnabled + NSLog(@"ASDeallocQueue Processing: %d objects destroyed", weakSelf->_queue.size()); +#endif + weakSelf->_queueLock.lock(); + std::deque currentQueue = weakSelf->_queue; + if (currentQueue.size() == 0) { + weakSelf->_queueLock.unlock(); + return; + } + // Sometimes we release 10,000 objects at a time. Don't hold the lock while releasing. + weakSelf->_queue = std::deque(); + weakSelf->_queueLock.unlock(); + currentQueue.clear(); + }); + + CFRunLoopRef runloop = CFRunLoopGetCurrent(); + CFRunLoopAddTimer(runloop, timer, kCFRunLoopCommonModes); + + [_condition lock]; + [_condition signal]; + // At this moment, the thread is guaranteed to be finished starting. + [_condition unlock]; + + // Keep processing events until the runloop is stopped. + CFRunLoopRun(); + + CFRunLoopTimerInvalidate(timer); + CFRunLoopRemoveTimer(runloop, timer, kCFRunLoopCommonModes); + } +} + +- (instancetype)init +{ + if ((self = [super init])) { + _condition = [[NSCondition alloc] init]; + + _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil]; + _thread.name = @"ASDeallocQueue"; + + // Use condition to ensure NSThread has finished starting. + [_condition lock]; + [_thread start]; + [_condition wait]; + [_condition unlock]; + } + return self; +} + +- (void)stop +{ + if (!_thread) { + return; + } + + [_condition lock]; + [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO]; + [_condition wait]; + [_condition unlock]; + _thread = nil; +} + +- (void)_stop +{ + CFRunLoopStop(CFRunLoopGetCurrent()); +} + +- (void)dealloc +{ + [self stop]; +} + +@end + +#pragma mark - ASRunLoopQueue + @interface ASRunLoopQueue () { CFRunLoopRef _runLoop; - CFRunLoopObserverRef _runLoopObserver; CFRunLoopSourceRef _runLoopSource; + CFRunLoopObserverRef _runLoopObserver; std::deque _internalQueue; ASDN::RecursiveMutex _internalQueueLock; @@ -50,8 +159,13 @@ static void runLoopSourceCallback(void *info) { _internalQueue = std::deque(); _queueConsumer = [handlerBlock copy]; _batchSize = 1; + _ensureExclusiveMembership = YES; + + // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, + // __unsafe_unretained allows us to avoid flagging the memory cycle detector. + __unsafe_unretained __typeof__(self) weakSelf = self; void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - [self processQueue]; + [weakSelf processQueue]; }; _runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock); CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes); @@ -101,7 +215,7 @@ static void runLoopSourceCallback(void *info) { - (void)processQueue { - std::deque itemsToProcess = std::deque(); + std::deque itemsToProcess = std::deque(); BOOL isQueueDrained = NO; { ASDN::MutexLocker l(_internalQueueLock); @@ -129,9 +243,9 @@ static void runLoopSourceCallback(void *info) { unsigned long numberOfItems = itemsToProcess.size(); for (int i = 0; i < numberOfItems; i++) { if (isQueueDrained && i == numberOfItems - 1) { - self.queueConsumer(itemsToProcess[i], YES); + _queueConsumer(itemsToProcess[i], YES); } else { - self.queueConsumer(itemsToProcess[i], isQueueDrained); + _queueConsumer(itemsToProcess[i], isQueueDrained); } } @@ -139,7 +253,7 @@ static void runLoopSourceCallback(void *info) { if (!isQueueDrained) { CFRunLoopSourceSignal(_runLoopSource); CFRunLoopWakeUp(_runLoop); - } + } ASProfilingSignpostEnd(0, self); } @@ -154,10 +268,13 @@ static void runLoopSourceCallback(void *info) { // Check if the object exists. BOOL foundObject = NO; - for (id currentObject : _internalQueue) { - if (currentObject == object) { - foundObject = YES; - break; + + if (_ensureExclusiveMembership) { + for (id currentObject : _internalQueue) { + if (currentObject == object) { + foundObject = YES; + break; + } } } diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index e92ebf8c55..3de201d87e 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -307,12 +307,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (_renderer) { // Destruction of the layout managers/containers/text storage is quite // expensive, and can take some time, so we dispatch onto a bg queue to - // actually dealloc. - __block ASTextKitRenderer *renderer = _renderer; - - ASPerformBlockOnDeallocationQueue(^{ - renderer = nil; - }); + // actually dealloc. + ASPerformBackgroundDeallocation(_renderer); _renderer = nil; } } diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index 54e7a1b1d4..f145abac7c 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -26,8 +26,8 @@ void ASPerformBlockOnMainThread(void (^block)()); /// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT -/// Dispatches a block on to a serial queue that's main purpose is for deallocation of objects on a background thread -void ASPerformBlockOnDeallocationQueue(void (^block)()); +/// For deallocation of objects on a background thread without GCD overhead / thread explosion +void ASPerformBackgroundDeallocation(id object); CGFloat ASScreenScale(); diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.m b/AsyncDisplayKit/Private/ASInternalHelpers.m index 2a4ddba7c8..c9dae927ef 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.m +++ b/AsyncDisplayKit/Private/ASInternalHelpers.m @@ -9,6 +9,7 @@ // #import "ASInternalHelpers.h" +#import "ASRunLoopQueue.h" #import @@ -78,15 +79,9 @@ void ASPerformBlockOnBackgroundThread(void (^block)()) } } -void ASPerformBlockOnDeallocationQueue(void (^block)()) +void ASPerformBackgroundDeallocation(id object) { - static dispatch_queue_t queue; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - queue = dispatch_queue_create("org.AsyncDisplayKit.deallocationQueue", DISPATCH_QUEUE_SERIAL); - }); - - dispatch_async(queue, block); + [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object]; } CGFloat ASScreenScale() diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index 9faec1786f..0beb67902c 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -17,6 +17,7 @@ #import "ASTextKitTailTruncater.h" #import "ASTextKitFontSizeAdjuster.h" #import "ASInternalHelpers.h" +#import "ASRunLoopQueue.h" //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) @@ -127,17 +128,13 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() // truncater do it's job again for the new constrained size. This is necessary as after a truncation did happen // the context would use the truncated string and not the original string to truncate based on the new // constrained size - __block ASTextKitContext *ctx = _context; - __block ASTextKitTailTruncater *tru = _truncater; - __block ASTextKitFontSizeAdjuster *adj = _fontSizeAdjuster; + + ASPerformBackgroundDeallocation(_context); + ASPerformBackgroundDeallocation(_truncater); + ASPerformBackgroundDeallocation(_fontSizeAdjuster); _context = nil; _truncater = nil; _fontSizeAdjuster = nil; - ASPerformBlockOnDeallocationQueue(^{ - ctx = nil; - tru = nil; - adj = nil; - }); } }