FLAnimatedImage iOS平台上播放GIF动画的一个优秀解决方案,支持可变帧间延时、内存内存表现良好、播放流畅等特点。
FLAnimatedImage有两个类:
-
FLAnimatedImage
用来解析、封装GIF图像信息 (GIF帧数、GIF size、播放循环次数、posterImage、帧间延时) -
FLAnimatedImageView
用来控制GIF的播放
FLAnimatedImage
GIF图像信息的解析,关键代码:
关键是获取循环次数、帧间延时delayTimesForIndexesMutable
, 用到了底层的CGImageSourceRef
_imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
(__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
// Early return on failure!
if (!_imageSource) {
FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
return nil;
}
// Early return if not GIF!
CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource);
BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
if (!isGIFData) {
FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
return nil;
}
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
// Iterate through frame images
size_t imageCount = CGImageSourceGetCount(_imageSource);
NSUInteger skippedFrameCount = 0;
NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
for (size_t i = 0; i < imageCount; i++) {
@autoreleasepool {
CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
if (frameImageRef) {
UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
// Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
if (frameImage) {
// Set poster image
if (!self.posterImage) {
_posterImage = frameImage;
// Set its size to proxy our size.
_size = _posterImage.size;
// Remember index of poster image so we never purge it; also add it to the cache.
_posterImageFrameIndex = i;
[self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
[self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
}
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
// Try to use the unclamped delay time; fall back to the normal delay time.
NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
if (!delayTime) {
delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
}
delayTimesForIndexesMutable[@(i)] = delayTime;
} else {
skippedFrameCount++;
}
CFRelease(frameImageRef);
} else {
skippedFrameCount++;
}
}
}
FLAnimatedImage有一个关键接口imageLazilyCachedAtIndex
用于获取某一帧对应的Image。
关键思想是:内存管理、内存警告处理、缓存帧管理、子线程异步加载。
imageLazilyCachedAtIndex
获取某一帧的时候,会进行前面几帧的预加载,如果获取的一帧还没加载完成,那么会返回 nil
值,避免卡顿的情况。
FLAnimatedImageView
FLAnimatedImageView
的职责是绘制GIF动画。
那么如何绘制动画?如何驱动动画的绘制?怎么绘制?
驱动的关键是CADisplayLink
:
- (void)startAnimating
{
if (self.animatedImage) {
// Lazily create the display link.
if (!self.displayLink) {
// It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
// will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
// independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
// link which will lead to the deallocation of both the display link and the weak proxy.
FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
}
// Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
// Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
self.displayLink.paused = NO;
} else {
[super startAnimating];
}
}
注意:其中NSRunLoop
的mode设置, NSDefaultRunLoopMode
时,滑动scrollview时,GIF会暂停播放,NSRunLoopCommonModes
模式是不会暂停。
其中,有一个问题:使用CADisplayLink
如何避免循环引用?
CADisplayLink
的target是retain这个target, 而displayLink会add到主线程的Runloop中,就会形成 Runloop -> CADisplayLink -> self 的引用关系。
解决办法是使用FLWeakProxy
弱引用self, 这样引用关系变成了 Runloop -> CADisplayLink -> WeakProxy, WeakProxy再弱引用self。当self释放时移除CADisplayLink,这样就避免了循环引用。
- (void)dealloc
{
[_displayLink invalidate];
}
绘制
有了驱动,如何绘制?
在CADisplayLink
的回调中:
- (void)displayDidRefresh:(CADisplayLink *)displayLink
实现了loopCount控制、帧Index计数、延时管理(不能播放太快,也不能太慢!)
看源码:
- (void)displayDidRefresh:(CADisplayLink *)displayLink
{
NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
// If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
if (delayTimeNumber) {
NSTimeInterval delayTime = [delayTimeNumber floatValue];
// If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
if (image) {
FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
self.currentFrame = image; //更新当前currentFrame,在绘制的时候使用
if (self.needsDisplayWhenImageBecomesAvailable) {
[self.layer setNeedsDisplay];
self.needsDisplayWhenImageBecomesAvailable = NO;
}
self.accumulator += displayLink.duration * displayLink.frameInterval;
// While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
while (self.accumulator >= delayTime) {
self.accumulator -= delayTime;
self.currentFrameIndex++;
if (self.currentFrameIndex >= self.animatedImage.frameCount) {
// If we've looped the number of times that this animated image describes, stop looping.
self.loopCountdown--;
if (self.loopCompletionBlock) {
self.loopCompletionBlock(self.loopCountdown);
}
if (self.loopCountdown == 0) {
[self stopAnimating];
return;
}
self.currentFrameIndex = 0;
}
// Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
// Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
self.needsDisplayWhenImageBecomesAvailable = YES;
}
} else {
}
} else {
self.currentFrameIndex++;
}
特别注意的是其中while的设计,是为了在本次DisplayLink中拿到正确的currentFrameIndex
。
绘制
非常简单,拿到GIF帧的图片后,直接显示:
- (void)displayLayer:(CALayer *)layer
{
layer.contents = (__bridge id)self.image.CGImage;
}
- (UIImage *)image
{
UIImage *image = nil;
if (self.animatedImage) {
// Initially set to the poster image.
image = self.currentFrame;
} else {
image = super.image;
}
return image;
}