react-native-macos/Libraries/Image/RCTImageLoader.mm

1083 строки
40 KiB
Plaintext
Исходник Обычный вид История

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
2015-03-10 03:08:01 +03:00
#import <objc/runtime.h>
#import <atomic>
2015-03-10 03:08:01 +03:00
#import <ImageIO/ImageIO.h>
2015-03-10 03:08:01 +03:00
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConvert.h>
#import <React/RCTDefines.h>
#import <React/RCTImageCache.h>
#import <React/RCTImageLoader.h>
#import <React/RCTImageLoaderWithAttributionProtocol.h>
#import <React/RCTImageUtils.h>
#import <React/RCTLog.h>
#import <React/RCTNetworking.h>
#import <React/RCTUtils.h>
#import "RCTImagePlugins.h"
2015-03-10 03:08:01 +03:00
using namespace facebook::react;
static NSInteger RCTImageBytesForImage(UIImage *image)
{
NSInteger singleImageBytes = image.size.width * image.size.height * image.scale * image.scale * 4;
return image.images ? image.images.count * singleImageBytes : singleImageBytes;
}
@interface RCTImageLoader() <NativeImageLoaderIOSSpec, RCTImageLoaderWithAttributionProtocol>
@end
@implementation UIImage (React)
- (NSInteger)reactDecodedImageBytes
{
NSNumber *imageBytes = objc_getAssociatedObject(self, _cmd);
if (!imageBytes) {
imageBytes = @(RCTImageBytesForImage(self));
}
return [imageBytes integerValue];
}
- (void)setReactDecodedImageBytes:(NSInteger)bytes
{
objc_setAssociatedObject(self, @selector(reactDecodedImageBytes), @(bytes), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
2015-03-10 03:08:01 +03:00
@implementation RCTImageLoader
{
NSArray<id<RCTImageURLLoader>> * (^_loadersProvider)(void);
NSArray<id<RCTImageDataDecoder>> * (^_decodersProvider)(void);
NSArray<id<RCTImageURLLoader>> *_loaders;
NSArray<id<RCTImageDataDecoder>> *_decoders;
NSOperationQueue *_imageDecodeQueue;
dispatch_queue_t _URLRequestQueue;
id<RCTImageCache> _imageCache;
NSMutableArray *_pendingTasks;
NSInteger _activeTasks;
NSMutableArray *_pendingDecodes;
NSInteger _scheduledDecodes;
NSUInteger _activeBytes;
__weak id<RCTImageRedirectProtocol> _redirectDelegate;
}
2015-03-10 03:08:01 +03:00
@synthesize bridge = _bridge;
@synthesize maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks;
@synthesize maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks;
@synthesize maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes;
RCT_EXPORT_MODULE()
- (instancetype)init
{
return [self initWithRedirectDelegate:nil];
}
fix(RCTImageLoader): Adds `requiresMainQueueSetup` to fix v0.52 Summary: <!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. You can learn more about contributing to React Native here: http://facebook.github.io/react-native/docs/contributing.html Happy contributing! --> In react-native v0.52 this warning shows: ``` "RCTImageLoader requires main queue setup since it overrides `init` but doesn't implement `requiresMainQueueSetup`". ``` This removes the warning by implementing `requiresMainQueueSetup` on RCTImageLoader * Create new react-native project * Enable Remote JS Debugging * Should see no warning regarding RCTImageLoader requiring main queue setup <!-- Help reviewers and the release process by writing your own release notes **INTERNAL and MINOR tagged notes will not be included in the next version's final release notes.** CATEGORY [----------] TYPE [ CLI ] [-------------] LOCATION [ DOCS ] [ BREAKING ] [-------------] [ GENERAL ] [ BUGFIX ] [-{Component}-] [ INTERNAL ] [ ENHANCEMENT ] [ {File} ] [ IOS ] [ FEATURE ] [ {Directory} ] |-----------| [ ANDROID ] [ MINOR ] [ {Framework} ] - | {Message} | [----------] [-------------] [-------------] |-----------| [CATEGORY] [TYPE] [LOCATION] - MESSAGE EXAMPLES: [IOS] [BREAKING] [FlatList] - Change a thing that breaks other things [ANDROID] [BUGFIX] [TextInput] - Did a thing to TextInput [CLI] [FEATURE] [local-cli/info/info.js] - CLI easier to do things with [DOCS] [BUGFIX] [GettingStarted.md] - Accidentally a thing/word [GENERAL] [ENHANCEMENT] [Yoga] - Added new yoga thing/position [INTERNAL] [FEATURE] [./scripts] - Added thing to script that nobody will see --> [IOS] [BUGFIX] [Libraries/Image/RCTImageLoader.m] - Implements `requiresMainQueueSetup` Closes https://github.com/facebook/react-native/pull/17679 Reviewed By: shergin Differential Revision: D7159601 Pulled By: fkgozali fbshipit-source-id: e17bae67f4005d2c9ddd0d3701506521f3cac152
2018-04-04 02:13:58 +03:00
+ (BOOL)requiresMainQueueSetup
{
return NO;
fix(RCTImageLoader): Adds `requiresMainQueueSetup` to fix v0.52 Summary: <!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. You can learn more about contributing to React Native here: http://facebook.github.io/react-native/docs/contributing.html Happy contributing! --> In react-native v0.52 this warning shows: ``` "RCTImageLoader requires main queue setup since it overrides `init` but doesn't implement `requiresMainQueueSetup`". ``` This removes the warning by implementing `requiresMainQueueSetup` on RCTImageLoader * Create new react-native project * Enable Remote JS Debugging * Should see no warning regarding RCTImageLoader requiring main queue setup <!-- Help reviewers and the release process by writing your own release notes **INTERNAL and MINOR tagged notes will not be included in the next version's final release notes.** CATEGORY [----------] TYPE [ CLI ] [-------------] LOCATION [ DOCS ] [ BREAKING ] [-------------] [ GENERAL ] [ BUGFIX ] [-{Component}-] [ INTERNAL ] [ ENHANCEMENT ] [ {File} ] [ IOS ] [ FEATURE ] [ {Directory} ] |-----------| [ ANDROID ] [ MINOR ] [ {Framework} ] - | {Message} | [----------] [-------------] [-------------] |-----------| [CATEGORY] [TYPE] [LOCATION] - MESSAGE EXAMPLES: [IOS] [BREAKING] [FlatList] - Change a thing that breaks other things [ANDROID] [BUGFIX] [TextInput] - Did a thing to TextInput [CLI] [FEATURE] [local-cli/info/info.js] - CLI easier to do things with [DOCS] [BUGFIX] [GettingStarted.md] - Accidentally a thing/word [GENERAL] [ENHANCEMENT] [Yoga] - Added new yoga thing/position [INTERNAL] [FEATURE] [./scripts] - Added thing to script that nobody will see --> [IOS] [BUGFIX] [Libraries/Image/RCTImageLoader.m] - Implements `requiresMainQueueSetup` Closes https://github.com/facebook/react-native/pull/17679 Reviewed By: shergin Differential Revision: D7159601 Pulled By: fkgozali fbshipit-source-id: e17bae67f4005d2c9ddd0d3701506521f3cac152
2018-04-04 02:13:58 +03:00
}
- (instancetype)initWithRedirectDelegate:(id<RCTImageRedirectProtocol>)redirectDelegate
{
if (self = [super init]) {
_redirectDelegate = redirectDelegate;
}
return self;
}
- (instancetype)initWithRedirectDelegate:(id<RCTImageRedirectProtocol>)redirectDelegate
loadersProvider:(NSArray<id<RCTImageURLLoader>> * (^)(void))getLoaders
decodersProvider:(NSArray<id<RCTImageDataDecoder>> * (^)(void))getHandlers
{
if (self = [self initWithRedirectDelegate:redirectDelegate]) {
_loadersProvider = getLoaders;
_decodersProvider = getHandlers;
}
return self;
}
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 14:09:00 +03:00
- (void)setUp
{
// Set defaults
_maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks ?: 4;
_maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks ?: 2;
_maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 * 1024; // 30MB
_URLRequestQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLRequestQueue", DISPATCH_QUEUE_SERIAL);
}
- (float)handlerPriority
{
return 2;
}
- (id<RCTImageCache>)imageCache
{
if (!_imageCache) {
//set up with default cache
_imageCache = [RCTImageCache new];
}
return _imageCache;
}
- (void)setImageCache:(id<RCTImageCache>)cache
{
if (_imageCache) {
RCTLogWarn(@"RCTImageCache was already set and has now been overridden.");
}
_imageCache = cache;
}
- (id<RCTImageURLLoader>)imageURLLoaderForURL:(NSURL *)URL
{
if (!_maxConcurrentLoadingTasks) {
[self setUp];
}
if (!_loaders) {
// Get loaders, sorted in reverse priority order (highest priority first)
RCTAssert(_bridge, @"Bridge not set");
if (_loadersProvider) {
_loaders = _loadersProvider();
} else {
_loaders = [_bridge modulesConformingToProtocol:@protocol(RCTImageURLLoader)];
}
_loaders = [_loaders sortedArrayUsingComparator:^NSComparisonResult(id<RCTImageURLLoader> a, id<RCTImageURLLoader> b) {
float priorityA = [a respondsToSelector:@selector(loaderPriority)] ? [a loaderPriority] : 0;
float priorityB = [b respondsToSelector:@selector(loaderPriority)] ? [b loaderPriority] : 0;
if (priorityA > priorityB) {
return NSOrderedAscending;
} else if (priorityA < priorityB) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
}];
}
if (RCT_DEBUG) {
// Check for handler conflicts
float previousPriority = 0;
id<RCTImageURLLoader> previousLoader = nil;
for (id<RCTImageURLLoader> loader in _loaders) {
float priority = [loader respondsToSelector:@selector(loaderPriority)] ? [loader loaderPriority] : 0;
if (previousLoader && priority < previousPriority) {
return previousLoader;
}
if ([loader canLoadImageURL:URL]) {
if (previousLoader) {
if (priority == previousPriority) {
RCTLogError(@"The RCTImageURLLoaders %@ and %@ both reported that"
" they can load the URL %@, and have equal priority"
" (%g). This could result in non-deterministic behavior.",
loader, previousLoader, URL, priority);
}
} else {
previousLoader = loader;
previousPriority = priority;
}
}
}
return previousLoader;
}
// Normal code path
for (id<RCTImageURLLoader> loader in _loaders) {
if ([loader canLoadImageURL:URL]) {
return loader;
}
}
return nil;
}
- (id<RCTImageDataDecoder>)imageDataDecoderForData:(NSData *)data
{
if (!_maxConcurrentLoadingTasks) {
[self setUp];
}
if (!_decoders) {
// Get decoders, sorted in reverse priority order (highest priority first)
RCTAssert(_bridge, @"Bridge not set");
if (_decodersProvider) {
_decoders = _decodersProvider();
} else {
_decoders = [_bridge modulesConformingToProtocol:@protocol(RCTImageDataDecoder)];
}
_decoders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageDataDecoder)] sortedArrayUsingComparator:^NSComparisonResult(id<RCTImageDataDecoder> a, id<RCTImageDataDecoder> b) {
float priorityA = [a respondsToSelector:@selector(decoderPriority)] ? [a decoderPriority] : 0;
float priorityB = [b respondsToSelector:@selector(decoderPriority)] ? [b decoderPriority] : 0;
if (priorityA > priorityB) {
return NSOrderedAscending;
} else if (priorityA < priorityB) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
}];
}
if (RCT_DEBUG) {
// Check for handler conflicts
float previousPriority = 0;
id<RCTImageDataDecoder> previousDecoder = nil;
for (id<RCTImageDataDecoder> decoder in _decoders) {
float priority = [decoder respondsToSelector:@selector(decoderPriority)] ? [decoder decoderPriority] : 0;
if (previousDecoder && priority < previousPriority) {
return previousDecoder;
}
if ([decoder canDecodeImageData:data]) {
if (previousDecoder) {
if (priority == previousPriority) {
RCTLogError(@"The RCTImageDataDecoders %@ and %@ both reported that"
" they can decode the data <NSData %p; %tu bytes>, and"
" have equal priority (%g). This could result in"
" non-deterministic behavior.",
decoder, previousDecoder, data, data.length, priority);
}
} else {
previousDecoder = decoder;
previousPriority = priority;
}
}
}
return previousDecoder;
}
// Normal code path
for (id<RCTImageDataDecoder> decoder in _decoders) {
if ([decoder canDecodeImageData:data]) {
return decoder;
}
}
return nil;
}
static UIImage *RCTResizeImageIfNeeded(UIImage *image,
CGSize size,
CGFloat scale,
RCTResizeMode resizeMode)
{
if (CGSizeEqualToSize(size, CGSizeZero) ||
CGSizeEqualToSize(image.size, CGSizeZero) ||
CGSizeEqualToSize(image.size, size)) {
return image;
}
CGRect targetSize = RCTTargetRect(image.size, size, scale, resizeMode);
CGAffineTransform transform = RCTTransformFromTargetRect(image.size, targetSize);
image = RCTTransformImage(image, size, scale, transform);
return image;
}
- (RCTImageLoaderCancellationBlock) loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
callback:(RCTImageLoaderCompletionBlock)callback
{
return [self loadImageWithURLRequest:imageURLRequest
size:CGSizeZero
scale:1
clipped:YES
resizeMode:RCTResizeModeStretch
attribution:{}
progressBlock:nil
partialLoadBlock:nil
completionBlock:callback];
}
- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
size:(CGSize)size
scale:(CGFloat)scale
clipped:(BOOL)clipped
resizeMode:(RCTResizeMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
return [self loadImageWithURLRequest:imageURLRequest
size:size
scale:scale
clipped:clipped
resizeMode:resizeMode
attribution:{}
progressBlock:progressBlock
partialLoadBlock:partialLoadBlock
completionBlock:completionBlock];
}
- (void)dequeueTasks
{
dispatch_async(_URLRequestQueue, ^{
// Remove completed tasks
NSMutableArray *tasksToRemove = nil;
for (RCTNetworkTask *task in self->_pendingTasks.reverseObjectEnumerator) {
switch (task.status) {
case RCTNetworkTaskFinished:
if (!tasksToRemove) {
tasksToRemove = [NSMutableArray new];
}
[tasksToRemove addObject:task];
self->_activeTasks--;
break;
case RCTNetworkTaskPending:
break;
case RCTNetworkTaskInProgress:
// Check task isn't "stuck"
if (task.requestToken == nil) {
RCTLogWarn(@"Task orphaned for request %@", task.request);
if (!tasksToRemove) {
tasksToRemove = [NSMutableArray new];
}
[tasksToRemove addObject:task];
self->_activeTasks--;
[task cancel];
}
break;
}
}
if (tasksToRemove) {
[self->_pendingTasks removeObjectsInArray:tasksToRemove];
}
// Start queued decode
NSInteger activeDecodes = self->_scheduledDecodes - self->_pendingDecodes.count;
while (activeDecodes == 0 || (self->_activeBytes <= self->_maxConcurrentDecodingBytes &&
activeDecodes <= self->_maxConcurrentDecodingTasks)) {
dispatch_block_t decodeBlock = self->_pendingDecodes.firstObject;
if (decodeBlock) {
[self->_pendingDecodes removeObjectAtIndex:0];
decodeBlock();
} else {
break;
}
}
// Start queued tasks
for (RCTNetworkTask *task in self->_pendingTasks) {
if (MAX(self->_activeTasks, self->_scheduledDecodes) >= self->_maxConcurrentLoadingTasks) {
break;
}
if (task.status == RCTNetworkTaskPending) {
[task start];
self->_activeTasks++;
}
}
});
}
/**
* This returns either an image, or raw image data, depending on the loading
* path taken. This is useful if you want to skip decoding, e.g. when preloading
* the image, or retrieving metadata.
*/
- (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest *)request
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
attribution:(const ImageURLLoaderAttribution &)attribution
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response))completionBlock
2015-03-10 03:08:01 +03:00
{
{
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[NSURLProtocol setProperty:@"RCTImageLoader"
forKey:@"trackingName"
inRequest:mutableRequest];
// Add missing png extension
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
mutableRequest.URL = [request.URL URLByAppendingPathExtension:@"png"];
}
if (_redirectDelegate != nil) {
mutableRequest.URL = [_redirectDelegate redirectAssetsURL:mutableRequest.URL];
}
request = mutableRequest;
}
// Create a copy here so the value is retained when accessed in the blocks below.
ImageURLLoaderAttribution attributionCopy(attribution);
// Find suitable image URL loader
id<RCTImageURLLoader> loadHandler = [self imageURLLoaderForURL:request.URL];
BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ?
[loadHandler requiresScheduling] : YES;
BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ?
[loadHandler shouldCacheLoadedImages] : YES;
auto cancelled = std::make_shared<std::atomic<int>>(0);
__block dispatch_block_t cancelLoad = nil;
__block NSLock *cancelLoadLock = [NSLock new];
void (^completionHandler)(NSError *, id, NSURLResponse *) = ^(NSError *error, id imageOrData, NSURLResponse *response) {
[cancelLoadLock lock];
cancelLoad = nil;
[cancelLoadLock unlock];
// 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 (!std::atomic_load(cancelled.get())) {
completionBlock(error, imageOrData, cacheResult, response);
}
});
} else if (!std::atomic_load(cancelled.get())) {
completionBlock(error, imageOrData, cacheResult, response);
}
};
// If the loader doesn't require scheduling we call it directly on
// the main queue.
if (loadHandler && !requiresScheduling) {
if ([loadHandler conformsToProtocol:@protocol(RCTImageURLLoaderWithAttribution)]) {
return [(id<RCTImageURLLoaderWithAttribution>)loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
attribution:attributionCopy
progressHandler:progressHandler
partialLoadHandler:partialLoadHandler
completionHandler:^(NSError *error, UIImage *image) {
completionHandler(error, image, nil);
}];
}
return [loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
progressHandler:progressHandler
partialLoadHandler:partialLoadHandler
completionHandler:^(NSError *error, UIImage *image) {
completionHandler(error, image, nil);
}];
}
// All access to URL cache must be serialized
if (!_URLRequestQueue) {
[self setUp];
}
__weak RCTImageLoader *weakSelf = self;
dispatch_async(_URLRequestQueue, ^{
__typeof(self) strongSelf = weakSelf;
if (atomic_load(cancelled.get()) || !strongSelf) {
return;
}
if (loadHandler) {
dispatch_block_t cancelLoadLocal;
if ([loadHandler conformsToProtocol:@protocol(RCTImageURLLoaderWithAttribution)]) {
cancelLoadLocal = [(id<RCTImageURLLoaderWithAttribution>)loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
attribution:attributionCopy
progressHandler:progressHandler
partialLoadHandler:partialLoadHandler
completionHandler:^(NSError *error, UIImage *image) {
completionHandler(error, image, nil);
}];
} else {
cancelLoadLocal = [loadHandler loadImageForURL:request.URL
size:size
scale:scale
resizeMode:resizeMode
progressHandler:progressHandler
partialLoadHandler:partialLoadHandler
completionHandler:^(NSError *error, UIImage *image) {
completionHandler(error, image, nil);
}];
}
[cancelLoadLock lock];
cancelLoad = cancelLoadLocal;
[cancelLoadLock unlock];
} else {
UIImage *image;
if (cacheResult) {
image = [[strongSelf imageCache] imageForUrl:request.URL.absoluteString
size:size
scale:scale
resizeMode:resizeMode];
}
if (image) {
completionHandler(nil, image, nil);
} else {
// Use networking module to load image
dispatch_block_t cancelLoadLocal = [strongSelf _loadURLRequest:request
progressBlock:progressHandler
completionBlock:completionHandler];
[cancelLoadLock lock];
cancelLoad = cancelLoadLocal;
[cancelLoadLock unlock];
}
}
});
return ^{
BOOL alreadyCancelled = atomic_fetch_or(cancelled.get(), 1);
if (alreadyCancelled) {
return;
}
[cancelLoadLock lock];
dispatch_block_t cancelLoadLocal = cancelLoad;
cancelLoad = nil;
[cancelLoadLock unlock];
if (cancelLoadLocal) {
cancelLoadLocal();
}
};
}
- (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
completionBlock:(void (^)(NSError *error, id imageOrData, NSURLResponse *response))completionHandler
{
// Check if networking module is available
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
RCTLogError(@"No suitable image URL loader found for %@. You may need to "
" import the RCTNetwork library in order to load images.",
request.URL.absoluteString);
return NULL;
}
RCTNetworking *networking = [_bridge networking];
// Check if networking module can load image
if (RCT_DEBUG && ![networking canHandleRequest:request]) {
RCTLogError(@"No suitable image URL loader found for %@", request.URL.absoluteString);
return NULL;
}
// Use networking module to load image
RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) {
// Check for system errors
if (error) {
completionHandler(error, nil, response);
return;
} else if (!response) {
completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, response);
return;
} else if (!data) {
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, response);
return;
}
// Check for http errors
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, response);
return;
}
}
// Call handler
completionHandler(nil, data, response);
};
// Download image
__weak __typeof(self) weakSelf = self;
__block RCTNetworkTask *task =
[networking networkTaskWithRequest:request
completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
__typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (error || !response || !data) {
NSError *someError = nil;
if (error) {
someError = error;
} else if (!response) {
someError = RCTErrorWithMessage(@"Response metadata error");
} else {
someError = RCTErrorWithMessage(@"Unknown image download error");
}
completionHandler(someError, nil, response);
[strongSelf dequeueTasks];
return;
}
dispatch_async(strongSelf->_URLRequestQueue, ^{
// Process image data
processResponse(response, data, nil);
// Prepare for next task
[strongSelf dequeueTasks];
});
}];
task.downloadProgressBlock = ^(int64_t progress, int64_t total) {
if (progressHandler) {
progressHandler(progress, total);
}
};
if (task) {
if (!_pendingTasks) {
_pendingTasks = [NSMutableArray new];
}
[_pendingTasks addObject:task];
[self dequeueTasks];
}
return ^{
__typeof(self) strongSelf = weakSelf;
if (!strongSelf || !task) {
return;
}
dispatch_async(strongSelf->_URLRequestQueue, ^{
[task cancel];
task = nil;
});
[strongSelf dequeueTasks];
};
2015-09-02 18:25:10 +03:00
}
- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
size:(CGSize)size
scale:(CGFloat)scale
clipped:(BOOL)clipped
resizeMode:(RCTResizeMode)resizeMode
attribution:(const ImageURLLoaderAttribution &)attribution
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
auto cancelled = std::make_shared<std::atomic<int>>(0);
__block dispatch_block_t cancelLoad = nil;
__block NSLock *cancelLoadLock = [NSLock new];
dispatch_block_t cancellationBlock = ^{
BOOL alreadyCancelled = atomic_fetch_or(cancelled.get(), 1);
if (alreadyCancelled) {
return;
}
[cancelLoadLock lock];
dispatch_block_t cancelLoadLocal = cancelLoad;
cancelLoad = nil;
[cancelLoadLock unlock];
if (cancelLoadLocal) {
cancelLoadLocal();
}
};
__weak RCTImageLoader *weakSelf = self;
void (^completionHandler)(NSError *, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response) {
__typeof(self) strongSelf = weakSelf;
if (std::atomic_load(cancelled.get()) || !strongSelf) {
return;
}
if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) {
[cancelLoadLock lock];
cancelLoad = nil;
[cancelLoadLock unlock];
completionBlock(error, imageOrData);
return;
}
RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) {
if (cacheResult && image) {
// Store decoded image in cache
[[strongSelf imageCache] addImageToCache:image
URL:imageURLRequest.URL.absoluteString
size:size
scale:scale
resizeMode:resizeMode
response:response];
}
[cancelLoadLock lock];
cancelLoad = nil;
[cancelLoadLock unlock];
completionBlock(error_, image);
};
dispatch_block_t cancelLoadLocal = [strongSelf decodeImageData:imageOrData
size:size
scale:scale
clipped:clipped
resizeMode:resizeMode
completionBlock:decodeCompletionHandler];
[cancelLoadLock lock];
cancelLoad = cancelLoadLocal;
[cancelLoadLock unlock];
};
cancelLoad = [self _loadImageOrDataWithURLRequest:imageURLRequest
size:size
scale:scale
resizeMode:resizeMode
attribution:attribution
progressBlock:progressBlock
partialLoadBlock:partialLoadBlock
completionBlock:completionHandler];
return cancellationBlock;
}
2015-09-02 18:25:10 +03:00
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
size:(CGSize)size
scale:(CGFloat)scale
clipped:(BOOL)clipped
resizeMode:(RCTResizeMode)resizeMode
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
2015-09-02 18:25:10 +03:00
{
if (data.length == 0) {
completionBlock(RCTErrorWithMessage(@"No image data"), nil);
return ^{};
}
auto cancelled = std::make_shared<std::atomic<int>>(0);
void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) {
if (RCTIsMainQueue()) {
// 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 (!std::atomic_load(cancelled.get())) {
completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image);
}
});
} else if (!std::atomic_load(cancelled.get())) {
completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image);
}
};
id<RCTImageDataDecoder> imageDecoder = [self imageDataDecoderForData:data];
if (imageDecoder) {
return [imageDecoder decodeImageData:data
size:size
scale:scale
resizeMode:resizeMode
completionHandler:completionHandler] ?: ^{};
} else {
dispatch_block_t decodeBlock = ^{
// Calculate the size, in bytes, that the decompressed image will require
NSInteger decodedImageBytes = (size.width * scale) * (size.height * scale) * 4;
// Mark these bytes as in-use
self->_activeBytes += decodedImageBytes;
// Do actual decompression on a concurrent background queue
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (!std::atomic_load(cancelled.get())) {
// Decompress the image data (this may be CPU and memory intensive)
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
#if RCT_DEV
CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale);
CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale());
if (imagePixelSize.width * imagePixelSize.height >
screenPixelSize.width * screenPixelSize.height) {
RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger "
"than the screen size %@", NSStringFromCGSize(imagePixelSize),
NSStringFromCGSize(screenPixelSize));
}
#endif
if (image) {
completionHandler(nil, image);
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
NSError *finalError = RCTErrorWithMessage(errorMessage);
completionHandler(finalError, nil);
}
}
// We're no longer retaining the uncompressed data, so now we'll mark
// the decoding as complete so that the loading task queue can resume.
dispatch_async(self->_URLRequestQueue, ^{
self->_scheduledDecodes--;
self->_activeBytes -= decodedImageBytes;
[self dequeueTasks];
});
});
};
if (!_URLRequestQueue) {
[self setUp];
}
dispatch_async(_URLRequestQueue, ^{
// The decode operation retains the compressed image data until it's
// complete, so we'll mark it as having started, in order to block
// further image loads from happening until we're done with the data.
self->_scheduledDecodes++;
if (!self->_pendingDecodes) {
self->_pendingDecodes = [NSMutableArray new];
}
NSInteger activeDecodes = self->_scheduledDecodes - self->_pendingDecodes.count - 1;
if (activeDecodes == 0 || (self->_activeBytes <= self->_maxConcurrentDecodingBytes &&
activeDecodes <= self->_maxConcurrentDecodingTasks)) {
decodeBlock();
} else {
[self->_pendingDecodes addObject:decodeBlock];
}
});
return ^{
std::atomic_store(cancelled.get(), 1);
};
}
2015-03-10 03:08:01 +03:00
}
- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest
block:(void(^)(NSError *error, CGSize size))callback
{
void (^completion)(NSError *, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response) {
CGSize size;
if ([imageOrData isKindOfClass:[NSData class]]) {
NSDictionary *meta = RCTGetImageMetadata(imageOrData);
NSInteger imageOrientation = [meta[(id)kCGImagePropertyOrientation] integerValue];
switch (imageOrientation) {
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationRight:
case kCGImagePropertyOrientationLeftMirrored:
case kCGImagePropertyOrientationRightMirrored:
// swap width and height
size = (CGSize){
[meta[(id)kCGImagePropertyPixelHeight] floatValue],
[meta[(id)kCGImagePropertyPixelWidth] floatValue],
};
break;
case kCGImagePropertyOrientationUp:
case kCGImagePropertyOrientationDown:
case kCGImagePropertyOrientationUpMirrored:
case kCGImagePropertyOrientationDownMirrored:
default:
size = (CGSize){
[meta[(id)kCGImagePropertyPixelWidth] floatValue],
[meta[(id)kCGImagePropertyPixelHeight] floatValue],
};
break;
}
} else {
UIImage *image = imageOrData;
size = (CGSize){
image.size.width * image.scale,
image.size.height * image.scale,
};
}
callback(error, size);
};
return [self _loadImageOrDataWithURLRequest:imageURLRequest
size:CGSizeZero
scale:1
resizeMode:RCTResizeModeStretch
attribution:{}
progressBlock:NULL
partialLoadBlock:NULL
completionBlock:completion];
}
- (NSDictionary *)getImageCacheStatus:(NSArray *)requests
{
NSMutableDictionary *results = [NSMutableDictionary dictionary];
for (id request in requests) {
NSURLRequest *urlRequest = [RCTConvert NSURLRequest:request];
if (urlRequest) {
NSCachedURLResponse *cachedResponse = [NSURLCache.sharedURLCache cachedResponseForRequest:urlRequest];
if (cachedResponse) {
if (cachedResponse.storagePolicy == NSURLCacheStorageAllowedInMemoryOnly) {
results[urlRequest.URL.absoluteString] = @"memory";
} else if (NSURLCache.sharedURLCache.currentMemoryUsage == 0) {
// We can't check whether the file is cached on disk or memory.
// However, if currentMemoryUsage is disabled, it must be read from disk.
results[urlRequest.URL.absoluteString] = @"disk";
} else {
results[urlRequest.URL.absoluteString] = @"disk/memory";
}
}
}
}
return results;
}
2015-09-02 18:25:10 +03:00
#pragma mark - RCTURLRequestHandler
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
NSURL *requestURL = request.URL;
// If the data being loaded is a video, return NO
// Even better may be to implement this on the RCTImageURLLoader that would try to load it,
// but we'd have to run the logic both in RCTPhotoLibraryImageLoader and
// RCTAssetsLibraryRequestHandler. Once we drop iOS7 though, we'd drop
// RCTAssetsLibraryRequestHandler and can move it there.
static NSRegularExpression *videoRegex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error = nil;
videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error) {
RCTLogError(@"%@", error);
}
});
NSString *query = requestURL.query;
if (
query != nil &&
[videoRegex firstMatchInString:query
options:0
range:NSMakeRange(0, query.length)]
) {
return NO;
}
for (id<RCTImageURLLoader> loader in _loaders) {
// Don't use RCTImageURLLoader protocol for modules that already conform to
// RCTURLRequestHandler as it's inefficient to decode an image and then
// convert it back into data
if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] &&
[loader canLoadImageURL:requestURL]) {
return YES;
}
}
return NO;
}
2015-09-02 18:25:10 +03:00
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
{
__block RCTImageLoaderCancellationBlock requestToken;
requestToken = [self loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
if (error) {
[delegate URLRequest:requestToken didCompleteWithError:error];
return;
}
NSString *mimeType = nil;
NSData *imageData = nil;
if (RCTImageHasAlpha(image.CGImage)) {
mimeType = @"image/png";
imageData = UIImagePNGRepresentation(image);
} else {
mimeType = @"image/jpeg";
imageData = UIImageJPEGRepresentation(image, 1.0);
}
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
MIMEType:mimeType
expectedContentLength:imageData.length
textEncodingName:nil];
[delegate URLRequest:requestToken didReceiveResponse:response];
[delegate URLRequest:requestToken didReceiveData:imageData];
[delegate URLRequest:requestToken didCompleteWithError:nil];
}];
return requestToken;
2015-09-02 18:25:10 +03:00
}
- (void)cancelRequest:(id)requestToken
{
if (requestToken) {
((RCTImageLoaderCancellationBlock)requestToken)();
}
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:
(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
return std::make_shared<facebook::react::NativeImageLoaderIOSSpecJSI>(self, jsInvoker);
}
RCT_EXPORT_METHOD(getSize:(NSString *)uri resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
NSURLRequest *request = [RCTConvert NSURLRequest:uri];
[self getImageSizeForURLRequest:request
block:^(NSError *error, CGSize size) {
if (error) {
reject(
@"E_GET_SIZE_FAILURE",
[NSString stringWithFormat: @"Failed to getSize of %@", uri],
error);
} else {
resolve(@[@(size.width), @(size.height)]);
}
}];
}
RCT_EXPORT_METHOD(getSizeWithHeaders:(NSString *)uri
headers:(NSDictionary *)headers
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSURLRequest *request = [RCTConvert NSURLRequest:uri];
[self getImageSizeForURLRequest:request
block:^(NSError *error, CGSize size) {
if (error) {
reject(@"E_GET_SIZE_FAILURE", nil, error);
return;
}
resolve(@{@"width":@(size.width),@"height":@(size.height)});
}];
}
RCT_EXPORT_METHOD(prefetchImage:(NSString *)uri
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSURLRequest *request = [RCTConvert NSURLRequest:uri];
[self loadImageWithURLRequest:request
callback:^(NSError *error, UIImage *image) {
if (error) {
reject(@"E_PREFETCH_FAILURE", nil, error);
return;
}
resolve(@YES);
}];
}
RCT_EXPORT_METHOD(queryCache:(NSArray *)uris
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
resolve([self getImageCacheStatus:uris]);
}
2015-03-10 03:08:01 +03:00
@end
/**
* DEPRECATED!! DO NOT USE
* Instead use `[_bridge moduleForClass:[RCTImageLoader class]]`
*/
@implementation RCTBridge (RCTImageLoader)
- (RCTImageLoader *)imageLoader
{
RCTLogWarn(@"Calling bridge.imageLoader is deprecated and will not work in newer versions of RN. Please update to the "
"moduleForClass API or turboModuleLookupDelegate API.");
return [self moduleForClass:[RCTImageLoader class]];
}
@end
Class RCTImageLoaderCls(void)
{
return RCTImageLoader.class;
}