Skip to content

Commit 54f7eb3

Browse files
esamelsonfacebook-github-bot
authored andcommitted
Performance improvement for loading cached images on iOS (#20356)
Summary: This PR increases the speed at which cached images are loaded and displayed on the screen. Images are currently cached in memory using RCTImageCache, but each time they are loaded, a round trip through RCTNetworking happens before RCTImageCache is even checked. This is likely so that RCTNetworking can handle the caching behavior required by the HTTP headers. However, this means that at the very least, images are read from disk each time they're loaded. This PR makes RCTImageLoader check RCTImageCache _before_ sending a request to RCTNetworking. RCTImageCache stores a bit of information about the response headers so that it can respect Cache-Control fields without needing a roundtrip through RCTNetworking. Here are a couple of graphs showing improved loading times before this change (blue) and after (red) with SDWebImage (yellow) as a baseline comparison. The increase is most evident when loading especially large (hi-res photo size) images, or loading multiple images at a time. https://imgur.com/a/cnL47Z0 More performance gains can potentially be had by increasing the size limit of RCTImageCache: https://github.com/facebook/react-native/blob/1a6666a116fd8b9e8637956de2b41a1c315dd470/Libraries/Image/RCTImageCache.m#L39 but this comes at the tradeoff of being more likely to run into OOM crashes. Pull Request resolved: #20356 Reviewed By: PeteTheHeat Differential Revision: D12909121 Pulled By: alsun2001 fbshipit-source-id: 7f5e21928c53d7aa53f293b7f1b4ec5c99b5f0c2
1 parent 9d13233 commit 54f7eb3

File tree

3 files changed

+101
-48
lines changed

3 files changed

+101
-48
lines changed

Libraries/Image/RCTImageCache.m

+60-8
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,27 @@
2121
static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB
2222

2323
static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale,
24-
RCTResizeMode resizeMode, NSString *responseDate)
24+
RCTResizeMode resizeMode)
2525
{
26-
return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld|%@",
27-
imageTag, size.width, size.height, scale, (long long)resizeMode, responseDate];
26+
return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld",
27+
imageTag, size.width, size.height, scale, (long long)resizeMode];
2828
}
2929

3030
@implementation RCTImageCache
3131
{
3232
NSOperationQueue *_imageDecodeQueue;
3333
NSCache *_decodedImageCache;
34+
NSMutableDictionary *_cacheStaleTimes;
35+
36+
NSDateFormatter *_headerDateFormatter;
3437
}
3538

3639
- (instancetype)init
3740
{
3841
_decodedImageCache = [NSCache new];
3942
_decodedImageCache.totalCostLimit = 5 * 1024 * 1024; // 5MB
40-
43+
_cacheStaleTimes = [[NSMutableDictionary alloc] init];
44+
4145
[[NSNotificationCenter defaultCenter] addObserver:self
4246
selector:@selector(clearCache)
4347
name:UIApplicationDidReceiveMemoryWarningNotification
@@ -58,6 +62,9 @@ - (void)dealloc
5862
- (void)clearCache
5963
{
6064
[_decodedImageCache removeAllObjects];
65+
@synchronized(_cacheStaleTimes) {
66+
[_cacheStaleTimes removeAllObjects];
67+
}
6168
}
6269

6370
- (void)addImageToCache:(UIImage *)image
@@ -78,9 +85,19 @@ - (UIImage *)imageForUrl:(NSString *)url
7885
size:(CGSize)size
7986
scale:(CGFloat)scale
8087
resizeMode:(RCTResizeMode)resizeMode
81-
responseDate:(NSString *)responseDate
8288
{
83-
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate);
89+
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
90+
@synchronized(_cacheStaleTimes) {
91+
id staleTime = _cacheStaleTimes[cacheKey];
92+
if (staleTime) {
93+
if ([[NSDate new] compare:(NSDate *)staleTime] == NSOrderedDescending) {
94+
// cached image has expired, clear it out to make room for others
95+
[_cacheStaleTimes removeObjectForKey:cacheKey];
96+
[_decodedImageCache removeObjectForKey:cacheKey];
97+
return nil;
98+
}
99+
}
100+
}
84101
return [_decodedImageCache objectForKey:cacheKey];
85102
}
86103

@@ -90,9 +107,44 @@ - (void)addImageToCache:(UIImage *)image
90107
scale:(CGFloat)scale
91108
resizeMode:(RCTResizeMode)resizeMode
92109
responseDate:(NSString *)responseDate
110+
cacheControl:(NSString *)cacheControl
93111
{
94-
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate);
95-
return [self addImageToCache:image forKey:cacheKey];
112+
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
113+
BOOL shouldCache = YES;
114+
NSDate *staleTime;
115+
NSArray<NSString *> *components = [cacheControl componentsSeparatedByString:@","];
116+
for (NSString *component in components) {
117+
if ([component containsString:@"no-cache"] || [component containsString:@"no-store"] || [component hasSuffix:@"max-age=0"]) {
118+
shouldCache = NO;
119+
break;
120+
} else {
121+
NSRange range = [component rangeOfString:@"max-age="];
122+
if (range.location != NSNotFound) {
123+
NSInteger seconds = [[component substringFromIndex:range.location + range.length] integerValue];
124+
NSDate *originalDate = [self dateWithHeaderString:responseDate];
125+
staleTime = [originalDate dateByAddingTimeInterval:(NSTimeInterval)seconds];
126+
}
127+
}
128+
}
129+
if (shouldCache) {
130+
if (staleTime) {
131+
@synchronized(_cacheStaleTimes) {
132+
_cacheStaleTimes[cacheKey] = staleTime;
133+
}
134+
}
135+
return [self addImageToCache:image forKey:cacheKey];
136+
}
137+
}
138+
139+
- (NSDate *)dateWithHeaderString:(NSString *)headerDateString {
140+
if (_headerDateFormatter == nil) {
141+
_headerDateFormatter = [[NSDateFormatter alloc] init];
142+
_headerDateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
143+
_headerDateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
144+
_headerDateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
145+
}
146+
147+
return [_headerDateFormatter dateFromString:headerDateString];
96148
}
97149

