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:
parent
1a1c3f76a2
commit
0fe074acbd
|
@ -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 {
|
||||||
|
|
|
@ -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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue