Removed all calls to [UIImage imageWithData:] on a background thread

Summary: public

I had previously assumed (based on past experience and common wisdom) that `[UIImage imageWithData:]` was safe to call concurrently and/or off the main thread, but it seems that may not be the case (see https://github.com/AFNetworking/AFNetworking/pull/2815).

This diff replaces `[UIImage imageWithData:]` with ImageIO-based decoding wherever possible, and ensures that it is called on the main thread wherever that's not possible/convenient.

I've also serialized access to the `NSURLCache` inside `RCTImageLoader`, which was causing a separate-but-similar crash when loading images.

Reviewed By: fkgozali

Differential Revision: D2678369

fb-gh-sync-id: 74d033dafcf6c412556e4c96f5ac5d3432298b18
This commit is contained in:
Nick Lockwood 2015-11-20 05:15:49 -08:00 committed by facebook-github-bot-6
parent 1a1c3f76a2
commit 0fe074acbd
4 changed files with 137 additions and 105 deletions

View File

@ -37,7 +37,8 @@
{ {
NSArray<id<RCTImageURLLoader>> *_loaders; NSArray<id<RCTImageURLLoader>> *_loaders;
NSArray<id<RCTImageDataDecoder>> *_decoders; NSArray<id<RCTImageDataDecoder>> *_decoders;
NSURLCache *_cache; dispatch_queue_t _URLCacheQueue;
NSURLCache *_URLCache;
} }
@synthesize bridge = _bridge; @synthesize bridge = _bridge;
@ -87,9 +88,10 @@ RCT_EXPORT_MODULE()
_bridge = bridge; _bridge = bridge;
_loaders = loaders; _loaders = loaders;
_decoders = decoders; _decoders = decoders;
_cache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 // 5MB _URLCacheQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLCacheQueue", DISPATCH_QUEUE_SERIAL);
diskCapacity:200 * 1024 * 1024 // 200MB _URLCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 // 5MB
diskPath:@"React/RCTImageDownloader"]; diskCapacity:200 * 1024 * 1024 // 200MB
diskPath:@"React/RCTImageDownloader"];
} }
- (id<RCTImageURLLoader>)imageURLLoaderForURL:(NSURL *)URL - (id<RCTImageURLLoader>)imageURLLoaderForURL:(NSURL *)URL
@ -185,12 +187,10 @@ RCT_EXPORT_MODULE()
progressBlock:(RCTImageLoaderProgressBlock)progressHandler progressBlock:(RCTImageLoaderProgressBlock)progressHandler
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{ {
if (imageTag.length == 0) {
RCTLogWarn(@"source.uri should not be an empty string <Native>");
return ^{};
}
__block volatile uint32_t cancelled = 0; __block volatile uint32_t cancelled = 0;
__block void(^cancelLoad)(void) = nil;
__weak RCTImageLoader *weakSelf = self;
RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) { RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) {
if ([NSThread isMainThread]) { if ([NSThread isMainThread]) {
@ -206,111 +206,133 @@ RCT_EXPORT_MODULE()
} }
}; };
// Find suitable image URL loader if (imageTag.length == 0) {
NSURLRequest *request = [RCTConvert NSURLRequest:imageTag]; completionHandler(RCTErrorWithMessage(@"source.uri should not be an empty string"), nil);
id<RCTImageURLLoader> loadHandler = [self imageURLLoaderForURL:request.URL];
if (loadHandler) {
return [loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
progressHandler:progressHandler
completionHandler:completionHandler] ?: ^{};
}
// Check if networking module is available
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
RCTLogError(@"No suitable image URL loader found for %@. You may need to "
" import the RCTNetworking library in order to load images.",
imageTag);
return ^{}; return ^{};
} }
// Check if networking module can load image // All access to URL cache must be serialized
if (RCT_DEBUG && ![_bridge.networking canHandleRequest:request]) { dispatch_async(_URLCacheQueue, ^{
RCTLogError(@"No suitable image URL loader found for %@", imageTag); RCTImageLoader *strongSelf = weakSelf;
return ^{}; if (cancelled || !strongSelf) {
}
// Use networking module to load image
__weak RCTImageLoader *weakSelf = self;
__block RCTImageLoaderCancellationBlock decodeCancel = nil;
RCTURLRequestCompletionBlock processResponse =
^(NSURLResponse *response, NSData *data, NSError *error) {
// Check for system errors
if (error) {
completionHandler(error, nil);
return;
} else if (!data) {
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil);
return; return;
} }
// Check for http errors // Find suitable image URL loader
if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSURLRequest *request = [RCTConvert NSURLRequest:imageTag];
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; id<RCTImageURLLoader> loadHandler = [strongSelf imageURLLoaderForURL:request.URL];
if (statusCode != 200) { if (loadHandler) {
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain cancelLoad = [loadHandler loadImageForURL:request.URL
code:statusCode size:size
userInfo:nil], nil); scale:scale
resizeMode:resizeMode
progressHandler:progressHandler
completionHandler:completionHandler] ?: ^{};
return;
}
// Check if networking module is available
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
RCTLogError(@"No suitable image URL loader found for %@. You may need to "
" import the RCTNetworking library in order to load images.",
imageTag);
return;
}
// Check if networking module can load image
if (RCT_DEBUG && ![_bridge.networking canHandleRequest:request]) {
RCTLogError(@"No suitable image URL loader found for %@", imageTag);
return;
}
// Use networking module to load image
__block RCTImageLoaderCancellationBlock cancelDecode = nil;
RCTURLRequestCompletionBlock processResponse =
^(NSURLResponse *response, NSData *data, NSError *error) {
// Check for system errors
if (error) {
completionHandler(error, nil);
return;
} else if (!data) {
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil);
return; return;
} }
// Check for http errors
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (statusCode != 200) {
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
code:statusCode
userInfo:nil], nil);
return;
}
}
// Decode image
cancelDecode = [strongSelf decodeImageData:data
size:size
scale:scale
resizeMode:resizeMode
completionBlock:completionHandler];
};
// Add missing png extension
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]];
request = mutableRequest;
} }
// Decode image // Check for cached response before reloading
decodeCancel = [weakSelf decodeImageData:data
size:size
scale:scale
resizeMode:resizeMode
completionBlock:completionHandler];
};
// Check for cached response before reloading
// TODO: move URL cache out of RCTImageLoader into its own module
NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
if (cachedResponse) {
processResponse(cachedResponse.response, cachedResponse.data, nil);
return ^{};
}
// Add missing png extension
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]];
request = mutableRequest;
}
// Download image
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
^(NSURLResponse *response, NSData *data, NSError *error) {
if (error) {
completionHandler(error, nil);
return;
}
// Cache the response
// TODO: move URL cache out of RCTImageLoader into its own module // TODO: move URL cache out of RCTImageLoader into its own module
RCTImageLoader *strongSelf = weakSelf; NSCachedURLResponse *cachedResponse = [_URLCache cachedResponseForRequest:request];
BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"]; if (cachedResponse) {
[strongSelf->_cache storeCachedResponse: processResponse(cachedResponse.response, cachedResponse.data, nil);
[[NSCachedURLResponse alloc] initWithResponse:response }
data:data
userInfo:nil
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
forRequest:request];
// Process image data // Download image
processResponse(response, data, nil); RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
^(NSURLResponse *response, NSData *data, NSError *error) {
if (error) {
completionHandler(error, nil);
return;
}
}]; dispatch_async(_URLCacheQueue, ^{
task.downloadProgressBlock = progressHandler;
[task start]; // Cache the response
// TODO: move URL cache out of RCTImageLoader into its own module
BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"];
[strongSelf->_URLCache storeCachedResponse:
[[NSCachedURLResponse alloc] initWithResponse:response
data:data
userInfo:nil
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
forRequest:request];
// Process image data
processResponse(response, data, nil);
});
}];
task.downloadProgressBlock = progressHandler;
[task start];
cancelLoad = ^{
[task cancel];
if (cancelDecode) {
cancelDecode();
}
};
});
return ^{ return ^{
[task cancel]; if (cancelLoad) {
if (decodeCancel) { cancelLoad();
decodeCancel();
} }
OSAtomicOr32Barrier(1, &cancelled); OSAtomicOr32Barrier(1, &cancelled);
}; };
@ -342,7 +364,7 @@ RCT_EXPORT_MODULE()
if (cancelled) { if (cancelled) {
return; return;
} }
UIImage *image = [UIImage imageWithData:data scale:scale]; UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
if (image) { if (image) {
completionHandler(nil, image); completionHandler(nil, image);
} else { } else {

View File

@ -213,9 +213,8 @@ RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
RCTAssertParam(block); RCTAssertParam(block);
dispatch_async(_methodQueue, ^{ dispatch_async(_methodQueue, ^{
NSData *imageData = _store[imageTag]; NSData *imageData = _store[imageTag];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
block(image); block([UIImage imageWithData:imageData]);
}); });
}); });
} }