98150
@end

Libraries/Image/RCTImageLoader.h

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock;
2424
- (UIImage *)imageForUrl:(NSString *)url
2525
size:(CGSize)size
2626
scale:(CGFloat)scale
27-
resizeMode:(RCTResizeMode)resizeMode
28-
responseDate:(NSString *)responseDate;
27+
resizeMode:(RCTResizeMode)resizeMode;
2928

3029
- (void)addImageToCache:(UIImage *)image
3130
URL:(NSString *)url
3231
size:(CGSize)size
3332
scale:(CGFloat)scale
3433
resizeMode:(RCTResizeMode)resizeMode
35-
responseDate:(NSString *)responseDate;
34+
responseDate:(NSString *)responseDate
35+
cacheControl:(NSString *)cacheControl;
3636

3737
@end
3838

Libraries/Image/RCTImageLoader.m

+38-37
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
321321
resizeMode:(RCTResizeMode)resizeMode
322322
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
323323
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
324-
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate))completionBlock
324+
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl))completionBlock
325325
{
326326
{
327327
NSMutableURLRequest *mutableRequest = [request mutableCopy];
@@ -344,27 +344,27 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
344344
BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ?
345345
[loadHandler requiresScheduling] : YES;
346346

347+
BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ?
348+
[loadHandler shouldCacheLoadedImages] : YES;
349+
347350
__block atomic_bool cancelled = ATOMIC_VAR_INIT(NO);
348351
// TODO: Protect this variable shared between threads.
349352
__block dispatch_block_t cancelLoad = nil;
350-
void (^completionHandler)(NSError *, id, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate) {
353+
void (^completionHandler)(NSError *, id, NSString *, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate, NSString *cacheControl) {
351354
cancelLoad = nil;
352355

353-
BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ?
354-
[loadHandler shouldCacheLoadedImages] : YES;
355-
356356
// If we've received an image, we should try to set it synchronously,
357357
// if it's data, do decoding on a background thread.
358358
if (RCTIsMainQueue() && ![imageOrData isKindOfClass:[UIImage class]]) {
359359
// Most loaders do not return on the main thread, so caller is probably not
360360
// expecting it, and may do expensive post-processing in the callback
361361
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
362362
if (!atomic_load(&cancelled)) {
363-
completionBlock(error, imageOrData, cacheResult, fetchDate);
363+
completionBlock(error, imageOrData, cacheResult, fetchDate, cacheControl);
364364
}
365365
});
366366
} else if (!atomic_load(&cancelled)) {
367-
completionBlock(error, imageOrData, cacheResult, fetchDate);
367+
completionBlock(error, imageOrData, cacheResult, fetchDate, cacheControl);
368368
}
369369
};
370370

@@ -378,7 +378,7 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
378378
progressHandler:progressHandler
379379
partialLoadHandler:partialLoadHandler
380380
completionHandler:^(NSError *error, UIImage *image){
381-
completionHandler(error, image, nil);
381+
completionHandler(error, image, nil, nil);
382382
}];
383383
}
384384

