-
Notifications
You must be signed in to change notification settings - Fork 24.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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: #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
- Loading branch information
1 parent
690e85d
commit 3b67bfa
Showing
7 changed files
with
558 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <UIKit/UIKit.h> | ||
|
||
@protocol RCTAnimatedImage <NSObject> | ||
@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 <RCTAnimatedImage> | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<RCTGIFCoderFrame *> *_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<RCTGIFCoderFrame *> *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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.