Skip to content

Commit

Permalink
check memory cache before sending network request for images, and mak…
Browse files Browse the repository at this point in the history
…e RCTImageCache respond to http headers
  • Loading branch information
esamelson committed Jul 24, 2018
1 parent 1a6666a commit 517be39
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 48 deletions.
68 changes: 60 additions & 8 deletions Libraries/Image/RCTImageCache.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,27 @@
static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB

static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale,
RCTResizeMode resizeMode, NSString *responseDate)
RCTResizeMode resizeMode)
{
return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld|%@",
imageTag, size.width, size.height, scale, (long long)resizeMode, responseDate];
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
Expand All @@ -58,6 +62,9 @@ - (void)dealloc
- (void)clearCache
{
[_decodedImageCache removeAllObjects];
@synchronized(_cacheStaleTimes) {
[_cacheStaleTimes removeAllObjects];
}
}

- (void)addImageToCache:(UIImage *)image
Expand All @@ -78,9 +85,19 @@ - (UIImage *)imageForUrl:(NSString *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
responseDate:(NSString *)responseDate
{
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate);
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];
}

Expand All @@ -90,9 +107,44 @@ - (void)addImageToCache:(UIImage *)image
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
responseDate:(NSString *)responseDate
cacheControl:(NSString *)cacheControl
{
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate);
return [self addImageToCache:image forKey:cacheKey];
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
6 changes: 3 additions & 3 deletions Libraries/Image/RCTImageLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock;
- (UIImage *)imageForUrl:(NSString *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
responseDate:(NSString *)responseDate;
resizeMode:(RCTResizeMode)resizeMode;

- (void)addImageToCache:(UIImage *)image
URL:(NSString *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
responseDate:(NSString *)responseDate;
responseDate:(NSString *)responseDate
cacheControl:(NSString *)cacheControl;

@end

Expand Down
75 changes: 38 additions & 37 deletions Libraries/Image/RCTImageLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
resizeMode:(RCTResizeMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate))completionBlock
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl))completionBlock
{
{
NSMutableURLRequest *mutableRequest = [request mutableCopy];
Expand All @@ -344,27 +344,27 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ?
[loadHandler requiresScheduling] : YES;

BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ?
[loadHandler shouldCacheLoadedImages] : YES;

__block atomic_bool cancelled = ATOMIC_VAR_INIT(NO);
// TODO: Protect this variable shared between threads.
__block dispatch_block_t cancelLoad = nil;
void (^completionHandler)(NSError *, id, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate) {
void (^completionHandler)(NSError *, id, NSString *, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate, NSString *cacheControl) {
cancelLoad = nil;

BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ?
[loadHandler shouldCacheLoadedImages] : YES;

// If we've received an image, we should try to set it synchronously,
// if it's data, do decoding on a background thread.
if (RCTIsMainQueue() && ![imageOrData isKindOfClass:[UIImage class]]) {
// Most loaders do not return on the main thread, so caller is probably not
// expecting it, and may do expensive post-processing in the callback
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (!atomic_load(&cancelled)) {
completionBlock(error, imageOrData, cacheResult, fetchDate);
completionBlock(error, imageOrData, cacheResult, fetchDate, cacheControl);
}
});
} else if (!atomic_load(&cancelled)) {
completionBlock(error, imageOrData, cacheResult, fetchDate);
completionBlock(error, imageOrData, cacheResult, fetchDate, cacheControl);
}
};

Expand All @@ -378,7 +378,7 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
progressHandler:progressHandler
partialLoadHandler:partialLoadHandler
completionHandler:^(NSError *error, UIImage *image){
completionHandler(error, image, nil);
completionHandler(error, image, nil, nil);
}];
}

Expand All @@ -402,13 +402,25 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
progressHandler:progressHandler
partialLoadHandler:partialLoadHandler
completionHandler:^(NSError *error, UIImage *image) {
completionHandler(error, image, nil);
completionHandler(error, image, nil, nil);
}];
} else {
// Use networking module to load image
cancelLoad = [strongSelf _loadURLRequest:request
progressBlock:progressHandler
completionBlock:completionHandler];
UIImage *image;
if (cacheResult) {
image = [[strongSelf imageCache] imageForUrl:request.URL.absoluteString
size:size
scale:scale
resizeMode:resizeMode];
}

if (image) {
completionHandler(nil, image, nil, nil);
} else {
// Use networking module to load image
cancelLoad = [strongSelf _loadURLRequest:request
progressBlock:progressHandler
completionBlock:completionHandler];
}
}
});

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

- (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate))completionHandler
completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate, NSString *cacheControl))completionHandler
{
// Check if networking module is available
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
Expand All @@ -449,34 +461,36 @@ - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) {
// Check for system errors
if (error) {
completionHandler(error, nil, nil);
completionHandler(error, nil, nil, nil);
return;
} else if (!response) {
completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil);
completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil, nil);
return;
} else if (!data) {
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil);
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil, nil);
return;
}

// Check for http errors
NSString *responseDate;
NSString *cacheControl;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (statusCode != 200) {
NSString *errorMessage = [NSString stringWithFormat:@"Failed to load %@", response.URL];
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: errorMessage};
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
code:statusCode
userInfo:userInfo], nil, nil);
userInfo:userInfo], nil, nil, nil);
return;
}

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

// Call handler
completionHandler(nil, data, responseDate);
completionHandler(nil, data, responseDate, cacheControl);
};

// Download image
Expand All @@ -498,7 +512,7 @@ - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
} else {
someError = RCTErrorWithMessage(@"Unknown image download error");
}
completionHandler(someError, nil, nil);
completionHandler(someError, nil, nil, nil);
[strongSelf dequeueTasks];
return;
}
Expand Down Expand Up @@ -564,7 +578,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image
};

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

// Check decoded image cache
if (cacheResult) {
UIImage *image = [[strongSelf imageCache] imageForUrl:imageURLRequest.URL.absoluteString
size:size
scale:scale
resizeMode:resizeMode
responseDate:fetchDate];
if (image) {
cancelLoad = nil;
completionBlock(nil, image);
return;
}
}

RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) {
if (cacheResult && image) {
// Store decoded image in cache
Expand All @@ -598,7 +598,8 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image
size:size
scale:scale
resizeMode:resizeMode
responseDate:fetchDate];
responseDate:fetchDate
cacheControl:cacheControl];
}

cancelLoad = nil;
Expand Down Expand Up @@ -732,7 +733,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest
block:(void(^)(NSError *error, CGSize size))callback
{
void (^completion)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) {
void (^completion)(NSError *, id, BOOL, NSString *, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl) {
CGSize size;
if ([imageOrData isKindOfClass:[NSData class]]) {
NSDictionary *meta = RCTGetImageMetadata(imageOrData);
Expand Down

0 comments on commit 517be39

Please sign in to comment.