Load local assets synchronously to prevent image flicker

Summary:
This uses `[UIImage imageNamed:]` to load local assets that are bundled using `require('../image/path.png')` and makes sure it is done synchronously on the main queue to prevent images from flickering. This improves user experience a lot when using large local images and prevents icon flickers to match the behaviour of most native apps.

This adds to methods to the ImageLoader protocol, one to tell if the image loader must be executed on the url cache queue and one to tell if the result of the image loader should be cached. I then use these to make the LocalImageLoader bypass the url cache queue and avoid caching images twice.

Note that this doesn't affect debug builds since images are loaded from the packager.

I'm not sure if we want to still support async loading of local images as I'm not sure how much of a perf difference this will make. Maybe someone at fb can benchmark this see how it affects your apps but there wasn't a noticeable one in mine. Also I only enabled this for loading png and jpg im
Closes https://github.com/facebook/react-native/pull/8102

Reviewed By: bnham

Differential Revision: D3433647

Pulled By: javache

fbshipit-source-id: 37bd6aff20c0465c163db3cdbcaeaedff55f7b1f
This commit is contained in:
Janic Duplessis 2016-07-21 07:45:54 -07:00 committed by Facebook Github Bot 2
parent 7e5de707be
commit 5903949ad6
9 changed files with 101 additions and 51 deletions

View File

@ -589,6 +589,20 @@ exports.examples = [
},
platform: 'android',
},
{
title: 'Legacy local image',
description:
'Images shipped with the native bundle, but not managed ' +
'by the JS packager',
render: function() {
return (
<Image
source={require('image!hawk')}
style={styles.base}
/>
);
},
},
];
var fullImage = {uri: 'http://facebook.github.io/react/img/logo_og.png'};

View File

