Skip to content

Commit

Permalink
Added small decoded image cache to prevent images flashing when compo…
Browse files Browse the repository at this point in the history
…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 c8f39c3
Showing 1 changed file with 58 additions and 5 deletions.
63 changes: 58 additions & 5 deletions Libraries/Image/RCTImageLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
}
}
};
Expand All @@ -495,7 +548,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag
scale:scale
resizeMode:resizeMode
progressBlock:progressHandler
completionBlock:completionHandler] ?: ^{};
completionBlock:completionHandler];
return ^{
if (cancelLoad) {
cancelLoad();
Expand Down Expand Up @@ -551,7 +604,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data
size:size
scale:scale
resizeMode:resizeMode
completionHandler:completionHandler];
completionHandler:completionHandler] ?: ^{};
} else {

if (!_URLCacheQueue) {
Expand Down

3 comments on commit c8f39c3

@gre
Copy link
Contributor

@gre gre commented on c8f39c3 Jun 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great!

@mingkun868
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

@roman01la
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Please sign in to comment.