Skip to content

Commit 3b67bfa

Browse files
ericlewisfacebook-github-bot
authored andcommitted
Animated image improvements (facebook#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: facebook#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
1 parent 690e85d commit 3b67bfa

File tree

7 files changed

+558
-101
lines changed

7 files changed

+558
-101
lines changed

Libraries/Image/RCTAnimatedImage.h

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
@protocol RCTAnimatedImage <NSObject>
11+
@property (nonatomic, assign, readonly) NSUInteger animatedImageFrameCount;
12+
@property (nonatomic, assign, readonly) NSUInteger animatedImageLoopCount;
13+
14+
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
15+
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
16+
17+
@end
18+
19+
@interface RCTAnimatedImage : UIImage <RCTAnimatedImage>
20+
21+
@end

Libraries/Image/RCTAnimatedImage.m

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTAnimatedImage.h"
9+
10+
@interface RCTGIFCoderFrame : NSObject
11+
12+
@property (nonatomic, assign) NSUInteger index;
13+
@property (nonatomic, assign) NSTimeInterval duration;
14+
15+
@end
16+
17+
@implementation RCTGIFCoderFrame
18+
@end
19+
20+
@implementation RCTAnimatedImage {
21+
CGImageSourceRef _imageSource;
22+
CGFloat _scale;
23+
NSUInteger _loopCount;
24+
NSUInteger _frameCount;
25+
NSArray<RCTGIFCoderFrame *> *_frames;
26+
}
27+
28+
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale
29+
{
30+
if (self = [super init]) {
31+
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
32+
if (!imageSource) {
33+
return nil;
34+
}
35+
36+
BOOL framesValid = [self scanAndCheckFramesValidWithSource:imageSource];
37+
if (!framesValid) {
38+
CFRelease(imageSource);
39+
return nil;
40+
}
41+
42+
_imageSource = imageSource;
43+
44+
// grab image at the first index
45+
UIImage *image = [self animatedImageFrameAtIndex:0];
46+
if (!image) {
47+
return nil;
48+
}
49+
self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation];
50+
51+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
52+
}
53+
54+
return self;
55+
}
56+
57+
- (BOOL)scanAndCheckFramesValidWithSource:(CGImageSourceRef)imageSource
58+
{
59+
if (!imageSource) {
60+
return NO;
61+
}
62+
NSUInteger frameCount = CGImageSourceGetCount(imageSource);
63+
NSUInteger loopCount = [self imageLoopCountWithSource:imageSource];
64+
NSMutableArray<RCTGIFCoderFrame *> *frames = [NSMutableArray array];
65+
66+
for (size_t i = 0; i < frameCount; i++) {
67+
RCTGIFCoderFrame *frame = [[RCTGIFCoderFrame alloc] init];
68+
frame.index = i;
69+
frame.duration = [self frameDurationAtIndex:i source:imageSource];
70+
[frames addObject:frame];
71+
}
72+
73+
_frameCount = frameCount;
74+
_loopCount = loopCount;
75+
_frames = [frames copy];
76+
77+
return YES;
78+
}
79+
80+
- (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source
81+
{
82+
NSUInteger loopCount = 1;
83+
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil);
84+
NSDictionary *gifProperties = imageProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary];
85+
if (gifProperties) {
86+
NSNumber *gifLoopCount = gifProperties[(__bridge NSString *)kCGImagePropertyGIFLoopCount];
87+
if (gifLoopCount != nil) {
88+
loopCount = gifLoopCount.unsignedIntegerValue;
89+
}
90+
}
91+
return loopCount;
92+
}
93+
94+
- (float)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source
95+
{
96+
float frameDuration = 0.1f;
97+
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
98+
if (!cfFrameProperties) {
99+
return frameDuration;
100+
}
101+
NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
102+
NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
103+
104+
NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
105+
if (delayTimeUnclampedProp != nil) {
106+
frameDuration = [delayTimeUnclampedProp floatValue];
107+
} else {
108+
NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
109+
if (delayTimeProp != nil) {
110+
frameDuration = [delayTimeProp floatValue];
111+
}
112+
}
113+
114+
CFRelease(cfFrameProperties);
115+
return frameDuration;
116+
}
117+
118+
- (NSUInteger)animatedImageLoopCount
119+
{
120+
return _loopCount;
121+
}
122+
123+
- (NSUInteger)animatedImageFrameCount
124+
{
125+
return _frameCount;
126+
}
127+
128+
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index
129+
{
130+
if (index >= _frameCount) {
131+
return 0;
132+
}
133+
return _frames[index].duration;
134+
}
135+
136+
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index
137+
{
138+
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
139+
if (!imageRef) {
140+
return nil;
141+
}
142+
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:UIImageOrientationUp];
143+
CGImageRelease(imageRef);
144+
return image;
145+
}
146+
147+
- (void)didReceiveMemoryWarning:(NSNotification *)notification
148+
{
149+
if (_imageSource) {
150+
for (size_t i = 0; i < _frameCount; i++) {
151+
CGImageSourceRemoveCacheAtIndex(_imageSource, i);
152+
}
153+
}
154+
}
155+
156+
- (void)dealloc
157+
{
158+
if (_imageSource) {
159+
CFRelease(_imageSource);
160+
_imageSource = NULL;
161+
}
162+
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
163+
}
164+
165+
@end

