diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 552133d2ab..89a0ed740c 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -145,11 +145,13 @@ 292C59A21A956527007E5DD6 /* ASRangeHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 292C599C1A956527007E5DD6 /* ASRangeHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 292C59A31A956527007E5DD6 /* ASRangeHandlerRender.h in Headers */ = {isa = PBXBuildFile; fileRef = 292C599D1A956527007E5DD6 /* ASRangeHandlerRender.h */; settings = {ATTRIBUTES = (Public, ); }; }; 292C59A41A956527007E5DD6 /* ASRangeHandlerRender.mm in Sources */ = {isa = PBXBuildFile; fileRef = 292C599E1A956527007E5DD6 /* ASRangeHandlerRender.mm */; }; + 2967F9E21AB0A5190072E4AB /* ASBasicImageDownloaderInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; 296A0A2E1A9516B2005ACEAA /* ASBatchFetching.h in Headers */ = {isa = PBXBuildFile; fileRef = 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */; settings = {ATTRIBUTES = (Private, ); }; }; 296A0A2F1A9516B2005ACEAA /* ASBatchFetching.m in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */; }; 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */; }; 299DA1A91A828D2900162D41 /* ASBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 299DA1A71A828D2900162D41 /* ASBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 299DA1AA1A828D2900162D41 /* ASBatchContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.mm */; }; + 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 464052201A3F83C40061C0BA /* ASDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 464052191A3F83C40061C0BA /* ASDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 464052211A3F83C40061C0BA /* ASDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521A1A3F83C40061C0BA /* ASDataController.mm */; }; @@ -302,12 +304,14 @@ 292C599C1A956527007E5DD6 /* ASRangeHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandler.h; sourceTree = ""; }; 292C599D1A956527007E5DD6 /* ASRangeHandlerRender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandlerRender.h; sourceTree = ""; }; 292C599E1A956527007E5DD6 /* ASRangeHandlerRender.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRangeHandlerRender.mm; sourceTree = ""; }; + 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASBasicImageDownloaderInternal.h; sourceTree = ""; }; 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASBatchFetching.h; path = ../Details/ASBatchFetching.h; sourceTree = ""; }; 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASBatchFetching.m; path = ../Details/ASBatchFetching.m; sourceTree = ""; }; 296A0A311A951715005ACEAA /* ASScrollDirection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASScrollDirection.h; path = AsyncDisplayKit/Details/ASScrollDirection.h; sourceTree = SOURCE_ROOT; }; 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBatchFetchingTests.m; sourceTree = ""; }; 299DA1A71A828D2900162D41 /* ASBatchContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchContext.h; sourceTree = ""; }; 299DA1A81A828D2900162D41 /* ASBatchContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBatchContext.mm; sourceTree = ""; }; + 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderContextTests.m; sourceTree = ""; }; 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewTests.m; sourceTree = ""; }; 464052191A3F83C40061C0BA /* ASDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDataController.h; sourceTree = ""; }; 4640521A1A3F83C40061C0BA /* ASDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDataController.mm; sourceTree = ""; }; @@ -446,6 +450,7 @@ 058D0A2F195D057000B7D73C /* ASDisplayNodeTests.m */, 058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */, 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.m */, + 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */, 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */, @@ -495,11 +500,11 @@ 4640521F1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm */, 058D09E8195D050800B7D73C /* ASMutableAttributedStringBuilder.h */, 058D09E9195D050800B7D73C /* ASMutableAttributedStringBuilder.m */, - 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */, - 292C599B1A956527007E5DD6 /* ASRangeHandlerPreload.mm */, 055F1A3619ABD413004DAFF1 /* ASRangeController.h */, 055F1A3719ABD413004DAFF1 /* ASRangeController.mm */, 292C599C1A956527007E5DD6 /* ASRangeHandler.h */, + 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */, + 292C599B1A956527007E5DD6 /* ASRangeHandlerPreload.mm */, 292C599D1A956527007E5DD6 /* ASRangeHandlerRender.h */, 292C599E1A956527007E5DD6 /* ASRangeHandlerRender.mm */, 296A0A311A951715005ACEAA /* ASScrollDirection.h */, @@ -542,6 +547,7 @@ isa = PBXGroup; children = ( 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */, + 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */, 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */, 058D0A02195D050800B7D73C /* _AS-objc-internal.h */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, @@ -673,6 +679,7 @@ 058D0A78195D05F900B7D73C /* ASDisplayNode+DebugTiming.h in Headers */, 058D0A79195D05F900B7D73C /* ASDisplayNode+DebugTiming.mm in Headers */, 058D0A7A195D05F900B7D73C /* ASDisplayNode+UIViewBridge.mm in Headers */, + 2967F9E21AB0A5190072E4AB /* ASBasicImageDownloaderInternal.h in Headers */, 058D0A7B195D05F900B7D73C /* ASDisplayNodeInternal.h in Headers */, 058D0A7C195D05F900B7D73C /* ASImageNode+CGExtras.h in Headers */, 058D0A7D195D05F900B7D73C /* ASImageNode+CGExtras.m in Headers */, @@ -857,6 +864,7 @@ 058D0A3D195D057000B7D73C /* ASTextNodeCoreTextAdditionsTests.m in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m in Sources */, 058D0A3F195D057000B7D73C /* ASTextNodeShadowerTests.m in Sources */, + 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m in Sources */, 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */, 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */, diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 270586a988..c21d5ea402 100644 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -241,17 +241,11 @@ } }; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [_cache fetchCachedImageWithURL:_URL - callbackQueue:dispatch_get_main_queue() - completion:cacheCompletion]; - }); + [_cache fetchCachedImageWithURL:_URL + callbackQueue:dispatch_get_main_queue() + completion:cacheCompletion]; } else { - // NSURLSessionDownloadTask will do file I/O to create a temp directory. If called on the main thread this - // will cause significant performance issues. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self _downloadImageWithCompletion:finished]; - }); + [self _downloadImageWithCompletion:finished]; } } } diff --git a/AsyncDisplayKit/Details/ASBasicImageDownloader.mm b/AsyncDisplayKit/Details/ASBasicImageDownloader.mm index 5b26ee5271..1d082bcecc 100644 --- a/AsyncDisplayKit/Details/ASBasicImageDownloader.mm +++ b/AsyncDisplayKit/Details/ASBasicImageDownloader.mm @@ -12,6 +12,7 @@ #import +#import "ASBasicImageDownloaderInternal.h" #import "ASThread.h" @@ -19,13 +20,73 @@ /** * Collection of properties associated with a download request. */ -@interface ASBasicImageDownloaderMetadata : NSObject +@interface ASBasicImageDownloaderContext () +{ + BOOL _invalid; + ASDN::RecursiveMutex _propertyLock; +} + @property (nonatomic, strong) dispatch_queue_t callbackQueue; @property (nonatomic, copy) void (^downloadProgressBlock)(CGFloat); @property (nonatomic, copy) void (^completionBlock)(CGImageRef, NSError *); + @end -@implementation ASBasicImageDownloaderMetadata +@implementation ASBasicImageDownloaderContext + +static NSMutableDictionary *currentRequests = nil; +static ASDN::RecursiveMutex currentRequestsLock; + ++ (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL +{ + ASDN::MutexLocker l(currentRequestsLock); + if (!currentRequests) { + currentRequests = [[NSMutableDictionary alloc] init]; + } + ASBasicImageDownloaderContext *context = currentRequests[URL]; + if (!context) { + context = [[ASBasicImageDownloaderContext alloc] initWithURL:URL]; + currentRequests[URL] = context; + } + return context; +} + ++ (void)cancelContextWithURL:(NSURL *)URL +{ + ASDN::MutexLocker l(currentRequestsLock); + if (currentRequests) { + [currentRequests removeObjectForKey:URL]; + } +} + +- (instancetype)initWithURL:(NSURL *)URL +{ + if (self = [super init]) { + _URL = URL; + } + return self; +} + +- (void)cancel +{ + ASDN::MutexLocker l(_propertyLock); + + NSURLSessionTask *sessionTask = self.sessionTask; + if (sessionTask) { + [sessionTask cancel]; + self.sessionTask = nil; + } + + _invalid = YES; + [self.class cancelContextWithURL:self.URL]; +} + +- (BOOL)isCancelled +{ + ASDN::MutexLocker l(_propertyLock); + return _invalid; +} + @end @@ -34,18 +95,18 @@ * NSURLSessionDownloadTask lacks a `userInfo` property, so add this association ourselves. */ @interface NSURLRequest (ASBasicImageDownloader) -@property (nonatomic, strong) ASBasicImageDownloaderMetadata *asyncdisplaykit_metadata; +@property (nonatomic, strong) ASBasicImageDownloaderContext *asyncdisplaykit_context; @end @implementation NSURLRequest (ASBasicImageDownloader) -static const char *kMetadataKey = NSStringFromClass(ASBasicImageDownloaderMetadata.class).UTF8String; -- (void)setAsyncdisplaykit_metadata:(ASBasicImageDownloaderMetadata *)asyncdisplaykit_metadata +static const char *kContextKey = NSStringFromClass(ASBasicImageDownloaderContext.class).UTF8String; +- (void)setAsyncdisplaykit_context:(ASBasicImageDownloaderContext *)asyncdisplaykit_context { - objc_setAssociatedObject(self, kMetadataKey, asyncdisplaykit_metadata, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + objc_setAssociatedObject(self, kContextKey, asyncdisplaykit_context, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -- (ASBasicImageDownloader *)asyncdisplaykit_metadata +- (ASBasicImageDownloader *)asyncdisplaykit_context { - return objc_getAssociatedObject(self, kMetadataKey); + return objc_getAssociatedObject(self, kContextKey); } @end @@ -84,21 +145,38 @@ static const char *kMetadataKey = NSStringFromClass(ASBasicImageDownloaderMetada downloadProgressBlock:(void (^)(CGFloat))downloadProgressBlock completion:(void (^)(CGImageRef, NSError *))completion { - // create download task - NSURLSessionDownloadTask *task = [_session downloadTaskWithURL:URL]; + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:URL]; - // associate metadata with it - ASBasicImageDownloaderMetadata *metadata = [[ASBasicImageDownloaderMetadata alloc] init]; - metadata.callbackQueue = callbackQueue ?: dispatch_get_main_queue(); - metadata.downloadProgressBlock = downloadProgressBlock; - metadata.completionBlock = completion; - task.originalRequest.asyncdisplaykit_metadata = metadata; + // NSURLSessionDownloadTask will do file I/O to create a temp directory. If called on the main thread this will + // cause significant performance issues. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // the downloader may have been invalidated in the time it takes to async dispatch this block + if ([context isCancelled]) { + return; + } + + // create download task + NSURLSessionDownloadTask *task = [_session downloadTaskWithURL:URL]; - // start downloading - [task resume]; + // since creating the task does disk I/O, we should check if it has been invalidated + if ([context isCancelled]) { + return; + } - // return the task as an opaque cancellation token - return task; + // associate metadata with it + context.callbackQueue = callbackQueue ?: dispatch_get_main_queue(); + context.downloadProgressBlock = downloadProgressBlock; + context.completionBlock = completion; + context.sessionTask = task; + task.originalRequest.asyncdisplaykit_context = context; + + // start downloading + [task resume]; + + context.sessionTask = task; + }); + + return context; } - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier @@ -107,10 +185,10 @@ static const char *kMetadataKey = NSStringFromClass(ASBasicImageDownloaderMetada return; } - ASDisplayNodeAssert([downloadIdentifier isKindOfClass:NSURLSessionDownloadTask.class], @"unexpected downloadIdentifier"); - NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)downloadIdentifier; + ASDisplayNodeAssert([downloadIdentifier isKindOfClass:ASBasicImageDownloaderContext.class], @"unexpected downloadIdentifier"); + ASBasicImageDownloaderContext *context = (ASBasicImageDownloaderContext *)downloadIdentifier; - [task cancel]; + [context cancel]; } @@ -121,9 +199,9 @@ static const char *kMetadataKey = NSStringFromClass(ASBasicImageDownloaderMetada totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { - ASBasicImageDownloaderMetadata *metadata = downloadTask.originalRequest.asyncdisplaykit_metadata; - if (metadata.downloadProgressBlock) { - metadata.downloadProgressBlock((CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite); + ASBasicImageDownloaderContext *context = downloadTask.originalRequest.asyncdisplaykit_context; + if (context.downloadProgressBlock) { + context.downloadProgressBlock((CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite); } } @@ -131,12 +209,16 @@ static const char *kMetadataKey = NSStringFromClass(ASBasicImageDownloaderMetada - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { + ASBasicImageDownloaderContext *context = downloadTask.originalRequest.asyncdisplaykit_context; + if ([context isCancelled]) { + return; + } + UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]]; - ASBasicImageDownloaderMetadata *metadata = downloadTask.originalRequest.asyncdisplaykit_metadata; - if (metadata.completionBlock) { - dispatch_async(metadata.callbackQueue, ^{ - metadata.completionBlock(image.CGImage, nil); + if (context.completionBlock) { + dispatch_async(context.callbackQueue, ^{ + context.completionBlock(image.CGImage, nil); }); } } @@ -145,10 +227,10 @@ static const char *kMetadataKey = NSStringFromClass(ASBasicImageDownloaderMetada - (void)URLSession:(NSURLSession *)session task:(NSURLSessionDownloadTask *)task didCompleteWithError:(NSError *)error { - ASBasicImageDownloaderMetadata *metadata = task.originalRequest.asyncdisplaykit_metadata; - if (metadata && error) { - dispatch_async(metadata.callbackQueue, ^{ - metadata.completionBlock(NULL, error); + ASBasicImageDownloaderContext *context = task.originalRequest.asyncdisplaykit_context; + if (context && error) { + dispatch_async(context.callbackQueue, ^{ + context.completionBlock(NULL, error); }); } } diff --git a/AsyncDisplayKit/Details/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h index e1f843e996..38465a2d75 100644 --- a/AsyncDisplayKit/Details/ASImageProtocols.h +++ b/AsyncDisplayKit/Details/ASImageProtocols.h @@ -16,10 +16,12 @@ /** @abstract Attempts to fetch an image with the given URL from the cache. @param URL The URL of the image to retrieve from the cache. - @param callbackQueue The queue to call `completion` on. If this value is nil, @{ref completion} will be invoked on the main-queue. + @param callbackQueue The queue to call `completion` on. If this value is nil, @{ref completion} will be invoked on the + main-queue. @param completion The block to be called when the cache has either hit or missed. @param imageFromCache The image that was retrieved from the cache, if the image could be retrieved; nil otherwise. - @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image. + @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image. This method should not block + the calling thread as it is likely to be called from the main thread. */ - (void)fetchCachedImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue @@ -33,14 +35,18 @@ /** @abstract Downloads an image with the given URL. @param URL The URL of the image to download. - @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. If this value is nil, both blocks will be invoked on the main-queue. + @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. If this value is nil, both blocks + will be invoked on the main-queue. @param downloadProgressBlock The block to be invoked when the download of `URL` progresses. @param progress The progress of the download, in the range of (0.0, 1.0), inclusive. @param completion The block to be invoked when the download has completed, or has failed. @param image The image that was downloaded, if the image could be successfully downloaded; nil otherwise. @param error An error describing why the download of `URL` failed, if the download failed; nil otherwise. - @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image and an error describing why the download failed. - @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must retain the identifier if you wish to use it later. + @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image and an error describing why the + download failed. This method is likely to be called on the main thread, so any custom implementations should make + sure to background any expensive download operations. + @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must + retain the identifier if you wish to use it later. */ - (id)downloadImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue @@ -49,7 +55,8 @@ /** @abstract Cancels an image download. - @param downloadIdentifier The opaque download identifier object returned from `downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:`. + @param downloadIdentifier The opaque download identifier object returned from + `downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:`. @discussion This method has no effect if `downloadIdentifier` is nil. */ - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier; diff --git a/AsyncDisplayKit/Private/ASBasicImageDownloaderInternal.h b/AsyncDisplayKit/Private/ASBasicImageDownloaderInternal.h new file mode 100644 index 0000000000..b6751a5d09 --- /dev/null +++ b/AsyncDisplayKit/Private/ASBasicImageDownloaderInternal.h @@ -0,0 +1,22 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASThread.h" + + +@interface ASBasicImageDownloaderContext : NSObject + ++ (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL; + +@property (nonatomic, strong, readonly) NSURL *URL; +@property (nonatomic, weak) NSURLSessionTask *sessionTask; + +- (BOOL)isCancelled; +- (void)cancel; + +@end \ No newline at end of file diff --git a/AsyncDisplayKitTests/ASBasicImageDownloaderContextTests.m b/AsyncDisplayKitTests/ASBasicImageDownloaderContextTests.m new file mode 100644 index 0000000000..22681070a2 --- /dev/null +++ b/AsyncDisplayKitTests/ASBasicImageDownloaderContextTests.m @@ -0,0 +1,72 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import + +#import + + +@interface ASBasicImageDownloaderContextTests : XCTestCase + +@end + +@implementation ASBasicImageDownloaderContextTests + +- (NSURL *)randomURL +{ + // random URL for each test, doesn't matter that this is not really a URL + return [NSURL URLWithString:[NSUUID UUID].UUIDString]; +} + +- (void)testContextCreation +{ + NSURL *url = [self randomURL]; + ASBasicImageDownloaderContext *c1 = [ASBasicImageDownloaderContext contextForURL:url]; + ASBasicImageDownloaderContext *c2 = [ASBasicImageDownloaderContext contextForURL:url]; + XCTAssert(c1 == c2, @"Context objects are not the same"); +} + +- (void)testContextInvalidation +{ + NSURL *url = [self randomURL]; + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:url]; + [context cancel]; + XCTAssert([context isCancelled], @"Context should be cancelled"); +} + +- (void)testAsyncContextInvalidation +{ + NSURL *url = [self randomURL]; + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:url]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Context invalidation"]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [expectation fulfill]; + XCTAssert([context isCancelled], @"Context should be cancelled"); + }); + + [context cancel]; + [self waitForExpectationsWithTimeout:0.1 handler:nil]; +} + +- (void)testContextSessionCanceled +{ + NSURL *url = [self randomURL]; + id task = [OCMockObject mockForClass:[NSURLSessionTask class]]; + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:url]; + context.sessionTask = task; + + [[task expect] cancel]; + + [context cancel]; +} + +@end