// // YYWebImageOperation.m // YYKit // // Created by ibireme on 15/2/15. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "YYWebImageOperation.h" #import "UIApplication+YYAdd.h" #import "YYImage.h" #import "YYWeakProxy.h" #import "UIImage+YYAdd.h" #import #import "YYKitMacro.h" #if __has_include("YYDispatchQueuePool.h") #import "YYDispatchQueuePool.h" #else #import #endif #define MIN_PROGRESSIVE_TIME_INTERVAL 0.2 #define MIN_PROGRESSIVE_BLUR_TIME_INTERVAL 0.4 /// Returns YES if the right-bottom pixel is filled. static BOOL YYCGImageLastPixelFilled(CGImageRef image) { if (!image) return NO; size_t width = CGImageGetWidth(image); size_t height = CGImageGetHeight(image); if (width == 0 || height == 0) return NO; CGContextRef ctx = CGBitmapContextCreate(NULL, 1, 1, 8, 0, YYCGColorSpaceGetDeviceRGB(), kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrderDefault); if (!ctx) return NO; CGContextDrawImage(ctx, CGRectMake( -(int)width + 1, 0, width, height), image); uint8_t *bytes = CGBitmapContextGetData(ctx); BOOL isAlpha = bytes && bytes[0] == 0; CFRelease(ctx); return !isAlpha; } /// Returns JPEG SOS (Start Of Scan) Marker static NSData *JPEGSOSMarker() { // "Start Of Scan" Marker static NSData *marker = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ uint8_t bytes[2] = {0xFF, 0xDA}; marker = [NSData dataWithBytes:bytes length:2]; }); return marker; } static NSMutableSet *URLBlacklist; static dispatch_semaphore_t URLBlacklistLock; static void URLBlacklistInit() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ URLBlacklist = [NSMutableSet new]; URLBlacklistLock = dispatch_semaphore_create(1); }); } static BOOL URLBlackListContains(NSURL *url) { if (!url || url == (id)[NSNull null]) return NO; URLBlacklistInit(); dispatch_semaphore_wait(URLBlacklistLock, DISPATCH_TIME_FOREVER); BOOL contains = [URLBlacklist containsObject:url]; dispatch_semaphore_signal(URLBlacklistLock); return contains; } static void URLInBlackListAdd(NSURL *url) { if (!url || url == (id)[NSNull null]) return; URLBlacklistInit(); dispatch_semaphore_wait(URLBlacklistLock, DISPATCH_TIME_FOREVER); [URLBlacklist addObject:url]; dispatch_semaphore_signal(URLBlacklistLock); } @interface YYWebImageOperation() @property (readwrite, getter=isExecuting) BOOL executing; @property (readwrite, getter=isFinished) BOOL finished; @property (readwrite, getter=isCancelled) BOOL cancelled; @property (readwrite, getter=isStarted) BOOL started; @property (nonatomic, strong) NSRecursiveLock *lock; @property (nonatomic, strong) NSURLConnection *connection; @property (nonatomic, strong) NSMutableData *data; @property (nonatomic, assign) NSInteger expectedSize; @property (nonatomic, assign) UIBackgroundTaskIdentifier taskID; @property (nonatomic, assign) NSTimeInterval lastProgressiveDecodeTimestamp; @property (nonatomic, strong) YYImageDecoder *progressiveDecoder; @property (nonatomic, assign) BOOL progressiveIgnored; @property (nonatomic, assign) BOOL progressiveDetected; @property (nonatomic, assign) NSUInteger progressiveScanedLength; @property (nonatomic, assign) NSUInteger progressiveDisplayCount; @property (nonatomic, copy) YYWebImageProgressBlock progress; @property (nonatomic, copy) YYWebImageTransformBlock transform; @property (nonatomic, copy) YYWebImageCompletionBlock completion; @end @implementation YYWebImageOperation @synthesize executing = _executing; @synthesize finished = _finished; @synthesize cancelled = _cancelled; /// Network thread entry point. + (void)_networkThreadMain:(id)object { @autoreleasepool { [[NSThread currentThread] setName:@"com.ibireme.yykit.webimage.request"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } } /// Global image request network thread, used by NSURLConnection delegate. + (NSThread *)_networkThread { static NSThread *thread = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil]; if ([thread respondsToSelector:@selector(setQualityOfService:)]) { thread.qualityOfService = NSQualityOfServiceBackground; } [thread start]; }); return thread; } /// Global image queue, used for image reading and decoding. + (dispatch_queue_t)_imageQueue { #ifdef YYDispatchQueuePool_h return YYDispatchQueueGetForQOS(NSQualityOfServiceUtility); #else #define MAX_QUEUE_COUNT 16 static int queueCount; static dispatch_queue_t queues[MAX_QUEUE_COUNT]; static dispatch_once_t onceToken; static int32_t counter = 0; dispatch_once(&onceToken, ^{ queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount; queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount; if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) { for (NSUInteger i = 0; i < queueCount; i++) { dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0); queues[i] = dispatch_queue_create("com.ibireme.yykit.decode", attr); } } else { for (NSUInteger i = 0; i < queueCount; i++) { queues[i] = dispatch_queue_create("com.ibireme.yykit.decode", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)); } } }); int32_t cur = OSAtomicIncrement32(&counter); if (cur < 0) cur = -cur; return queues[(cur) % queueCount]; #undef MAX_QUEUE_COUNT #endif } - (instancetype)init { @throw [NSException exceptionWithName:@"YYWebImageOperation init error" reason:@"YYWebImageOperation must be initialized with a request. Use the designated initializer to init." userInfo:nil]; return [self initWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@""]] options:0 cache:nil cacheKey:nil progress:nil transform:nil completion:nil]; } - (instancetype)initWithRequest:(NSURLRequest *)request options:(YYWebImageOptions)options cache:(YYImageCache *)cache cacheKey:(NSString *)cacheKey progress:(YYWebImageProgressBlock)progress transform:(YYWebImageTransformBlock)transform completion:(YYWebImageCompletionBlock)completion { self = [super init]; if (!self) return nil; if (!request) return nil; _request = request; _options = options; _cache = cache; _cacheKey = cacheKey ? cacheKey : request.URL.absoluteString; _shouldUseCredentialStorage = YES; _progress = progress; _transform = transform; _completion = completion; _executing = NO; _finished = NO; _cancelled = NO; _taskID = UIBackgroundTaskInvalid; _lock = [NSRecursiveLock new]; return self; } - (void)dealloc { [_lock lock]; if (_taskID != UIBackgroundTaskInvalid) { [[UIApplication sharedExtensionApplication] endBackgroundTask:_taskID]; _taskID = UIBackgroundTaskInvalid; } if ([self isExecuting]) { self.cancelled = YES; self.finished = YES; if (_connection) { [_connection cancel]; if (![_request.URL isFileURL] && (_options & YYWebImageOptionShowNetworkActivity)) { [[UIApplication sharedExtensionApplication] decrementNetworkActivityCount]; } } if (_completion) { @autoreleasepool { _completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageCancelled, nil); } } } [_lock unlock]; } - (void)_endBackgroundTask { [_lock lock]; if (_taskID != UIBackgroundTaskInvalid) { [[UIApplication sharedExtensionApplication] endBackgroundTask:_taskID]; _taskID = UIBackgroundTaskInvalid; } [_lock unlock]; } #pragma mark - Runs in operation thread - (void)_finish { self.executing = NO; self.finished = YES; [self _endBackgroundTask]; } // runs on network thread - (void)_startOperation { if ([self isCancelled]) return; @autoreleasepool { // get image from cache if (_cache && !(_options & YYWebImageOptionUseNSURLCache) && !(_options & YYWebImageOptionRefreshImageCache)) { UIImage *image = [_cache getImageForKey:_cacheKey withType:YYImageCacheTypeMemory]; if (image) { [_lock lock]; if (![self isCancelled]) { if (_completion) _completion(image, _request.URL, YYWebImageFromMemoryCache, YYWebImageStageFinished, nil); } [self _finish]; [_lock unlock]; return; } if (!(_options & YYWebImageOptionIgnoreDiskCache)) { __weak typeof(self) _self = self; dispatch_async([self.class _imageQueue], ^{ __strong typeof(_self) self = _self; if (!self || [self isCancelled]) return; UIImage *image = [self.cache getImageForKey:self.cacheKey withType:YYImageCacheTypeDisk]; if (image) { [self.cache setImage:image imageData:nil forKey:self.cacheKey withType:YYImageCacheTypeMemory]; [self performSelector:@selector(_didReceiveImageFromDiskCache:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO]; } else { [self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO]; } }); return; } } } [self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO]; } // runs on network thread - (void)_startRequest:(id)object { if ([self isCancelled]) return; @autoreleasepool { if ((_options & YYWebImageOptionIgnoreFailedURL) && URLBlackListContains(_request.URL)) { NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{ NSLocalizedDescriptionKey : @"Failed to load URL, blacklisted." }]; [_lock lock]; if (![self isCancelled]) { if (_completion) _completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageFinished, error); } [self _finish]; [_lock unlock]; return; } if (_request.URL.isFileURL) { NSArray *keys = @[NSURLFileSizeKey]; NSDictionary *attr = [_request.URL resourceValuesForKeys:keys error:nil]; NSNumber *fileSize = attr[NSURLFileSizeKey]; _expectedSize = (fileSize != nil) ? fileSize.unsignedIntegerValue : -1; } // request image from web [_lock lock]; if (![self isCancelled]) { _connection = [[NSURLConnection alloc] initWithRequest:_request delegate:[YYWeakProxy proxyWithTarget:self]]; if (![_request.URL isFileURL] && (_options & YYWebImageOptionShowNetworkActivity)) { [[UIApplication sharedExtensionApplication] incrementNetworkActivityCount]; } } [_lock unlock]; } } // runs on network thread, called from outer "cancel" - (void)_cancelOperation { @autoreleasepool { if (_connection) { if (![_request.URL isFileURL] && (_options & YYWebImageOptionShowNetworkActivity)) { [[UIApplication sharedExtensionApplication] decrementNetworkActivityCount]; } } [_connection cancel]; _connection = nil; if (_completion) _completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageCancelled, nil); [self _endBackgroundTask]; } } // runs on network thread - (void)_didReceiveImageFromDiskCache:(UIImage *)image { @autoreleasepool { [_lock lock]; if (![self isCancelled]) { if (image) { if (_completion) _completion(image, _request.URL, YYWebImageFromDiskCache, YYWebImageStageFinished, nil); [self _finish]; } else { [self _startRequest:nil]; } } [_lock unlock]; } } - (void)_didReceiveImageFromWeb:(UIImage *)image { @autoreleasepool { [_lock lock]; if (![self isCancelled]) { if (_cache) { if (image || (_options & YYWebImageOptionRefreshImageCache)) { NSData *data = _data; dispatch_async([YYWebImageOperation _imageQueue], ^{ YYImageCacheType cacheType = (_options & YYWebImageOptionIgnoreDiskCache) ? YYImageCacheTypeMemory : YYImageCacheTypeAll; [_cache setImage:image imageData:data forKey:_cacheKey withType:cacheType]; }); } } _data = nil; NSError *error = nil; if (!image) { error = [NSError errorWithDomain:@"com.ibireme.yykit.image" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Web image decode fail." }]; if (_options & YYWebImageOptionIgnoreFailedURL) { if (URLBlackListContains(_request.URL)) { error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{ NSLocalizedDescriptionKey : @"Failed to load URL, blacklisted." }]; } else { URLInBlackListAdd(_request.URL); } } } if (_completion) _completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageFinished, error); [self _finish]; } [_lock unlock]; } } #pragma mark - NSURLConnectionDelegate runs in operation thread - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection { return _shouldUseCredentialStorage; } - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { @autoreleasepool { if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { if (!(_options & YYWebImageOptionAllowInvalidSSLCertificates) && [challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) { [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge]; } else { NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; } } else { if ([challenge previousFailureCount] == 0) { if (_credential) { [[challenge sender] useCredential:_credential forAuthenticationChallenge:challenge]; } else { [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; } } else { [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; } } } } - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { if (!cachedResponse) return cachedResponse; if (_options & YYWebImageOptionUseNSURLCache) { return cachedResponse; } else { // ignore NSURLCache return nil; } } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { @autoreleasepool { NSError *error = nil; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (id) response; NSInteger statusCode = httpResponse.statusCode; if (statusCode >= 400 || statusCode == 304) { error = [NSError errorWithDomain:NSURLErrorDomain code:statusCode userInfo:nil]; } } if (error) { [_connection cancel]; [self connection:_connection didFailWithError:error]; } else { if (response.expectedContentLength) { _expectedSize = (NSInteger)response.expectedContentLength; if (_expectedSize < 0) _expectedSize = -1; } _data = [NSMutableData dataWithCapacity:_expectedSize > 0 ? _expectedSize : 0]; if (_progress) { [_lock lock]; if (![self isCancelled]) _progress(0, _expectedSize); [_lock unlock]; } } } } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { @autoreleasepool { [_lock lock]; BOOL canceled = [self isCancelled]; [_lock unlock]; if (canceled) return; if (data) [_data appendData:data]; if (_progress) { [_lock lock]; if (![self isCancelled]) { _progress(_data.length, _expectedSize); } [_lock unlock]; } /*--------------------------- progressive ----------------------------*/ BOOL progressive = (_options & YYWebImageOptionProgressive) > 0; BOOL progressiveBlur = (_options & YYWebImageOptionProgressiveBlur) > 0; if (!_completion || !(progressive || progressiveBlur)) return; if (data.length <= 16) return; if (_expectedSize > 0 && data.length >= _expectedSize * 0.99) return; if (_progressiveIgnored) return; NSTimeInterval min = progressiveBlur ? MIN_PROGRESSIVE_BLUR_TIME_INTERVAL : MIN_PROGRESSIVE_TIME_INTERVAL; NSTimeInterval now = CACurrentMediaTime(); if (now - _lastProgressiveDecodeTimestamp < min) return; if (!_progressiveDecoder) { _progressiveDecoder = [[YYImageDecoder alloc] initWithScale:[UIScreen mainScreen].scale]; } [_progressiveDecoder updateData:_data final:NO]; if ([self isCancelled]) return; if (_progressiveDecoder.type == YYImageTypeUnknown || _progressiveDecoder.type == YYImageTypeWebP || _progressiveDecoder.type == YYImageTypeOther) { _progressiveDecoder = nil; _progressiveIgnored = YES; return; } if (progressiveBlur) { // only support progressive JPEG and interlaced PNG if (_progressiveDecoder.type != YYImageTypeJPEG && _progressiveDecoder.type != YYImageTypePNG) { _progressiveDecoder = nil; _progressiveIgnored = YES; return; } } if (_progressiveDecoder.frameCount == 0) return; if (!progressiveBlur) { YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES]; if (frame.image) { [_lock lock]; if (![self isCancelled]) { _completion(frame.image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil); _lastProgressiveDecodeTimestamp = now; } [_lock unlock]; } return; } else { if (_progressiveDecoder.type == YYImageTypeJPEG) { if (!_progressiveDetected) { NSDictionary *dic = [_progressiveDecoder framePropertiesAtIndex:0]; NSDictionary *jpeg = dic[(id)kCGImagePropertyJFIFDictionary]; NSNumber *isProg = jpeg[(id)kCGImagePropertyJFIFIsProgressive]; if (!isProg.boolValue) { _progressiveIgnored = YES; _progressiveDecoder = nil; return; } _progressiveDetected = YES; } NSInteger scanLength = (NSInteger)_data.length - (NSInteger)_progressiveScanedLength - 4; if (scanLength <= 2) return; NSRange scanRange = NSMakeRange(_progressiveScanedLength, scanLength); NSRange markerRange = [_data rangeOfData:JPEGSOSMarker() options:kNilOptions range:scanRange]; _progressiveScanedLength = _data.length; if (markerRange.location == NSNotFound) return; if ([self isCancelled]) return; } else if (_progressiveDecoder.type == YYImageTypePNG) { if (!_progressiveDetected) { NSDictionary *dic = [_progressiveDecoder framePropertiesAtIndex:0]; NSDictionary *png = dic[(id)kCGImagePropertyPNGDictionary]; NSNumber *isProg = png[(id)kCGImagePropertyPNGInterlaceType]; if (!isProg.boolValue) { _progressiveIgnored = YES; _progressiveDecoder = nil; return; } _progressiveDetected = YES; } } YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES]; UIImage *image = frame.image; if (!image) return; if ([self isCancelled]) return; if (!YYCGImageLastPixelFilled(image.CGImage)) return; _progressiveDisplayCount++; CGFloat radius = 32; if (_expectedSize > 0) { radius *= 1.0 / (3 * _data.length / (CGFloat)_expectedSize + 0.6) - 0.25; } else { radius /= (_progressiveDisplayCount); } image = [image imageByBlurRadius:radius tintColor:nil tintMode:0 saturation:1 maskImage:nil]; if (image) { [_lock lock]; if (![self isCancelled]) { _completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil); _lastProgressiveDecodeTimestamp = now; } [_lock unlock]; } } } } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { @autoreleasepool { [_lock lock]; _connection = nil; if (![self isCancelled]) { __weak typeof(self) _self = self; dispatch_async([self.class _imageQueue], ^{ __strong typeof(_self) self = _self; if (!self) return; BOOL shouldDecode = (self.options & YYWebImageOptionIgnoreImageDecoding) == 0; BOOL allowAnimation = (self.options & YYWebImageOptionIgnoreAnimatedImage) == 0; UIImage *image; BOOL hasAnimation = NO; if (allowAnimation) { image = [[YYImage alloc] initWithData:self.data scale:[UIScreen mainScreen].scale]; if (shouldDecode) image = [image imageByDecoded]; if ([((YYImage *)image) animatedImageFrameCount] > 1) { hasAnimation = YES; } } else { YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale]; image = [decoder frameAtIndex:0 decodeForDisplay:shouldDecode].image; } /* If the image has animation, save the original image data to disk cache. If the image is not PNG or JPEG, re-encode the image to PNG or JPEG for better decoding performance. */ YYImageType imageType = YYImageDetectType((__bridge CFDataRef)self.data); switch (imageType) { case YYImageTypeJPEG: case YYImageTypeGIF: case YYImageTypePNG: case YYImageTypeWebP: { // save to disk cache if (!hasAnimation) { if (imageType == YYImageTypeGIF || imageType == YYImageTypeWebP) { self.data = nil; // clear the data, re-encode for disk cache } } } break; default: { self.data = nil; // clear the data, re-encode for disk cache } break; } if ([self isCancelled]) return; if (self.transform && image) { UIImage *newImage = self.transform(image, self.request.URL); if (newImage != image) { self.data = nil; } image = newImage; if ([self isCancelled]) return; } [self performSelector:@selector(_didReceiveImageFromWeb:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO]; }); if (![self.request.URL isFileURL] && (self.options & YYWebImageOptionShowNetworkActivity)) { [[UIApplication sharedExtensionApplication] decrementNetworkActivityCount]; } } [_lock unlock]; } } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { @autoreleasepool { [_lock lock]; if (![self isCancelled]) { if (_completion) { _completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageFinished, error); } _connection = nil; _data = nil; if (![_request.URL isFileURL] && (_options & YYWebImageOptionShowNetworkActivity)) { [[UIApplication sharedExtensionApplication] decrementNetworkActivityCount]; } [self _finish]; if (_options & YYWebImageOptionIgnoreFailedURL) { if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut && error.code != NSURLErrorUserCancelledAuthentication && error.code != NSURLErrorNetworkConnectionLost) { URLInBlackListAdd(_request.URL); } } } [_lock unlock]; } } #pragma mark - Override NSOperation - (void)start { @autoreleasepool { [_lock lock]; self.started = YES; if ([self isCancelled]) { [self performSelector:@selector(_cancelOperation) onThread:[[self class] _networkThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; self.finished = YES; } else if ([self isReady] && ![self isFinished] && ![self isExecuting]) { if (!_request) { self.finished = YES; if (_completion) { NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{NSLocalizedDescriptionKey:@"request in nil"}]; _completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageFinished, error); } } else { self.executing = YES; [self performSelector:@selector(_startOperation) onThread:[[self class] _networkThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; if ((_options & YYWebImageOptionAllowBackgroundTask) && ![UIApplication isAppExtension]) { __weak __typeof__ (self) _self = self; if (_taskID == UIBackgroundTaskInvalid) { _taskID = [[UIApplication sharedExtensionApplication] beginBackgroundTaskWithExpirationHandler:^{ __strong __typeof (_self) self = _self; if (self) { [self cancel]; self.finished = YES; } }]; } } } } [_lock unlock]; } } - (void)cancel { [_lock lock]; if (![self isCancelled]) { [super cancel]; self.cancelled = YES; if ([self isExecuting]) { self.executing = NO; [self performSelector:@selector(_cancelOperation) onThread:[[self class] _networkThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; } if (self.started) { self.finished = YES; } } [_lock unlock]; } - (void)setExecuting:(BOOL)executing { [_lock lock]; if (_executing != executing) { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } [_lock unlock]; } - (BOOL)isExecuting { [_lock lock]; BOOL executing = _executing; [_lock unlock]; return executing; } - (void)setFinished:(BOOL)finished { [_lock lock]; if (_finished != finished) { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } [_lock unlock]; } - (BOOL)isFinished { [_lock lock]; BOOL finished = _finished; [_lock unlock]; return finished; } - (void)setCancelled:(BOOL)cancelled { [_lock lock]; if (_cancelled != cancelled) { [self willChangeValueForKey:@"isCancelled"]; _cancelled = cancelled; [self didChangeValueForKey:@"isCancelled"]; } [_lock unlock]; } - (BOOL)isCancelled { [_lock lock]; BOOL cancelled = _cancelled; [_lock unlock]; return cancelled; } - (BOOL)isConcurrent { return YES; } - (BOOL)isAsynchronous { return YES; } + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"isExecuting"] || [key isEqualToString:@"isFinished"] || [key isEqualToString:@"isCancelled"]) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; } - (NSString *)description { NSMutableString *string = [NSMutableString stringWithFormat:@"<%@: %p ",self.class, self]; [string appendFormat:@" executing:%@", [self isExecuting] ? @"YES" : @"NO"]; [string appendFormat:@" finished:%@", [self isFinished] ? @"YES" : @"NO"]; [string appendFormat:@" cancelled:%@", [self isCancelled] ? @"YES" : @"NO"]; [string appendString:@">"]; return string; } @end