View File

@ -255,7 +255,8 @@ UIImage *RCTDecodeImageWithData(NSData *data,
// adjust scale // adjust scale
size_t actualWidth = CGImageGetWidth(imageRef); size_t actualWidth = CGImageGetWidth(imageRef);
CGFloat scale = actualWidth / targetSize.width; CGFloat scale = actualWidth / targetSize.width * destScale;
// return image // return image
UIImage *image = [UIImage imageWithCGImage:imageRef UIImage *image = [UIImage imageWithCGImage:imageRef
scale:scale scale:scale

View File

@ -429,7 +429,18 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
return nil; return nil;
} }
UIImage *image; __block UIImage *image;
if (![NSThread isMainThread]) {
// It seems that none of the UIImage loading methods can be guaranteed
// thread safe, so we'll pick the lesser of two evils here and block rather
// than run the risk of crashing
RCTLogWarn(@"Calling [RCTConvert UIImage:] on a background thread is not recommended");
dispatch_sync(dispatch_get_main_queue(), ^{
image = [self UIImage:json];
});
return image;
}
NSString *path; NSString *path;
CGFloat scale = 0.0; CGFloat scale = 0.0;
BOOL isPackagerAsset = NO; BOOL isPackagerAsset = NO;
@ -452,7 +463,6 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
if (RCTIsXCAssetURL(URL)) { if (RCTIsXCAssetURL(URL)) {
// Image may reside inside a .car file, in which case we have no choice // Image may reside inside a .car file, in which case we have no choice
// but to use +[UIImage imageNamed] - but this method isn't thread safe // but to use +[UIImage imageNamed] - but this method isn't thread safe
RCTAssertMainThread();
NSString *assetName = RCTBundlePathForURL(URL); NSString *assetName = RCTBundlePathForURL(URL);
image = [UIImage imageNamed:assetName]; image = [UIImage imageNamed:assetName];
} else { } else {