@@ -402,13 +402,25 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
402402
progressHandler:progressHandler
403403
partialLoadHandler:partialLoadHandler
404404
completionHandler:^(NSError *error, UIImage *image) {
405-
completionHandler(error, image, nil);
405+
completionHandler(error, image, nil, nil);
406406
}];
407407
} else {
408-
// Use networking module to load image
409-
cancelLoad = [strongSelf _loadURLRequest:request
410-
progressBlock:progressHandler
411-
completionBlock:completionHandler];
408+
UIImage *image;
409+
if (cacheResult) {
410+
image = [[strongSelf imageCache] imageForUrl:request.URL.absoluteString
411+
size:size
412+
scale:scale
413+
resizeMode:resizeMode];
414+
}
415+
416+
if (image) {
417+
completionHandler(nil, image, nil, nil);
418+
} else {
419+
// Use networking module to load image
420+
cancelLoad = [strongSelf _loadURLRequest:request
421+
progressBlock:progressHandler
422+
completionBlock:completionHandler];
423+
}
412424
}
413425
});
414426

@@ -427,7 +439,7 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
427439

428440
- (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
429441
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
430-
completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate))completionHandler
442+
completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate, NSString *cacheControl))completionHandler
431443
{
432444
// Check if networking module is available
433445
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
@@ -449,34 +461,36 @@ - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
449461
RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) {
450462
// Check for system errors
451463
if (error) {
452-
completionHandler(error, nil, nil);
464+
completionHandler(error, nil, nil, nil);
453465
return;
454466
} else if (!response) {
455-
completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil);
467+
completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil, nil);
456468
return;
457469
} else if (!data) {
458-
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil);
470+
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil, nil);
459471
return;
460472
}
461473

462474
// Check for http errors
463475
NSString *responseDate;
476+
NSString *cacheControl;
464477
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
465478
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
466479
if (statusCode != 200) {
467480
NSString *errorMessage = [NSString stringWithFormat:@"Failed to load %@", response.URL];
468481
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: errorMessage};
469482
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
470483
code:statusCode
471-
userInfo:userInfo], nil, nil);
484+
userInfo:userInfo], nil, nil, nil);
472485
return;
473486
}
474487

475488
responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"];
489+
cacheControl = ((NSHTTPURLResponse *)response).allHeaderFields[@"Cache-Control"];
476490
}
477491

478492
// Call handler
479-
completionHandler(nil, data, responseDate);
493+
completionHandler(nil, data, responseDate, cacheControl);
480494
};
481495

482496
// Download image
@@ -498,7 +512,7 @@ - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
498512
} else {
499513
someError = RCTErrorWithMessage(@"Unknown image download error");
500514
}
501-
completionHandler(someError, nil, nil);
515+
completionHandler(someError, nil, nil, nil);
502516
[strongSelf dequeueTasks];
503517
return;
504518
}
@@ -564,7 +578,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image
564578
};
565579

566580
__weak RCTImageLoader *weakSelf = self;
567-
void (^completionHandler)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) {
581+
void (^completionHandler)(NSError *, id, BOOL, NSString *, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl) {
568582
__typeof(self) strongSelf = weakSelf;
569583
if (atomic_load(&cancelled) || !strongSelf) {
570584
return;
@@ -576,20 +590,6 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image
576590
return;
577591
}
578592

579-
// Check decoded image cache
580-
if (cacheResult) {
581-
UIImage *image = [[strongSelf imageCache] imageForUrl:imageURLRequest.URL.absoluteString
582-
size:size
583-
scale:scale
584-
resizeMode:resizeMode
585-
responseDate:fetchDate];
586-
if (image) {
587-
cancelLoad = nil;
588-
completionBlock(nil, image);
589-
return;
590-
}
591-
}
592-
593593
RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) {
594594
if (cacheResult && image) {
595595
// Store decoded image in cache
@@ -598,7 +598,8 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image
598598
size:size
599599
scale:scale
600600
resizeMode:resizeMode
601-
responseDate:fetchDate];
601+
responseDate:fetchDate
602+
cacheControl:cacheControl];
602603
}
603604

604605
cancelLoad = nil;
@@ -732,7 +733,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
732733
- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest
733734
block:(void(^)(NSError *error, CGSize size))callback
734735
{
735-
void (^completion)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) {
736+
void (^completion)(NSError *, id, BOOL, NSString *, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl) {
736737
CGSize size;
737738
if ([imageOrData isKindOfClass:[NSData class]]) {
738739
NSDictionary *meta = RCTGetImageMetadata(imageOrData);

0 commit comments

Comments
 (0)