Skip to content

Commit 0fe074a

Browse files
nicklockwoodfacebook-github-bot-6
authored andcommitted
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 AFNetworking/AFNetworking#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
1 parent 1a1c3f7 commit 0fe074a

File tree

4 files changed

+131
-99
lines changed

4 files changed

+131
-99
lines changed

Libraries/Image/RCTImageLoader.m

Lines changed: 116 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ @implementation RCTImageLoader
3737
{
3838
NSArray<id<RCTImageURLLoader>> *_loaders;
3939
NSArray<id<RCTImageDataDecoder>> *_decoders;
40-
NSURLCache *_cache;
40+
dispatch_queue_t _URLCacheQueue;
41+
NSURLCache *_URLCache;
4142
}
4243

4344
@synthesize bridge = _bridge;
@@ -87,9 +88,10 @@ - (void)setBridge:(RCTBridge *)bridge
8788
_bridge = bridge;
8889
_loaders = loaders;
8990
_decoders = decoders;
90-
_cache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 // 5MB
91-
diskCapacity:200 * 1024 * 1024 // 200MB
92-
diskPath:@"React/RCTImageDownloader"];
91+
_URLCacheQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLCacheQueue", DISPATCH_QUEUE_SERIAL);
92+
_URLCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 // 5MB
93+
diskCapacity:200 * 1024 * 1024 // 200MB
94+
diskPath:@"React/RCTImageDownloader"];
9395
}
9496

9597
- (id<RCTImageURLLoader>)imageURLLoaderForURL:(NSURL *)URL
@@ -185,12 +187,10 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
185187
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
186188
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
187189
{
188-
if (imageTag.length == 0) {
189-
RCTLogWarn(@"source.uri should not be an empty string <Native>");
190-
return ^{};
191-
}
192-
193190
__block volatile uint32_t cancelled = 0;
191+
__block void(^cancelLoad)(void) = nil;
192+
__weak RCTImageLoader *weakSelf = self;
193+
194194
RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) {
195195
if ([NSThread isMainThread]) {
196196

@@ -206,111 +206,133 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
206206
}
207207
};
208208

209-
// Find suitable image URL loader
210-
NSURLRequest *request = [RCTConvert NSURLRequest:imageTag];
211-
id<RCTImageURLLoader> loadHandler = [self imageURLLoaderForURL:request.URL];
212-
if (loadHandler) {
213-
return [loadHandler loadImageForURL:request.URL
214-
size:size
215-
scale:scale
216-
resizeMode:resizeMode
217-
progressHandler:progressHandler
218-
completionHandler:completionHandler] ?: ^{};
219-
}
220-
221-
// Check if networking module is available
222-
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
223-
RCTLogError(@"No suitable image URL loader found for %@. You may need to "
224-
" import the RCTNetworking library in order to load images.",
225-
imageTag);
209+
if (imageTag.length == 0) {
210+
completionHandler(RCTErrorWithMessage(@"source.uri should not be an empty string"), nil);
226211
return ^{};
227212
}
228213

229-
// Check if networking module can load image
230-
if (RCT_DEBUG && ![_bridge.networking canHandleRequest:request]) {
231-
RCTLogError(@"No suitable image URL loader found for %@", imageTag);
232-
return ^{};
233-
}
214+
// All access to URL cache must be serialized
215+
dispatch_async(_URLCacheQueue, ^{
216+
RCTImageLoader *strongSelf = weakSelf;
217+
if (cancelled || !strongSelf) {
218+
return;
219+
}
234220

235-
// Use networking module to load image
236-
__weak RCTImageLoader *weakSelf = self;
237-
__block RCTImageLoaderCancellationBlock decodeCancel = nil;
238-
RCTURLRequestCompletionBlock processResponse =
239-
^(NSURLResponse *response, NSData *data, NSError *error) {
221+
// Find suitable image URL loader
222+
NSURLRequest *request = [RCTConvert NSURLRequest:imageTag];
223+
id<RCTImageURLLoader> loadHandler = [strongSelf imageURLLoaderForURL:request.URL];
224+
if (loadHandler) {
225+
cancelLoad = [loadHandler loadImageForURL:request.URL
226+
size:size
227+
scale:scale
228+
resizeMode:resizeMode
229+
progressHandler:progressHandler
230+
completionHandler:completionHandler] ?: ^{};
231+
return;
232+
}
240233

241-
// Check for system errors
242-
if (error) {
243-
completionHandler(error, nil);
234+
// Check if networking module is available
235+
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
236+
RCTLogError(@"No suitable image URL loader found for %@. You may need to "
237+
" import the RCTNetworking library in order to load images.",
238+
imageTag);
244239
return;
245-
} else if (!data) {
246-
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil);
240+
}
241+
242+
// Check if networking module can load image
243+
if (RCT_DEBUG && ![_bridge.networking canHandleRequest:request]) {
244+
RCTLogError(@"No suitable image URL loader found for %@", imageTag);
247245
return;
248246
}
249247

250-
// Check for http errors
251-
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
252-
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
253-
if (statusCode != 200) {
254-
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
255-
code:statusCode
256-
userInfo:nil], nil);
248+
// Use networking module to load image
249+
__block RCTImageLoaderCancellationBlock cancelDecode = nil;
250+
RCTURLRequestCompletionBlock processResponse =
251+
^(NSURLResponse *response, NSData *data, NSError *error) {
252+
253+
// Check for system errors
254+
if (error) {
255+
completionHandler(error, nil);
256+
return;
257+
} else if (!data) {
258+
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil);
257259
return;
258260
}
259-
}
260261