Libraries/Image/RCTGIFImageDecoder.m

+5-87
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <QuartzCore/QuartzCore.h>
1212

1313
#import <React/RCTUtils.h>
14+
#import "RCTAnimatedImage.h"
1415

1516
@implementation RCTGIFImageDecoder
1617

@@ -30,96 +31,13 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
3031
resizeMode:(RCTResizeMode)resizeMode
3132
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
3233
{
33-
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
34-
if (!imageSource) {
34+
RCTAnimatedImage *image = [[RCTAnimatedImage alloc] initWithData:imageData scale:scale];
35+
36+
if (!image) {
3537
completionHandler(nil, nil);
3638
return ^{};
3739
}
38-
NSDictionary<NSString *, id> *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL);
39-
CGFloat loopCount = 0;
40-
if ([[properties[(id)kCGImagePropertyGIFDictionary] allKeys] containsObject:(id)kCGImagePropertyGIFLoopCount]) {
41-
loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
42-
if (loopCount == 0) {
43-
// A loop count of 0 means infinite
44-
loopCount = HUGE_VALF;
45-
} else {
46-
// A loop count of 1 means it should repeat twice, 2 means, thrice, etc.
47-
loopCount += 1;
48-
}
49-
}
50-
51-
UIImage *image = nil;
52-
size_t imageCount = CGImageSourceGetCount(imageSource);
53-
if (imageCount > 1) {
54-
55-
NSTimeInterval duration = 0;
56-
NSMutableArray<NSNumber *> *delays = [NSMutableArray arrayWithCapacity:imageCount];
57-
NSMutableArray<id /* CGIMageRef */> *images = [NSMutableArray arrayWithCapacity:imageCount];
58-
for (size_t i = 0; i < imageCount; i++) {
59-
60-
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL);
61-
if (!imageRef) {
62-
continue;
63-
}
64-
if (!image) {
65-
image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
66-
}
67-
68-
NSDictionary<NSString *, id> *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL);
69-
NSDictionary<NSString *, id> *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary];
70-
71-
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
72-
NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime];
73-
if (delayTime == nil) {
74-
if (delays.count == 0) {
75-
delayTime = @(kDelayTimeIntervalDefault);
76-
} else {
77-
delayTime = delays.lastObject;
78-
}
79-
}
80-
81-
const NSTimeInterval kDelayTimeIntervalMinimum = 0.02;
82-
if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) {
83-
delayTime = @(kDelayTimeIntervalDefault);
84-
}
85-
86-
duration += delayTime.doubleValue;
87-
[delays addObject:delayTime];
88-
[images addObject:(__bridge_transfer id)imageRef];
89-
}
90-
CFRelease(imageSource);
91-
92-
NSMutableArray<NSNumber *> *keyTimes = [NSMutableArray arrayWithCapacity:delays.count];
93-
NSTimeInterval runningDuration = 0;
94-
for (NSNumber *delayNumber in delays) {
95-
[keyTimes addObject:@(runningDuration / duration)];
96-
runningDuration += delayNumber.doubleValue;
97-
}
98-
99-
[keyTimes addObject:@1.0];
100-
101-
// Create animation
102-
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
103-
animation.calculationMode = kCAAnimationDiscrete;
104-
animation.repeatCount = loopCount;
105-
animation.keyTimes = keyTimes;
106-
animation.values = images;
107-
animation.duration = duration;
108-
animation.removedOnCompletion = NO;
109-
animation.fillMode = kCAFillModeForwards;
110-
image.reactKeyframeAnimation = animation;
111-
112-
} else {
113-
114-
// Don't bother creating an animation
115-
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
116-
if (imageRef) {
117-
image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
118-
CFRelease(imageRef);
119-
}
120-
CFRelease(imageSource);
121-
}
122-
40+
12341
completionHandler(nil, image);
12442
return ^{};
12543
}

0 commit comments

Comments
 (0)