From 3b67bfab1ed8f2a4cede4e58b41a8b11c63ab484 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Mon, 24 Jun 2019 03:40:38 -0700 Subject: [PATCH] Animated image improvements (#24822) Summary: The goal of this PR is to improve the pipeline currently used for displaying GIFs / animated images on iOS. It is achieved by not holding all of the decoded frames in memory at the same time, as well as happily releasing existing memory whenever possible. This code is a simplified version of what you would find in SDWebImage (it is nearly 1:1, with unsupported or uneeded things removed). By adopting this API, it also allows classes conforming to RCTImageURLLoader or RCTImageDataDecoder to return any decodable UIImages conforming to RCTAnimatedImage and have improvements to memory consumption. Because RCTAnimatedImage is just a subset of the SDAnimatedImage protocol, it also means that you can use SDWebImage easier with Image directly. A nice to have would be progressive image loading, but is beyond scope for this PR. It would, however, touch most of these same parts. ## Changelog [iOS] [Fixed] - Substantially lower chances of crashes from abundant GIF use Pull Request resolved: https://github.com/facebook/react-native/pull/24822 Test Plan: TBD. (but i am running a version of this in my own app currently) Reviewed By: shergin Differential Revision: D15853479 Pulled By: sammy-SC fbshipit-source-id: 969e0d458da9fa49453aee1dcdf51783c2a45067 --- Libraries/Image/RCTAnimatedImage.h | 21 ++ Libraries/Image/RCTAnimatedImage.m | 165 +++++++++ Libraries/Image/RCTGIFImageDecoder.m | 92 +---- .../Image/RCTImage.xcodeproj/project.pbxproj | 41 ++- Libraries/Image/RCTImageView.m | 5 +- Libraries/Image/RCTUIImageViewAnimated.h | 12 + Libraries/Image/RCTUIImageViewAnimated.m | 323 ++++++++++++++++++ 7 files changed, 558 insertions(+), 101 deletions(-) create mode 100644 Libraries/Image/RCTAnimatedImage.h create mode 100644 Libraries/Image/RCTAnimatedImage.m create mode 100644 Libraries/Image/RCTUIImageViewAnimated.h create mode 100644 Libraries/Image/RCTUIImageViewAnimated.m diff --git a/Libraries/Image/RCTAnimatedImage.h b/Libraries/Image/RCTAnimatedImage.h new file mode 100644 index 0000000000..689effb2b1 --- /dev/null +++ b/Libraries/Image/RCTAnimatedImage.h @@ -0,0 +1,21 @@ +/** + * 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. + */ + +#import + +@protocol RCTAnimatedImage +@property (nonatomic, assign, readonly) NSUInteger animatedImageFrameCount; +@property (nonatomic, assign, readonly) NSUInteger animatedImageLoopCount; + +- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index; +- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index; + +@end + +@interface RCTAnimatedImage : UIImage + +@end diff --git a/Libraries/Image/RCTAnimatedImage.m b/Libraries/Image/RCTAnimatedImage.m new file mode 100644 index 0000000000..bfb4ca5a4d --- /dev/null +++ b/Libraries/Image/RCTAnimatedImage.m @@ -0,0 +1,165 @@ +/** + * 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. + */ + +#import "RCTAnimatedImage.h" + +@interface RCTGIFCoderFrame : NSObject + +@property (nonatomic, assign) NSUInteger index; +@property (nonatomic, assign) NSTimeInterval duration; + +@end + +@implementation RCTGIFCoderFrame +@end + +@implementation RCTAnimatedImage { + CGImageSourceRef _imageSource; + CGFloat _scale; + NSUInteger _loopCount; + NSUInteger _frameCount; + NSArray *_frames; +} + +- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale +{ + if (self = [super init]) { + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); + if (!imageSource) { + return nil; + } + + BOOL framesValid = [self scanAndCheckFramesValidWithSource:imageSource]; + if (!framesValid) { + CFRelease(imageSource); + return nil; + } + + _imageSource = imageSource; + + // grab image at the first index + UIImage *image = [self animatedImageFrameAtIndex:0]; + if (!image) { + return nil; + } + self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + } + + return self; +} + +- (BOOL)scanAndCheckFramesValidWithSource:(CGImageSourceRef)imageSource +{ + if (!imageSource) { + return NO; + } + NSUInteger frameCount = CGImageSourceGetCount(imageSource); + NSUInteger loopCount = [self imageLoopCountWithSource:imageSource]; + NSMutableArray *frames = [NSMutableArray array]; + + for (size_t i = 0; i < frameCount; i++) { + RCTGIFCoderFrame *frame = [[RCTGIFCoderFrame alloc] init]; + frame.index = i; + frame.duration = [self frameDurationAtIndex:i source:imageSource]; + [frames addObject:frame]; + } + + _frameCount = frameCount; + _loopCount = loopCount; + _frames = [frames copy]; + + return YES; +} + +- (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source +{ + NSUInteger loopCount = 1; + NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil); + NSDictionary *gifProperties = imageProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary]; + if (gifProperties) { + NSNumber *gifLoopCount = gifProperties[(__bridge NSString *)kCGImagePropertyGIFLoopCount]; + if (gifLoopCount != nil) { + loopCount = gifLoopCount.unsignedIntegerValue; + } + } + return loopCount; +} + +- (float)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source +{ + float frameDuration = 0.1f; + CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil); + if (!cfFrameProperties) { + return frameDuration; + } + NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties; + NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; + + NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; + if (delayTimeUnclampedProp != nil) { + frameDuration = [delayTimeUnclampedProp floatValue]; + } else { + NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; + if (delayTimeProp != nil) { + frameDuration = [delayTimeProp floatValue]; + } + } + + CFRelease(cfFrameProperties); + return frameDuration; +} + +- (NSUInteger)animatedImageLoopCount +{ + return _loopCount; +} + +- (NSUInteger)animatedImageFrameCount +{ + return _frameCount; +} + +- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index +{ + if (index >= _frameCount) { + return 0; + } + return _frames[index].duration; +} + +- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index +{ + CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); + if (!imageRef) { + return nil; + } + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:UIImageOrientationUp]; + CGImageRelease(imageRef); + return image; +} + +- (void)didReceiveMemoryWarning:(NSNotification *)notification +{ + if (_imageSource) { + for (size_t i = 0; i < _frameCount; i++) { + CGImageSourceRemoveCacheAtIndex(_imageSource, i); + } + } +} + +- (void)dealloc +{ + if (_imageSource) { + CFRelease(_imageSource); + _imageSource = NULL; + } + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +} + +@end diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index a0afd43f4d..a5f13a6668 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -11,6 +11,7 @@ #import #import +#import "RCTAnimatedImage.h" @implementation RCTGIFImageDecoder @@ -30,96 +31,13 @@ RCT_EXPORT_MODULE() resizeMode:(RCTResizeMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { - CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); - if (!imageSource) { + RCTAnimatedImage *image = [[RCTAnimatedImage alloc] initWithData:imageData scale:scale]; + + if (!image) { completionHandler(nil, nil); return ^{}; } - NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); - CGFloat loopCount = 0; - if ([[properties[(id)kCGImagePropertyGIFDictionary] allKeys] containsObject:(id)kCGImagePropertyGIFLoopCount]) { - loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; - if (loopCount == 0) { - // A loop count of 0 means infinite - loopCount = HUGE_VALF; - } else { - // A loop count of 1 means it should repeat twice, 2 means, thrice, etc. - loopCount += 1; - } - } - - UIImage *image = nil; - size_t imageCount = CGImageSourceGetCount(imageSource); - if (imageCount > 1) { - - NSTimeInterval duration = 0; - NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; - NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; - for (size_t i = 0; i < imageCount; i++) { - - CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); - if (!imageRef) { - continue; - } - if (!image) { - image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; - } - - NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL); - NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary]; - - const NSTimeInterval kDelayTimeIntervalDefault = 0.1; - NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; - if (delayTime == nil) { - if (delays.count == 0) { - delayTime = @(kDelayTimeIntervalDefault); - } else { - delayTime = delays.lastObject; - } - } - - const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; - if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) { - delayTime = @(kDelayTimeIntervalDefault); - } - - duration += delayTime.doubleValue; - [delays addObject:delayTime]; - [images addObject:(__bridge_transfer id)imageRef]; - } - CFRelease(imageSource); - - NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; - NSTimeInterval runningDuration = 0; - for (NSNumber *delayNumber in delays) { - [keyTimes addObject:@(runningDuration / duration)]; - runningDuration += delayNumber.doubleValue; - } - - [keyTimes addObject:@1.0]; - - // Create animation - CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; - animation.calculationMode = kCAAnimationDiscrete; - animation.repeatCount = loopCount; - animation.keyTimes = keyTimes; - animation.values = images; - animation.duration = duration; - animation.removedOnCompletion = NO; - animation.fillMode = kCAFillModeForwards; - image.reactKeyframeAnimation = animation; - - } else { - - // Don't bother creating an animation - CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); - if (imageRef) { - image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; - CFRelease(imageRef); - } - CFRelease(imageSource); - } - + completionHandler(nil, image); return ^{}; } diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index bbf4d71042..30fbcdf83b 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */; }; 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */; }; - 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 */; }; 13EF7F7F1BC825B1003F47DD /* RCTLocalAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */; }; @@ -18,7 +17,6 @@ 2D3B5F1B1D9B0D0700451313 /* RCTImageBlurUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */; }; 2D3B5F1C1D9B0D1300451313 /* RCTResizeMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 139A38831C4D587C00862840 /* RCTResizeMode.m */; }; 2D3B5F1D1D9B0D1300451313 /* RCTLocalAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */; }; - 2D3B5F1E1D9B0D1300451313 /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */; }; 2D3B5F1F1D9B0D1300451313 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; }; 2D3B5F201D9B0D1300451313 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 2D3B5F211D9B0D1300451313 /* RCTImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */; }; @@ -31,7 +29,6 @@ 3D302F211DF8269200D6DDAE /* RCTImageUtils.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */; }; 3DA05A5A1EE0312600805843 /* RCTImageShadowView.h in Headers */ = {isa = PBXBuildFile; fileRef = 59AB09281EDE5DD1009F97B5 /* RCTImageShadowView.h */; }; 3DA05A5B1EE0312900805843 /* RCTImageShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59AB09291EDE5DD1009F97B5 /* RCTImageShadowView.m */; }; - 3DED3A8A1DE6F79800336DD7 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */; }; 3DED3A8B1DE6F79800336DD7 /* RCTImageBlurUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */; }; 3DED3A8C1DE6F79800336DD7 /* RCTImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CCD34C251D4B8FE900268922 /* RCTImageCache.h */; }; 3DED3A8D1DE6F79800336DD7 /* RCTImageEditingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 354631661B69857700AA0B86 /* RCTImageEditingManager.h */; }; @@ -42,7 +39,6 @@ 3DED3A921DE6F79800336DD7 /* RCTImageViewManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */; }; 3DED3A931DE6F79800336DD7 /* RCTLocalAssetImageLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 13EF7F7D1BC825B1003F47DD /* RCTLocalAssetImageLoader.h */; }; 3DED3A941DE6F79800336DD7 /* RCTResizeMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5FA63E1DE4B44A0058FD77 /* RCTResizeMode.h */; }; - 3DED3A961DE6F7A400336DD7 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */; }; 3DED3A971DE6F7A400336DD7 /* RCTImageBlurUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */; }; 3DED3A981DE6F7A400336DD7 /* RCTImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CCD34C251D4B8FE900268922 /* RCTImageCache.h */; }; 3DED3A991DE6F7A400336DD7 /* RCTImageEditingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 354631661B69857700AA0B86 /* RCTImageEditingManager.h */; }; @@ -55,6 +51,14 @@ 3DED3AA01DE6F7A400336DD7 /* RCTResizeMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5FA63E1DE4B44A0058FD77 /* RCTResizeMode.h */; }; 59AB092A1EDE5DD1009F97B5 /* RCTImageShadowView.h in Headers */ = {isa = PBXBuildFile; fileRef = 59AB09281EDE5DD1009F97B5 /* RCTImageShadowView.h */; }; 59AB092B1EDE5DD1009F97B5 /* RCTImageShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59AB09291EDE5DD1009F97B5 /* RCTImageShadowView.m */; }; + 681D33F822880795003CD9D1 /* RCTAnimatedImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D33F622880794003CD9D1 /* RCTAnimatedImage.m */; }; + 681D33F922880795003CD9D1 /* RCTAnimatedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D33F722880795003CD9D1 /* RCTAnimatedImage.h */; }; + 681D33FC228807A1003CD9D1 /* RCTUIImageViewAnimated.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D33FA228807A1003CD9D1 /* RCTUIImageViewAnimated.h */; }; + 681D33FD228807A1003CD9D1 /* RCTUIImageViewAnimated.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D33FB228807A1003CD9D1 /* RCTUIImageViewAnimated.m */; }; + 681D340422880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */; }; + 681D340522880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */; }; + 681D340622880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */; }; + 681D340722880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */; }; CCD34C271D4B8FE900268922 /* RCTImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CCD34C261D4B8FE900268922 /* RCTImageCache.m */; }; EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */; }; /* End PBXBuildFile section */ @@ -89,8 +93,6 @@ 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageView.m; sourceTree = ""; }; 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageViewManager.h; sourceTree = ""; }; 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageViewManager.m; sourceTree = ""; }; - 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTGIFImageDecoder.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImageDecoder.m; sourceTree = ""; }; 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageUtils.h; sourceTree = ""; }; 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = ""; }; 139A38831C4D587C00862840 /* RCTResizeMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTResizeMode.m; sourceTree = ""; }; @@ -108,6 +110,12 @@ 58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; }; 59AB09281EDE5DD1009F97B5 /* RCTImageShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageShadowView.h; sourceTree = ""; }; 59AB09291EDE5DD1009F97B5 /* RCTImageShadowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageShadowView.m; sourceTree = ""; }; + 681D33F622880794003CD9D1 /* RCTAnimatedImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimatedImage.m; sourceTree = ""; }; + 681D33F722880795003CD9D1 /* RCTAnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimatedImage.h; sourceTree = ""; }; + 681D33FA228807A1003CD9D1 /* RCTUIImageViewAnimated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIImageViewAnimated.h; sourceTree = ""; }; + 681D33FB228807A1003CD9D1 /* RCTUIImageViewAnimated.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIImageViewAnimated.m; sourceTree = ""; }; + 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImageDecoder.h; sourceTree = ""; }; + 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImageDecoder.m; sourceTree = ""; }; CCD34C251D4B8FE900268922 /* RCTImageCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTImageCache.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; CCD34C261D4B8FE900268922 /* RCTImageCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageCache.m; sourceTree = ""; }; EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTImageBlurUtils.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; @@ -128,8 +136,6 @@ children = ( 3D5FA68B1DE4BA290058FD77 /* Frameworks */, 58B5115E1A9E6B3D00147676 /* Products */, - 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */, - 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */, EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */, EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */, CCD34C251D4B8FE900268922 /* RCTImageCache.h */, @@ -152,6 +158,12 @@ 13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */, 3D5FA63E1DE4B44A0058FD77 /* RCTResizeMode.h */, 139A38831C4D587C00862840 /* RCTResizeMode.m */, + 681D33F722880795003CD9D1 /* RCTAnimatedImage.h */, + 681D33F622880794003CD9D1 /* RCTAnimatedImage.m */, + 681D33FA228807A1003CD9D1 /* RCTUIImageViewAnimated.h */, + 681D33FB228807A1003CD9D1 /* RCTUIImageViewAnimated.m */, + 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */, + 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */, ); indentWidth = 2; sourceTree = ""; @@ -174,11 +186,13 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 3DED3A8A1DE6F79800336DD7 /* RCTGIFImageDecoder.h in Headers */, 3DED3A901DE6F79800336DD7 /* RCTImageUtils.h in Headers */, + 681D33F922880795003CD9D1 /* RCTAnimatedImage.h in Headers */, 3DED3A8B1DE6F79800336DD7 /* RCTImageBlurUtils.h in Headers */, 3DED3A8C1DE6F79800336DD7 /* RCTImageCache.h in Headers */, + 681D340422880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */, 3DED3A8D1DE6F79800336DD7 /* RCTImageEditingManager.h in Headers */, + 681D33FC228807A1003CD9D1 /* RCTUIImageViewAnimated.h in Headers */, 3DED3A8E1DE6F79800336DD7 /* RCTImageLoader.h in Headers */, 3DED3A8F1DE6F79800336DD7 /* RCTImageStoreManager.h in Headers */, 59AB092A1EDE5DD1009F97B5 /* RCTImageShadowView.h in Headers */, @@ -193,9 +207,9 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 3DED3A961DE6F7A400336DD7 /* RCTGIFImageDecoder.h in Headers */, 3DED3A971DE6F7A400336DD7 /* RCTImageBlurUtils.h in Headers */, 3DED3A981DE6F7A400336DD7 /* RCTImageCache.h in Headers */, + 681D340522880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */, 3DED3A991DE6F7A400336DD7 /* RCTImageEditingManager.h in Headers */, 3DED3A9A1DE6F7A400336DD7 /* RCTImageLoader.h in Headers */, 3DED3A9B1DE6F7A400336DD7 /* RCTImageStoreManager.h in Headers */, @@ -268,6 +282,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 58B511541A9E6B3D00147676; @@ -289,8 +304,8 @@ 2D3B5F231D9B0D1300451313 /* RCTImageStoreManager.m in Sources */, 2D3B5F1A1D9B0D0400451313 /* RCTImageCache.m in Sources */, 2D3B5F1D1D9B0D1300451313 /* RCTLocalAssetImageLoader.m in Sources */, + 681D340722880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */, 2D3B5F1F1D9B0D1300451313 /* RCTImageEditingManager.m in Sources */, - 2D3B5F1E1D9B0D1300451313 /* RCTGIFImageDecoder.m in Sources */, 2D3B5F1C1D9B0D1300451313 /* RCTResizeMode.m in Sources */, 2D3B5F221D9B0D1300451313 /* RCTImageViewManager.m in Sources */, 2D3B5F211D9B0D1300451313 /* RCTImageView.m in Sources */, @@ -306,13 +321,15 @@ buildActionMask = 2147483647; files = ( 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */, + 681D340622880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */, - 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */, + 681D33F822880795003CD9D1 /* RCTAnimatedImage.m in Sources */, 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */, 139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */, + 681D33FD228807A1003CD9D1 /* RCTUIImageViewAnimated.m in Sources */, 59AB092B1EDE5DD1009F97B5 /* RCTImageShadowView.m in Sources */, 13EF7F7F1BC825B1003F47DD /* RCTLocalAssetImageLoader.m in Sources */, 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */, diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index e2f221ba85..7cea4625f1 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -14,6 +14,7 @@ #import #import +#import "RCTUIImageViewAnimated.h" #import "RCTImageBlurUtils.h" #import "RCTImageLoader.h" #import "RCTImageUtils.h" @@ -79,7 +80,7 @@ static NSDictionary *onLoadParamsForSource(RCTImageSource *source) // Whether the latest change of props requires the image to be reloaded BOOL _needsReload; - UIImageView *_imageView; + RCTUIImageViewAnimated *_imageView; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -96,7 +97,7 @@ static NSDictionary *onLoadParamsForSource(RCTImageSource *source) selector:@selector(clearImageIfDetached) name:UIApplicationDidEnterBackgroundNotification object:nil]; - _imageView = [[UIImageView alloc] init]; + _imageView = [[RCTUIImageViewAnimated alloc] init]; _imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self addSubview:_imageView]; } diff --git a/Libraries/Image/RCTUIImageViewAnimated.h b/Libraries/Image/RCTUIImageViewAnimated.h new file mode 100644 index 0000000000..4afe90a64e --- /dev/null +++ b/Libraries/Image/RCTUIImageViewAnimated.h @@ -0,0 +1,12 @@ +/** + * 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. + */ + +#import "RCTAnimatedImage.h" + +@interface RCTUIImageViewAnimated : UIImageView + +@end diff --git a/Libraries/Image/RCTUIImageViewAnimated.m b/Libraries/Image/RCTUIImageViewAnimated.m new file mode 100644 index 0000000000..b47db503d7 --- /dev/null +++ b/Libraries/Image/RCTUIImageViewAnimated.m @@ -0,0 +1,323 @@ +/** + * 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. + */ + +#import "RCTUIImageViewAnimated.h" + +#import +#import + +static NSUInteger RCTDeviceTotalMemory() { + return (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; +} + +static NSUInteger RCTDeviceFreeMemory() { + mach_port_t host_port = mach_host_self(); + mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t); + vm_size_t page_size; + vm_statistics_data_t vm_stat; + kern_return_t kern; + + kern = host_page_size(host_port, &page_size); + if (kern != KERN_SUCCESS) return 0; + kern = host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size); + if (kern != KERN_SUCCESS) return 0; + return vm_stat.free_count * page_size; +} + +@interface RCTUIImageViewAnimated () + +@property (nonatomic, assign) NSUInteger maxBufferSize; +@property (nonatomic, strong, readwrite) UIImage *currentFrame; +@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; +@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount; +@property (nonatomic, assign) NSUInteger totalFrameCount; +@property (nonatomic, assign) NSUInteger totalLoopCount; +@property (nonatomic, strong) UIImage *animatedImage; +@property (nonatomic, strong) NSMutableDictionary *frameBuffer; +@property (nonatomic, assign) NSTimeInterval currentTime; +@property (nonatomic, assign) BOOL bufferMiss; +@property (nonatomic, assign) NSUInteger maxBufferCount; +@property (nonatomic, strong) NSOperationQueue *fetchQueue; +@property (nonatomic, strong) dispatch_semaphore_t lock; +@property (nonatomic, assign) CGFloat animatedImageScale; +@property (nonatomic, strong) CADisplayLink *displayLink; + +@end + +@implementation RCTUIImageViewAnimated + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.lock = dispatch_semaphore_create(1); + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + + } + return self; +} + +- (void)resetAnimatedImage +{ + self.animatedImage = nil; + self.totalFrameCount = 0; + self.totalLoopCount = 0; + self.currentFrame = nil; + self.currentFrameIndex = 0; + self.currentLoopCount = 0; + self.currentTime = 0; + self.bufferMiss = NO; + self.maxBufferCount = 0; + self.animatedImageScale = 1; + [_fetchQueue cancelAllOperations]; + _fetchQueue = nil; + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + [_frameBuffer removeAllObjects]; + _frameBuffer = nil; + dispatch_semaphore_signal(self.lock); +} + +- (void)setImage:(UIImage *)image +{ + if (self.image == image) { + return; + } + + if ([image respondsToSelector:@selector(animatedImageFrameAtIndex:)]) { + [self stop]; + [self resetAnimatedImage]; + + NSUInteger animatedImageFrameCount = ((UIImage *)image).animatedImageFrameCount; + + // Check the frame count + if (animatedImageFrameCount <= 1) { + return; + } + + self.animatedImage = (UIImage *)image; + self.totalFrameCount = animatedImageFrameCount; + + // Get the current frame and loop count. + self.totalLoopCount = self.animatedImage.animatedImageLoopCount; + + self.animatedImageScale = image.scale; + + self.currentFrame = image; + + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame; + dispatch_semaphore_signal(self.lock); + + // Calculate max buffer size + [self calculateMaxBufferCount]; + + if ([self paused]) { + [self start]; + } + + [self.layer setNeedsDisplay]; + } else { + super.image = image; + } +} + +#pragma mark - Private + +- (NSOperationQueue *)fetchQueue +{ + if (!_fetchQueue) { + _fetchQueue = [[NSOperationQueue alloc] init]; + _fetchQueue.maxConcurrentOperationCount = 1; + } + return _fetchQueue; +} + +- (NSMutableDictionary *)frameBuffer +{ + if (!_frameBuffer) { + _frameBuffer = [NSMutableDictionary dictionary]; + } + return _frameBuffer; +} + +- (CADisplayLink *)displayLink +{ + if (!_displayLink) { + __weak __typeof(self) weakSelf = self; + _displayLink = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(displayDidRefresh:)]; + NSString *runLoopMode = [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode]; + } + return _displayLink; +} + +#pragma mark - Animation + +- (void)start +{ + self.displayLink.paused = NO; +} + +- (void)stop +{ + self.displayLink.paused = YES; +} + +- (BOOL)paused +{ + return self.displayLink.isPaused; +} + +- (void)displayDidRefresh:(CADisplayLink *)displayLink +{ + NSTimeInterval duration = displayLink.duration * displayLink.frameInterval; + NSUInteger totalFrameCount = self.totalFrameCount; + NSUInteger currentFrameIndex = self.currentFrameIndex; + NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; + + // Check if we have the frame buffer firstly to improve performance + if (!self.bufferMiss) { + // Then check if timestamp is reached + self.currentTime += duration; + NSTimeInterval currentDuration = [self.animatedImage animatedImageDurationAtIndex:currentFrameIndex]; + if (self.currentTime < currentDuration) { + // Current frame timestamp not reached, return + return; + } + self.currentTime -= currentDuration; + NSTimeInterval nextDuration = [self.animatedImage animatedImageDurationAtIndex:nextFrameIndex]; + if (self.currentTime > nextDuration) { + // Do not skip frame + self.currentTime = nextDuration; + } + } + + // Update the current frame + UIImage *currentFrame; + UIImage *fetchFrame; + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + currentFrame = self.frameBuffer[@(currentFrameIndex)]; + fetchFrame = currentFrame ? self.frameBuffer[@(nextFrameIndex)] : nil; + dispatch_semaphore_signal(self.lock); + BOOL bufferFull = NO; + if (currentFrame) { + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + // Remove the frame buffer if need + if (self.frameBuffer.count > self.maxBufferCount) { + self.frameBuffer[@(currentFrameIndex)] = nil; + } + // Check whether we can stop fetch + if (self.frameBuffer.count == totalFrameCount) { + bufferFull = YES; + } + dispatch_semaphore_signal(self.lock); + self.currentFrame = currentFrame; + self.currentFrameIndex = nextFrameIndex; + self.bufferMiss = NO; + [self.layer setNeedsDisplay]; + } else { + self.bufferMiss = YES; + } + + // Update the loop count when last frame rendered + if (nextFrameIndex == 0 && !self.bufferMiss) { + // Update the loop count + self.currentLoopCount++; + // if reached the max loop count, stop animating, 0 means loop indefinitely + NSUInteger maxLoopCount = self.totalLoopCount; + if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) { + [self stop]; + return; + } + } + + // Check if we should prefetch next frame or current frame + NSUInteger fetchFrameIndex; + if (self.bufferMiss) { + // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame + fetchFrameIndex = currentFrameIndex; + } else { + // Or, most cases, the decode speed is faster than render speed, we fetch next frame + fetchFrameIndex = nextFrameIndex; + } + + if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { + // Prefetch next frame in background queue + UIImage *animatedImage = self.animatedImage; + NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + UIImage *frame = [animatedImage animatedImageFrameAtIndex:fetchFrameIndex]; + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + self.frameBuffer[@(fetchFrameIndex)] = frame; + dispatch_semaphore_signal(self.lock); + }]; + [self.fetchQueue addOperation:operation]; + } +} + +#pragma mark - CALayerDelegate + +- (void)displayLayer:(CALayer *)layer +{ + if (_currentFrame) { + layer.contentsScale = self.animatedImageScale; + layer.contents = (__bridge id)_currentFrame.CGImage; + } +} + +#pragma mark - Util + +- (void)calculateMaxBufferCount +{ + NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage); + if (bytes == 0) bytes = 1024; + + NSUInteger max = 0; + if (self.maxBufferSize > 0) { + max = self.maxBufferSize; + } else { + // Calculate based on current memory, these factors are by experience + NSUInteger total = RCTDeviceTotalMemory(); + NSUInteger free = RCTDeviceFreeMemory(); + max = MIN(total * 0.2, free * 0.6); + } + + NSUInteger maxBufferCount = (double)max / (double)bytes; + if (!maxBufferCount) { + // At least 1 frame + maxBufferCount = 1; + } + + self.maxBufferCount = maxBufferCount; +} + +#pragma mark - Lifecycle + +- (void)dealloc +{ + // Removes the display link from all run loop modes. + [_displayLink invalidate]; + _displayLink = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +} + +- (void)didReceiveMemoryWarning:(NSNotification *)notification +{ + [_fetchQueue cancelAllOperations]; + [_fetchQueue addOperationWithBlock:^{ + NSNumber *currentFrameIndex = @(self.currentFrameIndex); + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + NSArray *keys = self.frameBuffer.allKeys; + // only keep the next frame for later rendering + for (NSNumber * key in keys) { + if (![key isEqualToNumber:currentFrameIndex]) { + [self.frameBuffer removeObjectForKey:key]; + } + } + dispatch_semaphore_signal(self.lock); + }]; +} + +@end