mirror of
https://github.com/status-im/react-native.git
synced 2025-01-13 11:05:21 +00:00
2ca7701aae
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: 1a6666a116/Libraries/Image/RCTImageCache.m (L39)
but this comes at the tradeoff of being more likely to run into OOM crashes.
Pull Request resolved: https://github.com/facebook/react-native/pull/20356
Reviewed By: shergin
Differential Revision: D8978844
Pulled By: hramos
fbshipit-source-id: 4b86043bc14c40007b0596c9f8a213455b697686
151 lines
4.8 KiB
Objective-C
151 lines
4.8 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTImageCache.h"
|
|
|
|
#import <objc/runtime.h>
|
|
|
|
#import <ImageIO/ImageIO.h>
|
|
|
|
#import <React/RCTConvert.h>
|
|
#import <React/RCTNetworking.h>
|
|
#import <React/RCTUtils.h>
|
|
#import <React/RCTResizeMode.h>
|
|
|
|
#import "RCTImageUtils.h"
|
|
|
|
static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB
|
|
|
|
static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale,
|
|
RCTResizeMode resizeMode)
|
|
{
|
|
return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld",
|
|
imageTag, size.width, size.height, scale, (long long)resizeMode];
|
|
}
|
|
|
|
@implementation RCTImageCache
|
|
{
|
|
NSOperationQueue *_imageDecodeQueue;
|
|
NSCache *_decodedImageCache;
|
|
NSMutableDictionary *_cacheStaleTimes;
|
|
|
|
NSDateFormatter *_headerDateFormatter;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
_decodedImageCache = [NSCache new];
|
|
_decodedImageCache.totalCostLimit = 5 * 1024 * 1024; // 5MB
|
|
_cacheStaleTimes = [[NSMutableDictionary alloc] init];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(clearCache)
|
|
name:UIApplicationDidReceiveMemoryWarningNotification
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(clearCache)
|
|
name:UIApplicationWillResignActiveNotification
|
|
object:nil];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)clearCache
|
|
{
|
|
[_decodedImageCache removeAllObjects];
|
|
@synchronized(_cacheStaleTimes) {
|
|
[_cacheStaleTimes removeAllObjects];
|
|
}
|
|
}
|
|
|
|
- (void)addImageToCache:(UIImage *)image
|
|
forKey:(NSString *)cacheKey
|
|
{
|
|
if (!image) {
|
|
return;
|
|
}
|
|
CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4;
|
|
if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) {
|
|
[self->_decodedImageCache setObject:image
|
|
forKey:cacheKey
|
|
cost:bytes];
|
|
}
|
|
}
|
|
|
|
- (UIImage *)imageForUrl:(NSString *)url
|
|
size:(CGSize)size
|
|
scale:(CGFloat)scale
|
|
resizeMode:(RCTResizeMode)resizeMode
|
|
{
|
|
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
|
|
@synchronized(_cacheStaleTimes) {
|
|
id staleTime = _cacheStaleTimes[cacheKey];
|
|
if (staleTime) {
|
|
if ([[NSDate new] compare:(NSDate *)staleTime] == NSOrderedDescending) {
|
|
// cached image has expired, clear it out to make room for others
|
|
[_cacheStaleTimes removeObjectForKey:cacheKey];
|
|
[_decodedImageCache removeObjectForKey:cacheKey];
|
|
return nil;
|
|
}
|
|
}
|
|
}
|
|
return [_decodedImageCache objectForKey:cacheKey];
|
|
}
|
|
|
|
- (void)addImageToCache:(UIImage *)image
|
|
URL:(NSString *)url
|
|
size:(CGSize)size
|
|
scale:(CGFloat)scale
|
|
resizeMode:(RCTResizeMode)resizeMode
|
|
responseDate:(NSString *)responseDate
|
|
cacheControl:(NSString *)cacheControl
|
|
{
|
|
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
|
|
BOOL shouldCache = YES;
|
|
NSDate *staleTime;
|
|
NSArray<NSString *> *components = [cacheControl componentsSeparatedByString:@","];
|
|
for (NSString *component in components) {
|
|
if ([component containsString:@"no-cache"] || [component containsString:@"no-store"] || [component hasSuffix:@"max-age=0"]) {
|
|
shouldCache = NO;
|
|
break;
|
|
} else {
|
|
NSRange range = [component rangeOfString:@"max-age="];
|
|
if (range.location != NSNotFound) {
|
|
NSInteger seconds = [[component substringFromIndex:range.location + range.length] integerValue];
|
|
NSDate *originalDate = [self dateWithHeaderString:responseDate];
|
|
staleTime = [originalDate dateByAddingTimeInterval:(NSTimeInterval)seconds];
|
|
}
|
|
}
|
|
}
|
|
if (shouldCache) {
|
|
if (staleTime) {
|
|
@synchronized(_cacheStaleTimes) {
|
|
_cacheStaleTimes[cacheKey] = staleTime;
|
|
}
|
|
}
|
|
return [self addImageToCache:image forKey:cacheKey];
|
|
}
|
|
}
|
|
|
|
- (NSDate *)dateWithHeaderString:(NSString *)headerDateString {
|
|
if (_headerDateFormatter == nil) {
|
|
_headerDateFormatter = [[NSDateFormatter alloc] init];
|
|
_headerDateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
|
|
_headerDateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
|
|
_headerDateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
|
|
}
|
|
|
|
return [_headerDateFormatter dateFromString:headerDateString];
|
|
}
|
|
|
|
@end
|