Skip to content
Permalink
Browse files

Added small decoded image cache to prevent images flashing when compo…

…nent is reloaded

Summary:
In RN we cache image data after loading/downloading an image, however the data we store is the compressed image data, and we decode this asynchronously each time it is displayed.

This can lead to a slight flicker when reloading image components because the decoded image is discarded and then re-decoded.

This diff adds a small (5MB) cache for decoded images so that images that are currently on screen shouldn't flicker any more if the component is reloaded.

Reviewed By: bnham

Differential Revision: D3305161

fbshipit-source-id: 9969012f576784dd6f37d9386cbced2df00c3e07
  • Loading branch information...
nicklockwood authored and Facebook Github Bot 2 committed May 23, 2016
1 parent 1e62602 commit c8f39c3d2c26963792a0e90e8e28db394b7d76f4
Showing with 58 additions and 5 deletions.
  1. +58 −5 Libraries/Image/RCTImageLoader.m
@@ -23,6 +23,35 @@
static NSString *const RCTErrorInvalidURI = @"E_INVALID_URI";
static NSString *const RCTErrorPrefetchFailure = @"E_PREFETCH_FAILURE";

static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB

static NSCache *RCTGetDecodedImageCache(void)
{
static NSCache *cache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cache = [NSCache new];
cache.totalCostLimit = 5 * 1024 * 1024; // 5MB

// Clear cache in the event of a memory warning, or if app enters background
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) {
[cache removeAllObjects];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) {
[cache removeAllObjects];
}];
});
return cache;

}

static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size,
CGFloat scale, RCTResizeMode resizeMode)
{
return [NSString stringWithFormat:@"%@|%f|%f|%f|%zd",
imageTag, size.width, size.height, scale, resizeMode];
}

@implementation UIImage (React)

- (CAKeyframeAnimation *)reactKeyframeAnimation
@@ -476,16 +505,40 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag
__block void(^cancelLoad)(void) = nil;
__weak RCTImageLoader *weakSelf = self;

void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) {
// Check decoded image cache
NSString *cacheKey = RCTCacheKeyForImage(imageTag, size, scale, resizeMode);
{
UIImage *image = [RCTGetDecodedImageCache() objectForKey:cacheKey];
if (image) {
// 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), ^{
completionBlock(nil, image);
});
return ^{};
}
}

RCTImageLoaderCompletionBlock cacheResultHandler = ^(NSError *error, UIImage *image) {
if (image) {
CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4;
if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) {
[RCTGetDecodedImageCache() setObject:image forKey:cacheKey cost:bytes];
}
}
completionBlock(error, image);
};

void (^completionHandler)(NSError *, id) = ^(NSError *error, id imageOrData) {
if (!cancelled) {
if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) {
completionBlock(error, imageOrData);
cacheResultHandler(error, imageOrData);
} else {
cancelLoad = [weakSelf decodeImageDataWithoutClipping:imageOrData
size:size
scale:scale
resizeMode:resizeMode
completionBlock:completionBlock] ?: ^{};
completionBlock:cacheResultHandler];
}
}
};
@@ -495,7 +548,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag
scale:scale
resizeMode:resizeMode
progressBlock:progressHandler
completionBlock:completionHandler] ?: ^{};
completionBlock:completionHandler];
return ^{
if (cancelLoad) {
cancelLoad();
@@ -551,7 +604,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data
size:size
scale:scale
resizeMode:resizeMode
completionHandler:completionHandler];
completionHandler:completionHandler] ?: ^{};
} else {

if (!_URLCacheQueue) {

3 comments on commit c8f39c3

@gre

This comment has been minimized.

Copy link
Contributor

replied Jun 20, 2016

great!

@mingkun868

This comment has been minimized.

Copy link

replied Jun 22, 2016

nice!

@roman01la

This comment has been minimized.

Copy link

replied Jul 6, 2016

Thanks! Is it expected that images still blinking in DEV mode?

Please sign in to comment.
You can’t perform that action at this time.