261-
// Decode image
262-
decodeCancel = [weakSelf decodeImageData:data
263-
size:size
264-
scale:scale
265-
resizeMode:resizeMode
266-
completionBlock:completionHandler];
267-
};
268-
269-
// Check for cached response before reloading
270-
// TODO: move URL cache out of RCTImageLoader into its own module
271-
NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
272-
if (cachedResponse) {
273-
processResponse(cachedResponse.response, cachedResponse.data, nil);
274-
return ^{};
275-
}
262+
// Check for http errors
263+
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
264+
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
265+
if (statusCode != 200) {
266+
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
267+
code:statusCode
268+
userInfo:nil], nil);
269+
return;
270+
}
271+
}
276272

277-
// Add missing png extension
278-
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
279-
NSMutableURLRequest *mutableRequest = [request mutableCopy];
280-
mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]];
281-
request = mutableRequest;
282-
}
273+
// Decode image
274+
cancelDecode = [strongSelf decodeImageData:data
275+
size:size
276+
scale:scale
277+
resizeMode:resizeMode
278+
completionBlock:completionHandler];
279+
};
283280

284-
// Download image
285-
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
286-
^(NSURLResponse *response, NSData *data, NSError *error) {
287-
if (error) {
288-
completionHandler(error, nil);
289-
return;
281+
// Add missing png extension
282+
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
283+
NSMutableURLRequest *mutableRequest = [request mutableCopy];
284+
mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]];
285+
request = mutableRequest;
290286
}
291287

292-
// Cache the response
288+
// Check for cached response before reloading
293289
// TODO: move URL cache out of RCTImageLoader into its own module
294-
RCTImageLoader *strongSelf = weakSelf;
295-
BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"];
296-
[strongSelf->_cache storeCachedResponse:
297-
[[NSCachedURLResponse alloc] initWithResponse:response
298-
data:data
299-
userInfo:nil
300-
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
301-
forRequest:request];
290+
NSCachedURLResponse *cachedResponse = [_URLCache cachedResponseForRequest:request];
291+
if (cachedResponse) {
292+
processResponse(cachedResponse.response, cachedResponse.data, nil);
293+
}
302294

303-
// Process image data
304-
processResponse(response, data, nil);
295+
// Download image
296+
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
297+
^(NSURLResponse *response, NSData *data, NSError *error) {
298+
if (error) {
299+
completionHandler(error, nil);
300+
return;
301+
}
305302

306-
}];
307-
task.downloadProgressBlock = progressHandler;
308-
[task start];
303+
dispatch_async(_URLCacheQueue, ^{
304+
305+
// Cache the response
306+
// TODO: move URL cache out of RCTImageLoader into its own module
307+
BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"];
308+
[strongSelf->_URLCache storeCachedResponse:
309+
[[NSCachedURLResponse alloc] initWithResponse:response
310+
data:data
311+
userInfo:nil
312+
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
313+
forRequest:request];
314+
315+
// Process image data
316+
processResponse(response, data, nil);
317+
318+
});
319+
320+
}];
321+
task.downloadProgressBlock = progressHandler;
322+
[task start];
323+
324+
cancelLoad = ^{
325+
[task cancel];
326+
if (cancelDecode) {
327+
cancelDecode();
328+
}
329+
};
330+
331+
});
309332

310333
return ^{
311-
[task cancel];
312-
if (decodeCancel) {
313-
decodeCancel();
334+
if (cancelLoad) {
335+
cancelLoad();
314336
}
315337
OSAtomicOr32Barrier(1, &cancelled);
316338
};
@@ -342,7 +364,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
342364
if (cancelled) {
343365
return;
344366
}
345-
UIImage *image = [UIImage imageWithData:data scale:scale];
367+
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
346368
if (image) {
347369
completionHandler(nil, image);
348370
} else {

Libraries/Image/RCTImageStoreManager.m

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,8 @@ - (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))
213213
RCTAssertParam(block);
214214
dispatch_async(_methodQueue, ^{
215215
NSData *imageData = _store[imageTag];
216-
UIImage *image = [UIImage imageWithData:imageData];
217216
dispatch_async(dispatch_get_main_queue(), ^{
218-
block(image);
217+
block([UIImage imageWithData:imageData]);
219218
});
220219
});
221220
}

Libraries/Image/RCTImageUtils.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
255255

256256
// adjust scale
257257
size_t actualWidth = CGImageGetWidth(imageRef);
258-
CGFloat scale = actualWidth / targetSize.width;
258+
CGFloat scale = actualWidth / targetSize.width * destScale;
259+
259260
// return image
260261
UIImage *image = [UIImage imageWithCGImage:imageRef
261262
scale:scale

React/Base/RCTConvert.m

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,18 @@ + (UIImage *)UIImage:(id)json
429429
return nil;
430430
}
431431

432-
UIImage *image;
432+
__block UIImage *image;
433+
if (![NSThread isMainThread]) {
434+
// It seems that none of the UIImage loading methods can be guaranteed
435+
// thread safe, so we'll pick the lesser of two evils here and block rather
436+
// than run the risk of crashing
437+
RCTLogWarn(@"Calling [RCTConvert UIImage:] on a background thread is not recommended");
438+
dispatch_sync(dispatch_get_main_queue(), ^{
439+
image = [self UIImage:json];
440+
});
441+
return image;
442+
}
443+
433444
NSString *path;
434445
CGFloat scale = 0.0;
435446
BOOL isPackagerAsset = NO;
@@ -452,7 +463,6 @@ + (UIImage *)UIImage:(id)json
452463
if (RCTIsXCAssetURL(URL)) {
453464
// Image may reside inside a .car file, in which case we have no choice
454465
// but to use +[UIImage imageNamed] - but this method isn't thread safe
455-
RCTAssertMainThread();
456466
NSString *assetName = RCTBundlePathForURL(URL);
457467
image = [UIImage imageNamed:assetName];
458468
} else {

0 commit comments

Comments
 (0)