Fixed threading issues in RCTImageDownloader

This commit is contained in:
Nick Lockwood 2015-03-30 03:42:13 -07:00
Родитель 5865cfa956
Коммит 15eb5fde51
4 изменённых файлов: 79 добавлений и 55 удалений

Просмотреть файл

@ -16,14 +16,30 @@ typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
+ (instancetype)sharedInstance; + (instancetype)sharedInstance;
/**
* Downloads a block of raw data and returns it. Note that the callback block
* will not be executed on the same thread you called the method from, nor on
* the main thread. Returns a token that can be used to cancel the download.
*/
- (id)downloadDataForURL:(NSURL *)url - (id)downloadDataForURL:(NSURL *)url
block:(RCTDataDownloadBlock)block; block:(RCTDataDownloadBlock)block;
/**
* Downloads an image and decompresses it a the size specified. The compressed
* image will be cached in memory and to disk. Note that the callback block
* will not be executed on the same thread you called the method from, nor on
* the main thread. Returns a token that can be used to cancel the download.
*/
- (id)downloadImageForURL:(NSURL *)url - (id)downloadImageForURL:(NSURL *)url
size:(CGSize)size size:(CGSize)size
scale:(CGFloat)scale scale:(CGFloat)scale
block:(RCTImageDownloadBlock)block; block:(RCTImageDownloadBlock)block;
/**
* Cancel an in-flight download. If multiple requets have been made for the
* same image, only the request that relates to the token passed will be
* cancelled.
*/
- (void)cancelDownload:(id)downloadToken; - (void)cancelDownload:(id)downloadToken;
@end @end

Просмотреть файл

@ -17,12 +17,12 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e
@implementation RCTImageDownloader @implementation RCTImageDownloader
{ {
RCTCache *_cache; RCTCache *_cache;
dispatch_queue_t _processingQueue;
NSMutableDictionary *_pendingBlocks; NSMutableDictionary *_pendingBlocks;
} }
+ (instancetype)sharedInstance + (instancetype)sharedInstance
{ {
RCTAssertMainThread();
static RCTImageDownloader *sharedInstance; static RCTImageDownloader *sharedInstance;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ dispatch_once(&onceToken, ^{
@ -35,27 +35,32 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e
{ {
if ((self = [super init])) { if ((self = [super init])) {
_cache = [[RCTCache alloc] initWithName:@"RCTImageDownloader"]; _cache = [[RCTCache alloc] initWithName:@"RCTImageDownloader"];
_pendingBlocks = [NSMutableDictionary dictionary]; _processingQueue = dispatch_queue_create("com.facebook.React.DownloadProcessingQueue", DISPATCH_QUEUE_SERIAL);
_pendingBlocks = [[NSMutableDictionary alloc] init];
} }
return self; return self;
} }
- (NSString *)cacheKeyForURL:(NSURL *)url static NSString *RCTCacheKeyForURL(NSURL *url)
{ {
return url.absoluteString; return url.absoluteString;
} }
- (id)_downloadDataForURL:(NSURL *)url block:(RCTCachedDataDownloadBlock)block - (id)_downloadDataForURL:(NSURL *)url block:(RCTCachedDataDownloadBlock)block
{ {
NSString *cacheKey = [self cacheKeyForURL:url]; NSString *cacheKey = RCTCacheKeyForURL(url);
__block BOOL cancelled = NO; __block BOOL cancelled = NO;
__block NSURLSessionDataTask *task = nil; __block NSURLSessionDataTask *task = nil;
dispatch_block_t cancel = ^{ dispatch_block_t cancel = ^{
cancelled = YES; cancelled = YES;
NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey]; dispatch_async(_processingQueue, ^{
[pendingBlocks removeObject:block]; NSMutableArray *pendingBlocks = self->_pendingBlocks[cacheKey];
[pendingBlocks removeObject:block];
});
if (task) { if (task) {
[task cancel]; [task cancel];
@ -63,56 +68,60 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e
} }
}; };
NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey]; dispatch_async(_processingQueue, ^{
if (pendingBlocks) { NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey];
[pendingBlocks addObject:block]; if (pendingBlocks) {
} else { [pendingBlocks addObject:block];
_pendingBlocks[cacheKey] = [NSMutableArray arrayWithObject:block];
__weak RCTImageDownloader *weakSelf = self;
RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSData *data, NSError *error) {
RCTImageDownloader *strongSelf = weakSelf;
NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
[strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
for (RCTCachedDataDownloadBlock block in blocks) {
block(cached, data, error);
}
};
if ([_cache hasDataForKey:cacheKey]) {
[_cache fetchDataForKey:cacheKey completionHandler:^(NSData *data) {
if (!cancelled) runBlocks(YES, data, nil);
}];
} else { } else {
task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { _pendingBlocks[cacheKey] = [NSMutableArray arrayWithObject:block];
if (!cancelled) runBlocks(NO, data, error);
}];
[task resume]; __weak RCTImageDownloader *weakSelf = self;
RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSData *data, NSError *error) {
RCTImageDownloader *strongSelf = weakSelf;
NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
[strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
for (RCTCachedDataDownloadBlock block in blocks) {
block(cached, data, error);
}
};
if ([_cache hasDataForKey:cacheKey]) {
[_cache fetchDataForKey:cacheKey completionHandler:^(NSData *data) {
if (!cancelled) {
runBlocks(YES, data, nil);
}
}];
} else {
task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!cancelled) {
runBlocks(NO, data, error);
}
}];
[task resume];
}
} }
} });
return [cancel copy]; return [cancel copy];
} }
- (id)downloadDataForURL:(NSURL *)url block:(RCTDataDownloadBlock)block - (id)downloadDataForURL:(NSURL *)url block:(RCTDataDownloadBlock)block
{ {
NSString *cacheKey = [self cacheKeyForURL:url]; NSString *cacheKey = RCTCacheKeyForURL(url);
__weak RCTImageDownloader *weakSelf = self; __weak RCTImageDownloader *weakSelf = self;
return [self _downloadDataForURL:url block:^(BOOL cached, NSData *data, NSError *error) { return [self _downloadDataForURL:url block:^(BOOL cached, NSData *data, NSError *error) {
if (!cached) { if (!cached) {
RCTImageDownloader *strongSelf = weakSelf; RCTImageDownloader *strongSelf = weakSelf;
[strongSelf->_cache setData:data forKey:cacheKey]; [strongSelf->_cache setData:data forKey:cacheKey];
} }
block(data, error);
dispatch_async(dispatch_get_main_queue(), ^{
block(data, error);
});
}]; }];
} }
- (id)downloadImageForURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale block:(RCTImageDownloadBlock)block - (id)downloadImageForURL:(NSURL *)url size:(CGSize)size
scale:(CGFloat)scale block:(RCTImageDownloadBlock)block
{ {
return [self downloadDataForURL:url block:^(NSData *data, NSError *error) { return [self downloadDataForURL:url block:^(NSData *data, NSError *error) {
@ -142,20 +151,14 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e
image = UIGraphicsGetImageFromCurrentImageContext(); image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext(); UIGraphicsEndImageContext();
} }
block(image, nil);
// TODO: should we cache the decompressed image?
dispatch_async(dispatch_get_main_queue(), ^{
block(image, nil);
});
}]; }];
} }
- (void)cancelDownload:(id)downloadToken - (void)cancelDownload:(id)downloadToken
{ {
if (downloadToken) { if (downloadToken) {
dispatch_block_t block = (id)downloadToken; ((dispatch_block_t)downloadToken)();
block();
} }
} }

Просмотреть файл

@ -19,7 +19,8 @@
#import "RCTImageDownloader.h" #import "RCTImageDownloader.h"
#import "RCTLog.h" #import "RCTLog.h"
NSError *errorWithMessage(NSString *message) { NSError *errorWithMessage(NSString *message)
{
NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message};
NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
return error; return error;

Просмотреть файл

@ -61,20 +61,24 @@
if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) { if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) {
_downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) { _downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) {
if (data) { if (data) {
CAKeyframeAnimation *animation = RCTGIFImageWithData(data); dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contentsScale = 1.0; CAKeyframeAnimation *animation = RCTGIFImageWithData(data);
self.layer.minificationFilter = kCAFilterLinear; self.layer.contentsScale = 1.0;
self.layer.magnificationFilter = kCAFilterLinear; self.layer.minificationFilter = kCAFilterLinear;
[self.layer addAnimation:animation forKey:@"contents"]; self.layer.magnificationFilter = kCAFilterLinear;
[self.layer addAnimation:animation forKey:@"contents"];
});
} }
// TODO: handle errors // TODO: handle errors
}]; }];
} else { } else {
_downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() block:^(UIImage *image, NSError *error) { _downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() block:^(UIImage *image, NSError *error) {
if (image) { if (image) {
[self.layer removeAnimationForKey:@"contents"]; dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contentsScale = image.scale; [self.layer removeAnimationForKey:@"contents"];
self.layer.contents = (__bridge id)image.CGImage; self.layer.contentsScale = image.scale;
self.layer.contents = (__bridge id)image.CGImage;
});
} }
// TODO: handle errors // TODO: handle errors
}]; }];