mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-29 12:45:37 +08:00
Added throttling on requests made by RCTImageLoader
Reviewed By: javache Differential Revision: D2938143 fb-gh-sync-id: bac1185d4792dcca0012905126c9ef2aa45905d5 shipit-source-id: bac1185d4792dcca0012905126c9ef2aa45905d5
This commit is contained in:
committed by
facebook-github-bot-4
parent
07a5f4407f
commit
0427c3d273
@@ -27,6 +27,31 @@ typedef void (^RCTImageLoaderCancellationBlock)(void);
|
|||||||
|
|
||||||
@interface RCTImageLoader : NSObject <RCTBridgeModule, RCTURLRequestHandler>
|
@interface RCTImageLoader : NSObject <RCTBridgeModule, RCTURLRequestHandler>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of concurrent image loading tasks. Loading and decoding
|
||||||
|
* images can consume a lot of memory, so setting this to a higher value may
|
||||||
|
* cause memory to spike. If you are seeing out-of-memory crashes, try reducing
|
||||||
|
* this value.
|
||||||
|
*/
|
||||||
|
@property (nonatomic, assign) NSUInteger maxConcurrentLoadingTasks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of concurrent image decoding tasks. Decoding large
|
||||||
|
* images can be especially CPU and memory intensive, so if your are decoding a
|
||||||
|
* lot of large images in your app, you may wish to adjust this value.
|
||||||
|
*/
|
||||||
|
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingTasks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decoding large images can use a lot of memory, and potentially cause the app
|
||||||
|
* to crash. This value allows you to throttle the amount of memory used by the
|
||||||
|
* decoder independently of the number of concurrent threads. This means you can
|
||||||
|
* still decode a lot of small images in parallel, without allowing the decoder
|
||||||
|
* to try to decompress multiple huge images at once. Note that this value is
|
||||||
|
* only a hint, and not an indicator of the total memory used by the app.
|
||||||
|
*/
|
||||||
|
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingBytes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the specified image at the highest available resolution.
|
* Loads the specified image at the highest available resolution.
|
||||||
* Can be called from any thread, will call back on an unspecified thread.
|
* Can be called from any thread, will call back on an unspecified thread.
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
NSOperationQueue *_imageDecodeQueue;
|
NSOperationQueue *_imageDecodeQueue;
|
||||||
dispatch_queue_t _URLCacheQueue;
|
dispatch_queue_t _URLCacheQueue;
|
||||||
NSURLCache *_URLCache;
|
NSURLCache *_URLCache;
|
||||||
|
NSMutableArray *_pendingTasks;
|
||||||
|
NSInteger _activeTasks;
|
||||||
|
NSMutableArray *_pendingDecodes;
|
||||||
|
NSInteger _scheduledDecodes;
|
||||||
|
NSUInteger _activeBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@synthesize bridge = _bridge;
|
@synthesize bridge = _bridge;
|
||||||
@@ -49,6 +54,11 @@ RCT_EXPORT_MODULE()
|
|||||||
|
|
||||||
- (void)setUp
|
- (void)setUp
|
||||||
{
|
{
|
||||||
|
// Set defaults
|
||||||
|
_maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks ?: 4;
|
||||||
|
_maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks ?: 2;
|
||||||
|
_maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 *1024; // 30MB
|
||||||
|
|
||||||
// Get image loaders and decoders
|
// Get image loaders and decoders
|
||||||
NSMutableArray<id<RCTImageURLLoader>> *loaders = [NSMutableArray array];
|
NSMutableArray<id<RCTImageURLLoader>> *loaders = [NSMutableArray array];
|
||||||
NSMutableArray<id<RCTImageDataDecoder>> *decoders = [NSMutableArray array];
|
NSMutableArray<id<RCTImageDataDecoder>> *decoders = [NSMutableArray array];
|
||||||
@@ -203,6 +213,50 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
completionBlock:callback];
|
completionBlock:callback];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)dequeueTasks
|
||||||
|
{
|
||||||
|
dispatch_async(_URLCacheQueue, ^{
|
||||||
|
|
||||||
|
// Remove completed tasks
|
||||||
|
for (RCTNetworkTask *task in _pendingTasks.reverseObjectEnumerator) {
|
||||||
|
switch (task.status) {
|
||||||
|
case RCTNetworkTaskFinished:
|
||||||
|
[_pendingTasks removeObject:task];
|
||||||
|
_activeTasks--;
|
||||||
|
break;
|
||||||
|
case RCTNetworkTaskPending:
|
||||||
|
case RCTNetworkTaskInProgress:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start queued decode
|
||||||
|
NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count;
|
||||||
|
while (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
|
||||||
|
activeDecodes <= _maxConcurrentDecodingTasks)) {
|
||||||
|
dispatch_block_t decodeBlock = _pendingDecodes.firstObject;
|
||||||
|
if (decodeBlock) {
|
||||||
|
[_pendingDecodes removeObjectAtIndex:0];
|
||||||
|
decodeBlock();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start queued tasks
|
||||||
|
for (RCTNetworkTask *task in _pendingTasks) {
|
||||||
|
if (MAX(_activeTasks, _scheduledDecodes) >= _maxConcurrentLoadingTasks) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (task.status == RCTNetworkTaskPending) {
|
||||||
|
[task start];
|
||||||
|
_activeTasks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This returns either an image, or raw image data, depending on the loading
|
* 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
|
* path taken. This is useful if you want to skip decoding, e.g. when preloading
|
||||||
@@ -327,8 +381,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download image
|
// Download image
|
||||||
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
|
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
|
||||||
^(NSURLResponse *response, NSData *data, NSError *error) {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
completionHandler(error, nil);
|
completionHandler(error, nil);
|
||||||
return;
|
return;
|
||||||
@@ -348,14 +401,26 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
// Process image data
|
// Process image data
|
||||||
processResponse(response, data, nil);
|
processResponse(response, data, nil);
|
||||||
|
|
||||||
|
//clean up
|
||||||
|
[weakSelf dequeueTasks];
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}];
|
}];
|
||||||
task.downloadProgressBlock = progressHandler;
|
task.downloadProgressBlock = progressHandler;
|
||||||
[task start];
|
|
||||||
|
if (!_pendingTasks) {
|
||||||
|
_pendingTasks = [NSMutableArray new];
|
||||||
|
}
|
||||||
|
[_pendingTasks addObject:task];
|
||||||
|
if (MAX(_activeTasks, _scheduledDecodes) < _maxConcurrentLoadingTasks) {
|
||||||
|
[task start];
|
||||||
|
_activeTasks++;
|
||||||
|
}
|
||||||
|
|
||||||
cancelLoad = ^{
|
cancelLoad = ^{
|
||||||
[task cancel];
|
[task cancel];
|
||||||
|
[weakSelf dequeueTasks];
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -453,7 +518,6 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
__block volatile uint32_t cancelled = 0;
|
__block volatile uint32_t cancelled = 0;
|
||||||
void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) {
|
void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) {
|
||||||
if ([NSThread isMainThread]) {
|
if ([NSThread isMainThread]) {
|
||||||
|
|
||||||
// Most loaders do not return on the main thread, so caller is probably not
|
// 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
|
// expecting it, and may do expensive post-processing in the callback
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
@@ -475,40 +539,71 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
completionHandler:completionHandler];
|
completionHandler:completionHandler];
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Serialize decoding to prevent excessive memory usage
|
dispatch_async(_URLCacheQueue, ^{
|
||||||
if (!_imageDecodeQueue) {
|
dispatch_block_t decodeBlock = ^{
|
||||||
_imageDecodeQueue = [NSOperationQueue new];
|
|
||||||
_imageDecodeQueue.name = @"com.facebook.react.ImageDecoderQueue";
|
|
||||||
_imageDecodeQueue.maxConcurrentOperationCount = 2;
|
|
||||||
}
|
|
||||||
[_imageDecodeQueue addOperationWithBlock:^{
|
|
||||||
if (cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
|
// 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
|
||||||
|
_activeBytes += decodedImageBytes;
|
||||||
|
|
||||||
|
// Do actual decompression on a concurrent background queue
|
||||||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
if (!cancelled) {
|
||||||
|
|
||||||
|
// Decompress the image data (this may be CPU and memory intensive)
|
||||||
|
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
|
||||||
|
|
||||||
#if RCT_DEV
|
#if RCT_DEV
|
||||||
|
|
||||||
CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale);
|
CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale);
|
||||||
CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale());
|
CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale());
|
||||||
if (imagePixelSize.width * imagePixelSize.height >
|
if (imagePixelSize.width * imagePixelSize.height >
|
||||||
screenPixelSize.width * screenPixelSize.height) {
|
screenPixelSize.width * screenPixelSize.height) {
|
||||||
RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger "
|
RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger "
|
||||||
"than the screen size %@", NSStringFromCGSize(imagePixelSize),
|
"than the screen size %@", NSStringFromCGSize(imagePixelSize),
|
||||||
NSStringFromCGSize(screenPixelSize));
|
NSStringFromCGSize(screenPixelSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
completionHandler(nil, image);
|
completionHandler(nil, image);
|
||||||
} else {
|
} else {
|
||||||
NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
|
NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
|
||||||
NSError *finalError = RCTErrorWithMessage(errorMessage);
|
NSError *finalError = RCTErrorWithMessage(errorMessage);
|
||||||
completionHandler(finalError, nil);
|
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(_URLCacheQueue, ^{
|
||||||
|
_scheduledDecodes--;
|
||||||
|
_activeBytes -= decodedImageBytes;
|
||||||
|
[self dequeueTasks];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
_scheduledDecodes++;
|
||||||
|
|
||||||
|
if (!_pendingDecodes) {
|
||||||
|
_pendingDecodes = [NSMutableArray new];
|
||||||
}
|
}
|
||||||
}];
|
NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count - 1;
|
||||||
|
if (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
|
||||||
|
activeDecodes <= _maxConcurrentDecodingTasks)) {
|
||||||
|
decodeBlock();
|
||||||
|
} else {
|
||||||
|
[_pendingDecodes addObject:decodeBlock];
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
return ^{
|
return ^{
|
||||||
OSAtomicOr32Barrier(1, &cancelled);
|
OSAtomicOr32Barrier(1, &cancelled);
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data);
|
|||||||
typedef void (^RCTURLRequestProgressBlock)(int64_t progress, int64_t total);
|
typedef void (^RCTURLRequestProgressBlock)(int64_t progress, int64_t total);
|
||||||
typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
|
typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, RCTNetworkTaskStatus) {
|
||||||
|
RCTNetworkTaskPending = 0,
|
||||||
|
RCTNetworkTaskInProgress,
|
||||||
|
RCTNetworkTaskFinished,
|
||||||
|
};
|
||||||
|
|
||||||
@interface RCTNetworkTask : NSObject <RCTURLRequestDelegate>
|
@interface RCTNetworkTask : NSObject <RCTURLRequestDelegate>
|
||||||
|
|
||||||
@property (nonatomic, readonly) NSURLRequest *request;
|
@property (nonatomic, readonly) NSURLRequest *request;
|
||||||
@@ -31,6 +37,8 @@ typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
|
|||||||
@property (nonatomic, copy) RCTURLRequestResponseBlock responseBlock;
|
@property (nonatomic, copy) RCTURLRequestResponseBlock responseBlock;
|
||||||
@property (nonatomic, copy) RCTURLRequestProgressBlock uploadProgressBlock;
|
@property (nonatomic, copy) RCTURLRequestProgressBlock uploadProgressBlock;
|
||||||
|
|
||||||
|
@property (nonatomic, readonly) RCTNetworkTaskStatus status;
|
||||||
|
|
||||||
- (instancetype)initWithRequest:(NSURLRequest *)request
|
- (instancetype)initWithRequest:(NSURLRequest *)request
|
||||||
handler:(id<RCTURLRequestHandler>)handler
|
handler:(id<RCTURLRequestHandler>)handler
|
||||||
completionBlock:(RCTURLRequestCompletionBlock)completionBlock NS_DESIGNATED_INITIALIZER;
|
completionBlock:(RCTURLRequestCompletionBlock)completionBlock NS_DESIGNATED_INITIALIZER;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
_request = request;
|
_request = request;
|
||||||
_handler = handler;
|
_handler = handler;
|
||||||
_completionBlock = completionBlock;
|
_completionBlock = completionBlock;
|
||||||
|
_status = RCTNetworkTaskPending;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -55,6 +56,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||||||
if ([self validateRequestToken:[_handler sendRequest:_request
|
if ([self validateRequestToken:[_handler sendRequest:_request
|
||||||
withDelegate:self]]) {
|
withDelegate:self]]) {
|
||||||
_selfReference = self;
|
_selfReference = self;
|
||||||
|
_status = RCTNetworkTaskInProgress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +68,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||||||
[_handler cancelRequest:strongToken];
|
[_handler cancelRequest:strongToken];
|
||||||
}
|
}
|
||||||
[self invalidate];
|
[self invalidate];
|
||||||
|
_status = RCTNetworkTaskFinished;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)validateRequestToken:(id)requestToken
|
- (BOOL)validateRequestToken:(id)requestToken
|
||||||
@@ -83,8 +86,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||||||
if (_completionBlock) {
|
if (_completionBlock) {
|
||||||
_completionBlock(_response, _data, [NSError errorWithDomain:RCTErrorDomain code:0
|
_completionBlock(_response, _data, [NSError errorWithDomain:RCTErrorDomain code:0
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Unrecognized request token."}]);
|
userInfo:@{NSLocalizedDescriptionKey: @"Unrecognized request token."}]);
|
||||||
[self invalidate];
|
|
||||||
}
|
}
|
||||||
|
[self invalidate];
|
||||||
|
_status = RCTNetworkTaskFinished;
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -130,8 +134,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||||||
if ([self validateRequestToken:requestToken]) {
|
if ([self validateRequestToken:requestToken]) {
|
||||||
if (_completionBlock) {
|
if (_completionBlock) {
|
||||||
_completionBlock(_response, _data, error);
|
_completionBlock(_response, _data, error);
|
||||||
[self invalidate];
|
|
||||||
}
|
}
|
||||||
|
[self invalidate];
|
||||||
|
_status = RCTNetworkTaskFinished;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ NSString *RCTJSCProfilerStop(JSContextRef ctx)
|
|||||||
if (isProfiling) {
|
if (isProfiling) {
|
||||||
NSString *filename = [NSString stringWithFormat:@"cpu_profile_%ld.json", (long)CFAbsoluteTimeGetCurrent()];
|
NSString *filename = [NSString stringWithFormat:@"cpu_profile_%ld.json", (long)CFAbsoluteTimeGetCurrent()];
|
||||||
outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
|
outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
|
||||||
RCTNativeProfilerEnd(ctx, JSCProfileName, outputFile.UTF8String);
|
if (RCTNativeProfilerEnd) {
|
||||||
|
RCTNativeProfilerEnd(ctx, JSCProfileName, outputFile.UTF8String);
|
||||||
|
}
|
||||||
RCTLogInfo(@"Stopped JSC profiler for context: %p", ctx);
|
RCTLogInfo(@"Stopped JSC profiler for context: %p", ctx);
|
||||||
} else {
|
} else {
|
||||||
RCTLogWarn(@"Trying to stop JSC profiler on a context which is not being profiled.");
|
RCTLogWarn(@"Trying to stop JSC profiler on a context which is not being profiled.");
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ void RCTProfileInit(RCTBridge *bridge)
|
|||||||
dispatch_async(RCTProfileGetQueue(), ^{
|
dispatch_async(RCTProfileGetQueue(), ^{
|
||||||
NSString *shadowQueue = @(dispatch_queue_get_label([[bridge uiManager] methodQueue]));
|
NSString *shadowQueue = @(dispatch_queue_get_label([[bridge uiManager] methodQueue]));
|
||||||
NSArray *orderedThreads = @[@"JS async", RCTJSCThreadName, shadowQueue, @"main"];
|
NSArray *orderedThreads = @[@"JS async", RCTJSCThreadName, shadowQueue, @"main"];
|
||||||
[orderedThreads enumerateObjectsUsingBlock:^(NSString *thread, NSUInteger idx, BOOL *stop) {
|
[orderedThreads enumerateObjectsUsingBlock:^(NSString *thread, NSUInteger idx, __unused BOOL *stop) {
|
||||||
RCTProfileAddEvent(RCTProfileTraceEvents,
|
RCTProfileAddEvent(RCTProfileTraceEvents,
|
||||||
@"ph": @"M", // metadata event
|
@"ph": @"M", // metadata event
|
||||||
@"name": @"thread_sort_index",
|
@"name": @"thread_sort_index",
|
||||||
|
|||||||
Reference in New Issue
Block a user