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<RCTImageDataDecoder>> *_decoders;
NSURLCache *_cache;
dispatch_queue_t _URLCacheQueue;
NSURLCache *_URLCache;
}
@synthesize bridge = _bridge;
@@ -87,7 +88,8 @@ RCT_EXPORT_MODULE()
_bridge = bridge;
_loaders = loaders;
_decoders = decoders;
_cache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 // 5MB
_URLCacheQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLCacheQueue", DISPATCH_QUEUE_SERIAL);
_URLCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 // 5MB
diskCapacity:200 * 1024 * 1024 // 200MB
diskPath:@"React/RCTImageDownloader"];
}
@@ -185,12 +187,10 @@ RCT_EXPORT_MODULE()
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
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 void(^cancelLoad)(void) = nil;
__weak RCTImageLoader *weakSelf = self;
RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) {
if ([NSThread isMainThread]) {
@@ -206,16 +206,29 @@ RCT_EXPORT_MODULE()
}
};
if (imageTag.length == 0) {
completionHandler(RCTErrorWithMessage(@"source.uri should not be an empty string"), nil);
return ^{};
}
// All access to URL cache must be serialized
dispatch_async(_URLCacheQueue, ^{
RCTImageLoader *strongSelf = weakSelf;
if (cancelled || !strongSelf) {
return;
}
// Find suitable image URL loader
NSURLRequest *request = [RCTConvert NSURLRequest:imageTag];
id<RCTImageURLLoader> loadHandler = [self imageURLLoaderForURL:request.URL];
id<RCTImageURLLoader> loadHandler = [strongSelf imageURLLoaderForURL:request.URL];
if (loadHandler) {
return [loadHandler loadImageForURL:request.URL
cancelLoad = [loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
progressHandler:progressHandler
completionHandler:completionHandler] ?: ^{};
return;
}
// Check if networking module is available
@@ -223,18 +236,17 @@ RCT_EXPORT_MODULE()
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
if (RCT_DEBUG && ![_bridge.networking canHandleRequest:request]) {
RCTLogError(@"No suitable image URL loader found for %@", imageTag);
return ^{};
return;
}
// Use networking module to load image
__weak RCTImageLoader *weakSelf = self;
__block RCTImageLoaderCancellationBlock decodeCancel = nil;
__block RCTImageLoaderCancellationBlock cancelDecode = nil;
RCTURLRequestCompletionBlock processResponse =
^(NSURLResponse *response, NSData *data, NSError *error) {
@@ -259,21 +271,13 @@ RCT_EXPORT_MODULE()
}
// Decode image
decodeCancel = [weakSelf decodeImageData:data
cancelDecode = [strongSelf 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];
@@ -281,6 +285,13 @@ RCT_EXPORT_MODULE()
request = mutableRequest;
}
// Check for cached response before reloading
// TODO: move URL cache out of RCTImageLoader into its own module
NSCachedURLResponse *cachedResponse = [_URLCache cachedResponseForRequest:request];
if (cachedResponse) {
processResponse(cachedResponse.response, cachedResponse.data, nil);
}
// Download image
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
^(NSURLResponse *response, NSData *data, NSError *error) {
@@ -289,11 +300,12 @@ RCT_EXPORT_MODULE()
return;
}
dispatch_async(_URLCacheQueue, ^{
// Cache the response
// TODO: move URL cache out of RCTImageLoader into its own module
RCTImageLoader *strongSelf = weakSelf;
BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"];
[strongSelf->_cache storeCachedResponse:
[strongSelf->_URLCache storeCachedResponse:
[[NSCachedURLResponse alloc] initWithResponse:response
data:data
userInfo:nil
@@ -303,14 +315,24 @@ RCT_EXPORT_MODULE()
// Process image data
processResponse(response, data, nil);
});
}];
task.downloadProgressBlock = progressHandler;
[task start];
return ^{
cancelLoad = ^{
[task cancel];
if (decodeCancel) {
decodeCancel();
if (cancelDecode) {
cancelDecode();
}
};
});
return ^{
if (cancelLoad) {
cancelLoad();
}
OSAtomicOr32Barrier(1, &cancelled);
};
@@ -342,7 +364,7 @@ RCT_EXPORT_MODULE()
if (cancelled) {
return;
}
UIImage *image = [UIImage imageWithData:data scale:scale];
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
if (image) {
completionHandler(nil, image);
} else {

View File

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

View File

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

View File

@@ -429,7 +429,18 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
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;
CGFloat scale = 0.0;
BOOL isPackagerAsset = NO;
@@ -452,7 +463,6 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
if (RCTIsXCAssetURL(URL)) {
// 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
RCTAssertMainThread();
NSString *assetName = RCTBundlePathForURL(URL);
image = [UIImage imageNamed:assetName];
} else {