@ -12,9 +12,7 @@
1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */; };
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */; };
139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 139A38831C4D587C00862840 /* RCTResizeMode.m */; };
13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */; };
13EF7F0C1BC42D4E003F47DD /* RCTVirtualImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */; };
13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */; };
13EF7F7F1BC825B1003F47DD /* RCTLocalAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */; };
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; };
35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; };
354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; };
@ -44,8 +42,8 @@
134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = "<group>"; };
139A38821C4D57AD00862840 /* RCTResizeMode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTResizeMode.h; sourceTree = "<group>"; };
139A38831C4D587C00862840 /* RCTResizeMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTResizeMode.m; sourceTree = "<group>"; };
13EF7F7D1BC825B1003F47DD /* RCTXCAssetImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTXCAssetImageLoader.h; sourceTree = "<group>"; };
13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTXCAssetImageLoader.m; sourceTree = "<group>"; };
13EF7F7D1BC825B1003F47DD /* RCTLocalAssetImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTLocalAssetImageLoader.h; sourceTree = "<group>"; };
13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTLocalAssetImageLoader.m; sourceTree = "<group>"; };
143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = "<group>"; };
143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = "<group>"; };
35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = "<group>"; };
@ -75,8 +73,8 @@
EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */,
139A38821C4D57AD00862840 /* RCTResizeMode.h */,
139A38831C4D587C00862840 /* RCTResizeMode.m */,
13EF7F7D1BC825B1003F47DD /* RCTXCAssetImageLoader.h */,
13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */,
13EF7F7D1BC825B1003F47DD /* RCTLocalAssetImageLoader.h */,
13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */,
1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */,
1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */,
354631661B69857700AA0B86 /* RCTImageEditingManager.h */,
@ -169,7 +167,7 @@
139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */,
1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */,
EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */,
13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */,
13EF7F7F1BC825B1003F47DD /* RCTLocalAssetImageLoader.m in Sources */,
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -141,6 +141,22 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock;
*/
- (float)loaderPriority;
/**
* If the loader must be called on the serial url cache queue, and whether the completion
* block should be dispatched off the main thread. If this is NO, the loader will be
* called from the main queue. Defaults to YES.
*
* Use with care: disabling scheduling will reduce RCTImageLoader's ability to throttle
* network requests.
*/
- (BOOL)requiresScheduling;
/**
* If images loaded by the loader should be cached in the decoded image cache.
* Defaults to YES.
*/
- (BOOL)shouldCacheLoadedImages;
@end
/**

View File

@ -287,31 +287,58 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
* path taken. This is useful if you want to skip decoding, e.g. when preloading
* the image, or retrieving metadata.
*/
- (RCTImageLoaderCancellationBlock)loadImageOrDataWithURLRequest:(NSURLRequest *)imageURLRequest
- (RCTImageLoaderCancellationBlock)loadImageOrDataWithURLRequest:(NSURLRequest *)request
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
completionBlock:(void (^)(NSError *error, id imageOrData))completionBlock
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult))completionBlock
{
__block volatile uint32_t cancelled = 0;
__block dispatch_block_t cancelLoad = nil;
__weak RCTImageLoader *weakSelf = self;
// Add missing png extension
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]];
request = mutableRequest;
}
// Find suitable image URL loader
id<RCTImageURLLoader> loadHandler = [self imageURLLoaderForURL:request.URL];
BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ?
[loadHandler requiresScheduling] : YES;
void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) {
if (RCTIsMainQueue()) {
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 (!cancelled) {
completionBlock(error, imageOrData);
completionBlock(error, imageOrData, cacheResult);
}
});
} else if (!cancelled) {
completionBlock(error, imageOrData);
completionBlock(error, imageOrData, cacheResult);
}
};
// If the loader doesn't require scheduling we call it directly on
// the main queue.
if (loadHandler && !requiresScheduling) {
return [loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
progressHandler:progressHandler
completionHandler:completionHandler];
}
// All access to URL cache must be serialized
if (!_URLCacheQueue) {
[self setUp];
@ -323,25 +350,14 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
return;
}
// Use a local variable so we can reassign it in this block
NSURLRequest *request = imageURLRequest;
// Add missing png extension
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]];
request = mutableRequest;
}
// Find suitable image URL loader
id<RCTImageURLLoader> loadHandler = [strongSelf imageURLLoaderForURL:request.URL];
if (loadHandler) {
cancelLoad = [loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
progressHandler:progressHandler
completionHandler:completionHandler] ?: ^{};
completionHandler:completionHandler];
} else {
// Use networking module to load image
cancelLoad = [strongSelf _loadURLRequest:request
@ -530,17 +546,18 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
completionBlock(error, image);
};
void (^completionHandler)(NSError *, id) = ^(NSError *error, id imageOrData) {
void (^completionHandler)(NSError *, id, BOOL) = ^(NSError *error, id imageOrData, BOOL cacheResult) {
if (!cancelled) {
RCTImageLoaderCompletionBlock resultHandler = cacheResult ? cacheResultHandler : completionBlock;
if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) {
cacheResultHandler(error, imageOrData);
resultHandler(error, imageOrData);
} else {
cancelLoad = [weakSelf decodeImageData:imageOrData
size:size
scale:scale
clipped:clipped
resizeMode:resizeMode
completionBlock:cacheResultHandler];
completionBlock:resultHandler];
}
}
};
@ -678,7 +695,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
scale:1
resizeMode:RCTResizeModeStretch
progressBlock:nil
completionBlock:^(NSError *error, id imageOrData) {
completionBlock:^(NSError *error, id imageOrData, __unused BOOL cacheResult) {
CGSize size;
if ([imageOrData isKindOfClass:[NSData class]]) {
NSDictionary *meta = RCTGetImageMetadata(imageOrData);

View File

@ -9,6 +9,6 @@
#import "RCTImageLoader.h"
@interface RCTXCAssetImageLoader : NSObject <RCTImageURLLoader>
@interface RCTLocalAssetImageLoader : NSObject <RCTImageURLLoader>
@end

View File

@ -7,19 +7,33 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTXCAssetImageLoader.h"
#import "RCTLocalAssetImageLoader.h"
#import <libkern/OSAtomic.h>
#import "RCTUtils.h"
@implementation RCTXCAssetImageLoader
@implementation RCTLocalAssetImageLoader
RCT_EXPORT_MODULE()
- (BOOL)canLoadImageURL:(NSURL *)requestURL
{
return RCTIsXCAssetURL(requestURL);
return RCTIsLocalAssetURL(requestURL);
}
- (BOOL)requiresScheduling
{
// Don't schedule this loader on the URL queue so we can load the
// local assets synchronously to avoid flickers.
return NO;
}
- (BOOL)shouldCacheLoadedImages
{
// UIImage imageNamed handles the caching automatically so we don't want
// to add it to the image cache.
return NO;
}
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
@ -30,11 +44,11 @@ RCT_EXPORT_MODULE()
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
{
__block volatile uint32_t cancelled = 0;
dispatch_async(dispatch_get_main_queue(), ^{
RCTExecuteOnMainQueue(^{
if (cancelled) {
return;
}
NSString *imageName = RCTBundlePathForURL(imageURL);
UIImage *image = [UIImage imageNamed:imageName];
if (image) {

View File

@ -30,7 +30,7 @@ RCT_EXPORT_MODULE()
{
return
[request.URL.scheme caseInsensitiveCompare:@"file"] == NSOrderedSame
&& !RCTIsXCAssetURL(request.URL);
&& !RCTIsLocalAssetURL(request.URL);
}
- (NSOperation *)sendRequest:(NSURLRequest *)request

View File

@ -120,8 +120,8 @@ RCT_EXTERN NSData *__nullable RCTGzipData(NSData *__nullable data, float level);
// (or nil, if the URL does not specify a path within the main bundle)
RCT_EXTERN NSString *__nullable RCTBundlePathForURL(NSURL *__nullable URL);
// Determines if a given image URL actually refers to an XCAsset
RCT_EXTERN BOOL RCTIsXCAssetURL(NSURL *__nullable imageURL);
// Determines if a given image URL refers to a local image
RCT_EXTERN BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL);
// Creates a new, unique temporary file path with the specified extension
RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *__nullable extension, NSError **error);

View File

@ -612,24 +612,15 @@ NSString *__nullable RCTBundlePathForURL(NSURL *__nullable URL)
return path;
}
BOOL RCTIsXCAssetURL(NSURL *__nullable imageURL)
BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL)
{
NSString *name = RCTBundlePathForURL(imageURL);
if (name.pathComponents.count != 1) {
// URL is invalid, or is a file path, not an XCAsset identifier
if (!name) {
return NO;
}
NSString *extension = [name pathExtension];
if (extension.length && ![extension isEqualToString:@"png"]) {
// Not a png
return NO;
}
extension = extension.length ? nil : @"png";
if ([[NSBundle mainBundle] pathForResource:name ofType:extension]) {
// File actually exists in bundle, so is not an XCAsset
return NO;
}
return YES;
return [extension isEqualToString:@"png"] || [extension isEqualToString:@"jpg"];
}
RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *extension, NSError **error)