@@ -41,6 +41,11 @@ @implementation RCTImageLoader
4141 NSOperationQueue *_imageDecodeQueue;
4242 dispatch_queue_t _URLCacheQueue;
4343 NSURLCache *_URLCache;
44+ NSMutableArray *_pendingTasks;
45+ NSInteger _activeTasks;
46+ NSMutableArray *_pendingDecodes;
47+ NSInteger _scheduledDecodes;
48+ NSUInteger _activeBytes;
4449}
4550
4651@synthesize bridge = _bridge;
@@ -49,6 +54,11 @@ @implementation RCTImageLoader
4954
5055- (void )setUp
5156{
57+ // Set defaults
58+ _maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks ?: 4 ;
59+ _maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks ?: 2 ;
60+ _maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 *1024 ; // 30MB
61+
5262 // Get image loaders and decoders
5363 NSMutableArray <id <RCTImageURLLoader>> *loaders = [NSMutableArray array ];
5464 NSMutableArray <id <RCTImageDataDecoder>> *decoders = [NSMutableArray array ];
@@ -203,6 +213,50 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
203213 completionBlock: callback];
204214}
205215
216+ - (void )dequeueTasks
217+ {
218+ dispatch_async (_URLCacheQueue, ^{
219+
220+ // Remove completed tasks
221+ for (RCTNetworkTask *task in _pendingTasks.reverseObjectEnumerator ) {
222+ switch (task.status ) {
223+ case RCTNetworkTaskFinished:
224+ [_pendingTasks removeObject: task];
225+ _activeTasks--;
226+ break ;
227+ case RCTNetworkTaskPending:
228+ case RCTNetworkTaskInProgress:
229+ // Do nothing
230+ break ;
231+ }
232+ }
233+
234+ // Start queued decode
235+ NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count ;
236+ while (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
237+ activeDecodes <= _maxConcurrentDecodingTasks)) {
238+ dispatch_block_t decodeBlock = _pendingDecodes.firstObject ;
239+ if (decodeBlock) {
240+ [_pendingDecodes removeObjectAtIndex: 0 ];
241+ decodeBlock ();
242+ } else {
243+ break ;
244+ }
245+ }
246+
247+ // Start queued tasks
248+ for (RCTNetworkTask *task in _pendingTasks) {
249+ if (MAX (_activeTasks, _scheduledDecodes) >= _maxConcurrentLoadingTasks) {
250+ break ;
251+ }
252+ if (task.status == RCTNetworkTaskPending) {
253+ [task start ];
254+ _activeTasks++;
255+ }
256+ }
257+ });
258+ }
259+
206260/* *
207261 * This returns either an image, or raw image data, depending on the loading
208262 * path taken. This is useful if you want to skip decoding, e.g. when preloading
@@ -327,8 +381,7 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag
327381 }
328382
329383 // Download image
330- RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest: request completionBlock:
331- ^(NSURLResponse *response, NSData *data, NSError *error) {
384+ RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest: request completionBlock: ^(NSURLResponse *response, NSData *data, NSError *error) {
332385 if (error) {
333386 completionHandler (error, nil );
334387 return ;
@@ -348,14 +401,26 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag
348401 // Process image data
349402 processResponse (response, data, nil );
350403
404+ // clean up
405+ [weakSelf dequeueTasks ];
406+
351407 });
352408
353409 }];
354410 task.downloadProgressBlock = progressHandler;
355- [task start ];
411+
412+ if (!_pendingTasks) {
413+ _pendingTasks = [NSMutableArray new ];
414+ }
415+ [_pendingTasks addObject: task];
416+ if (MAX (_activeTasks, _scheduledDecodes) < _maxConcurrentLoadingTasks) {
417+ [task start ];
418+ _activeTasks++;
419+ }
356420
357421 cancelLoad = ^{
358422 [task cancel ];
423+ [weakSelf dequeueTasks ];
359424 };
360425
361426 });
@@ -453,7 +518,6 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data
453518 __block volatile uint32_t cancelled = 0 ;
454519 void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) {
455520 if ([NSThread isMainThread ]) {
456-
457521 // Most loaders do not return on the main thread, so caller is probably not
458522 // expecting it, and may do expensive post-processing in the callback
459523 dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
@@ -475,40 +539,71 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data
475539 completionHandler: completionHandler];
476540 } else {
477541
478- // Serialize decoding to prevent excessive memory usage
479- if (!_imageDecodeQueue) {
480- _imageDecodeQueue = [NSOperationQueue new ];
481- _imageDecodeQueue.name = @" com.facebook.react.ImageDecoderQueue" ;
482- _imageDecodeQueue.maxConcurrentOperationCount = 2 ;
483- }
484- [_imageDecodeQueue addOperationWithBlock: ^{
485- if (cancelled) {
486- return ;
487- }
542+ dispatch_async (_URLCacheQueue, ^{
543+ dispatch_block_t decodeBlock = ^{
544+
545+ // Calculate the size, in bytes, that the decompressed image will require
546+ NSInteger decodedImageBytes = (size.width * scale) * (size.height * scale) * 4 ;
488547
489- UIImage *image = RCTDecodeImageWithData (data, size, scale, resizeMode);
548+ // Mark these bytes as in-use
549+ _activeBytes += decodedImageBytes;
550+
551+ // Do actual decompression on a concurrent background queue
552+ dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
553+ if (!cancelled) {
554+
555+ // Decompress the image data (this may be CPU and memory intensive)
556+ UIImage *image = RCTDecodeImageWithData (data, size, scale, resizeMode);
490557
491558#if RCT_DEV
492559
493- CGSize imagePixelSize = RCTSizeInPixels (image.size , image.scale );
494- CGSize screenPixelSize = RCTSizeInPixels (RCTScreenSize (), RCTScreenScale ());
495- if (imagePixelSize.width * imagePixelSize.height >
496- screenPixelSize.width * screenPixelSize.height ) {
497- RCTLogInfo (@" [PERF ASSETS] Loading image at size %@ , which is larger "
498- " than the screen size %@ " , NSStringFromCGSize(imagePixelSize),
499- NSStringFromCGSize (screenPixelSize));
500- }
560+ CGSize imagePixelSize = RCTSizeInPixels (image.size , image.scale );
561+ CGSize screenPixelSize = RCTSizeInPixels (RCTScreenSize (), RCTScreenScale ());
562+ if (imagePixelSize.width * imagePixelSize.height >
563+ screenPixelSize.width * screenPixelSize.height ) {
564+ RCTLogInfo (@" [PERF ASSETS] Loading image at size %@ , which is larger "
565+ " than the screen size %@ " , NSStringFromCGSize(imagePixelSize),
566+ NSStringFromCGSize (screenPixelSize));
567+ }
501568
502569#endif
503570
504- if (image) {
505- completionHandler (nil , image);
571+ if (image) {
572+ completionHandler (nil , image);
573+ } else {
574+ NSString *errorMessage = [NSString stringWithFormat: @" Error decoding image data <NSData %p ; %tu bytes>" , data, data.length];
575+ NSError *finalError = RCTErrorWithMessage (errorMessage);
576+ completionHandler (finalError, nil );
577+ }
578+ }
579+
580+ // We're no longer retaining the uncompressed data, so now we'll mark
581+ // the decoding as complete so that the loading task queue can resume.
582+ dispatch_async (_URLCacheQueue, ^{
583+ _scheduledDecodes--;
584+ _activeBytes -= decodedImageBytes;
585+ [self dequeueTasks ];
586+ });
587+ });
588+ };
589+
590+ // The decode operation retains the compressed image data until it's
591+ // complete, so we'll mark it as having started, in order to block
592+ // further image loads from happening until we're done with the data.
593+ _scheduledDecodes++;
594+
595+ if (!_pendingDecodes) {
596+ _pendingDecodes = [NSMutableArray new ];
597+ }
598+ NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count - 1 ;
599+ if (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
600+ activeDecodes <= _maxConcurrentDecodingTasks)) {
601+ decodeBlock ();
506602 } else {
507- NSString *errorMessage = [NSString stringWithFormat: @" Error decoding image data <NSData %p ; %tu bytes>" , data, data.length];
508- NSError *finalError = RCTErrorWithMessage (errorMessage);
509- completionHandler (finalError, nil );
603+ [_pendingDecodes addObject: decodeBlock];
510604 }
511- }];
605+
606+ });
512607
513608 return ^{
514609 OSAtomicOr32Barrier (1 , &cancelled);
0 commit comments