iOS GIF动画加载框架-FLAnimatedImage解读
FLAnimatedImage 是由Flipboard开源的iOS平台上播放GIF动画的一个优秀解决方案,在内存占用和播放体验都有不错的表现。
本文章主要是介绍FLAnimatedImage框架的GIF动画加载和播放流程,旨在说明流程和主要细节点,大家可以参考流程进行源码解读并调试,相信可以得到大量有用信息。
文章不免有不足或者错误之处,请大家在下方评论指出,我会尽快修正 l-(>-<)-l 。
FLAnimatedImage简单流程图
FLAnimatedImage项目的流程比较简单,FLAnimatedImage就是负责GIF数据的处理,然后提供给FLAnimatedImageView一个UIImage对象。FLAnimatedImageView拿到UIImage对象显示出来就可以了。

FLAnimatedImage使用
使用FLAnimatedImage处理GIF动画数据,使用FLAnimatedImageView展示FLAnimatedImage处理后的动画数据。
- 使用NSData初始化FLAnimatedImage,然后将FLAnimatedImage赋值给FLAnimatedImageView
if (!self.imageView1) {
self.imageView1 = [[FLAnimatedImageView alloc] init];
self.imageView1.contentMode = UIViewContentModeScaleAspectFill;
self.imageView1.clipsToBounds = YES;
}
[self.view addSubview:self.imageView1];
self.imageView1.frame = CGRectMake(0.0, 120.0, self.view.bounds.size.width, 447.0);
NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"gif"];
NSData *data1 = [NSData dataWithContentsOfURL:url1];
FLAnimatedImage *animatedImage1 = [FLAnimatedImage animatedImageWithGIFData:data1];
self.imageView1.animatedImage = animatedImage1;
- 使用URL初始化FLAnimatedImage,然后将FLAnimatedImage赋值给FLAnimatedImageView
if (!self.imageView2) {
self.imageView2 = [[FLAnimatedImageView alloc] init];
self.imageView2.contentMode = UIViewContentModeScaleAspectFill;
self.imageView2.clipsToBounds = YES;
}
[self.view addSubview:self.imageView2];
self.imageView2.frame = CGRectMake(0.0, 577.0, 379.0, 447.0);
NSURL *url2 = [NSURL URLWithString:@"https://cloud.githubusercontent.com/assets/1567433/10417835/1c97e436-7052-11e5-8fb5-69373072a5a0.gif"];
[self loadAnimatedImageWithURL:url2 completion:^(FLAnimatedImage *animatedImage) {
self.imageView2.animatedImage = animatedImage;
}];
FLAnimatedImage项目代码结构
FLAnimatedImage项目采用了“生产者和消费者”模型来处理这个GIF动画的播放问题。一个线程负责生产数据,另一个线程负责消费数据。生产者FLAnimatedImage负责提供帧UIImage对象,消费者FLAnimatedImageView负责显示该UIImage对象。

FLAnimatedImage接口
@property (nonatomic, strong, readonly) UIImage *posterImage;//GIF动画的封面帧图片
@property (nonatomic, assign, readonly) CGSize size; //GIF动画的封面帧图片的尺寸
@property (nonatomic, assign, readonly) NSUInteger loopCount; //GIF动画的循环播放次数
@property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // GIF动画中的每帧图片的显示时间集合
@property (nonatomic, assign, readonly) NSUInteger frameCount; //GIF动画的帧数量
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; //当前被缓存的帧图片的总数量
@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // 允许缓存多少帧图片
// Intended to be called from main thread synchronously; will return immediately.
// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling.
// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache.
// 取出对应索引的帧图片
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index;
// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size
// 计算该帧图片的尺寸
+ (CGSize)sizeForImage:(id)image;
// 初始化方法
// On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged.
- (instancetype)initWithAnimatedGIFData:(NSData *)data;
// Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default.
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER;
+ (instancetype)animatedImageWithGIFData:(NSData *)data;
//初始化数据
@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only
FLAnimatedImage解析
- 关键方法 初始化解析
a、对传进来的数据进行合法性判断,至少不能为nil。
b、初始化对应的变量,用于存储各类辅助数据。
c、将传进来的数据处理成图片数据,根据 kCGImageSourceShouldCache 的官方文档描述 Whether the image should be cached in a decoded form. The value of this key must be a CFBoolean value. The default value is kCFBooleanFalse in 32-bit, kCFBooleanTrue in 64-bit. 所以设置 kCGImageSourceShouldCache为NO,可以避免系统对图片进行缓存,
d、从数据中读取图片类型,判断该图片是不是GIF动画类型。
e、读取GIF动画中的动画信息,包括动画循环次数,有几帧图片等。
f、遍历GIF动画中的所有帧图片,取出并保存帧图片的播放信息,设置GIF动画的封面帧图片
g、根据设置或者GIF动画的占用内存大小,与缓存策略对比,确认缓存策略。
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled
{
// 1、进行数据合法性判断
BOOL hasData = ([data length] > 0);
if (!hasData) {
FLLog(FLLogLevelError, @"No animated GIF data supplied.");
return nil;
}
self = [super init];
if (self) {
// 2、初始化对应的变量
// Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides.
// Keep a strong reference to `data` and expose it read-only publicly.
// However, we will use the `_imageSource` as handler to the image data throughout our life cycle.
_data = data;
_predrawingEnabled = isPredrawingEnabled;
// Initialize internal data structures
_cachedFramesForIndexes = [[NSMutableDictionary alloc] init];//key->帧图片在GIF动画的索引位置 value->单帧图片
_cachedFrameIndexes = [[NSMutableIndexSet alloc] init];//缓存的帧图片在GIF动画的索引位置集合
_requestedFrameIndexes = [[NSMutableIndexSet alloc] init];//需要生产者生产的的帧图片的索引位置
// 3、创建图片数据
// Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`.
_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;
}
// 4、取出图片类型,判断是否是GIF动画
// 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;
}
// 5、取出GIF动画信息
// Get `LoopCount`
// Note: 0 means repeating the animation indefinitely.
// Image properties example:
// {
// FileSize = 314446;
// "{GIF}" = {
// HasGlobalColorMap = 1;
// LoopCount = 0;
// };
// }
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
//获取GIF动画循环次数
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
// Iterate through frame images
//遍历图片
size_t imageCount = CGImageSourceGetCount(_imageSource);
NSUInteger skippedFrameCount = 0;//用于记录GIF动画中异常帧的数量
NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];//记录GIF动画中每帧图片的显示时间
for (size_t i = 0; i < imageCount; i++) {
@autoreleasepool {
// 6、取出帧图片
//Return the image at `index' in the image source `isrc'.
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
// 取出的第一张图片为GIF动画的封面图片
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];
}
// 7、取出帧图片的信息
// Get `DelayTime`
// Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`.
// Frame properties example:
// {
// ColorModel = RGB;
// Depth = 8;
// PixelHeight = 960;
// PixelWidth = 640;
// "{GIF}" = {
// DelayTime = "0.4";
// UnclampedDelayTime = "0.4";
// };
// }
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
// 8、取出帧图片的展示时间
// 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];
}
// If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value.
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
if (!delayTime) {
if (i == 0) {
FLLog(FLLogLevelInfo, @"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties);
delayTime = @(kDelayTimeIntervalDefault);
} else {
FLLog(FLLogLevelInfo, @"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties);
delayTime = delayTimesForIndexesMutable[@(i - 1)];
}
}
// Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility.
// To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO.
if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum);
delayTime = @(kDelayTimeIntervalDefault);
}
delayTimesForIndexesMutable[@(i)] = delayTime;
} else {
skippedFrameCount++;
FLLog(FLLogLevelInfo, @"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef);
}
CFRelease(frameImageRef);
} else {
skippedFrameCount++;
FLLog(FLLogLevelInfo, @"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, _imageSource);
}
}
}
//帧图片展示时间的数组
_delayTimesForIndexes = [delayTimesForIndexesMutable copy];
//GIF动画有多少帧图片
_frameCount = imageCount;
if (self.frameCount == 0) {
FLLog(FLLogLevelInfo, @"Failed to create any valid frames for GIF with properties %@", imageProperties);
return nil;
} else if (self.frameCount == 1) {
// Warn when we only have a single frame but return a valid GIF.
FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties);
} else {
// We have multiple frames, rock on!
}
// 9、GIF动画缓存策略
// If no value is provided, select a default based on the GIF.
if (optimalFrameCacheSize == 0) {
// Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size.
// It's only dependent on the image size & number of frames and never changes.
// 图片的每行字节大小*高*图片数量/1M的字节 = GIF大小(M)
// 根据GIF图的大小和缓存策略判断需要缓存的单帧图片数量
//GIF动画的占用内存大小与FLAnimatedImageDataSizeCategory的方案比较,确认缓存策略
CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (self.frameCount - skippedFrameCount) / MEGABYTE;
if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) {
_frameCacheSizeOptimal = self.frameCount;
} else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) {
// This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames.
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault;
} else {
// The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning.
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory;
}
} else {
// Use the provided value.
_frameCacheSizeOptimal = optimalFrameCacheSize;
}
// In any case, cap the optimal cache size at the frame count.
// _frameCacheSizeOptimal 不能大于 self.frameCount
// 确认最佳的GIF动画的帧图片缓存数量
_frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount);
// Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`.
_allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)];
// See the property declarations for descriptions.
//成为FLWeakProxy的代理
_weakProxy = (id)[FLWeakProxy weakProxyForObject:self];
// Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone.
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
@synchronized(allAnimatedImagesWeak) {
[allAnimatedImagesWeak addObject:self];
}
}
return self;
}
- 关键方法 取UIImage对象
a、对索引位置进行判断,避免出现越界情况
b、记录当前取出的帧图片的索引位置
c、根据缓存策略判断接下来需要生产的帧图片索引,正常是当前显示帧图片之后的帧图片的索引。
d、根据需要生产的帧图片索引生产帧图片
e、取出对应的帧图片
f、根据缓存策略清缓存
// See header for more details.
// Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size).
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index
{
// Early return if the requested index is beyond bounds.
// Note: We're comparing an index with a count and need to bail on greater than or equal to.
// 1、索引位置判断
if (index >= self.frameCount) {
FLLog(FLLogLevelWarn, @"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self);
return nil;
}
// Remember requested frame index, this influences what we should cache next.
// 2、记录当前要生产的帧图片在GIF动画中的索引位置
self.requestedFrameIndex = index;
#if defined(DEBUG) && DEBUG
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) {
[self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index];
}
#endif
// Quick check to avoid doing any work if we already have all possible frames cached, a common case.
// 3、判断GIF动画的帧图片的是否全部缓存下来了,因为有可能缓存策略是缓存所有的帧图片
if ([self.cachedFrameIndexes count] < self.frameCount) {
// If we have frames that should be cached but aren't and aren't requested yet, request them.
// Exclude existing cached frames, frames already requested, and specially cached poster image.
// 4、根据缓存策略得到接下来需要缓存的帧图片索引,
NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache];
// 5、除去已经缓存下来的帧图片索引
[frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes];
[frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes];
[frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex];
NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy];
// Asynchronously add frames to our cache.
if ([frameIndexesToAddToCache count] > 0) {
// 6、生产帧图片
[self addFrameIndexesToCache:frameIndexesToAddToCache];
}
}
// Get the specified image.
// 7、取出帧图片
UIImage *image = self.cachedFramesForIndexes[@(index)];
// Purge if needed based on the current playhead position.
// 8、根据缓存策略清缓存
[self purgeFrameCacheIfNeeded];
return image;
}
- 其他关键方法简单介绍
// Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping.
// 生产帧图片
- (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache;
// 取出GIF动画的帧图片
- (UIImage *)imageAtIndex:(NSUInteger)index;
// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread.
// On success, the returned object is a new `UIImage` instance with the same content as the one passed in.
// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged.
// First inspired by & good Karma to: https://gist.github.com/steipete/1144242
// 解码图片
+ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw;
FLAnimatedImageView接口
// FLAnimatedImageView是UIImageView的子类,完全兼容UIImageView的各个方法。
// An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed.
// The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`.
// It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`.
// Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`.
//
@interface FLAnimatedImageView : UIImageView
// Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`.
// And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`.
@property (nonatomic, strong) FLAnimatedImage *animatedImage;//设置GIF动画数据
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);//GIF动画播放一次之后的回调Block
@property (nonatomic, strong, readonly) UIImage *currentFrame;//GIF动画当前显示的帧图片
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;//GIF动画当前显示的帧图片索引
// The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes.
// To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode.
@property (nonatomic, copy) NSString *runLoopMode;
@end
FLAnimatedImageView解析
- 关键方法 设置FLAnimatedImage对象解析
a、判断新旧FLAnimatedImage对象是否一致,一致就不需要继续操作了
b、设置GIF动画的封面帧图片,当前帧索引,GIF动画的循环播放次数,播放时间累加器
c、更新是否发起动画的标志位,判断是否启动GIF动画
d、刷新View的layer
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
//新设置的GIF动画数据和当前的数据不一致
if (![_animatedImage isEqual:animatedImage]) {
if (animatedImage) {
// Clear out the image.
super.image = nil;
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
super.highlighted = NO;
// UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
//确保UIImageView的content size 大小来自 animated image
[self invalidateIntrinsicContentSize];
} else {
// Stop animating before the animated image gets cleared out.
// animatedImage为nil,需要清空当前动画图片
[self stopAnimating];
}
_animatedImage = animatedImage;
self.currentFrame = animatedImage.posterImage;//GIF动画的封面帧图片
self.currentFrameIndex = 0;//当前的帧图片索引
//设置GIF动画的循环播放次数
if (animatedImage.loopCount > 0) {
self.loopCountdown = animatedImage.loopCount;
} else {
self.loopCountdown = NSUIntegerMax;
}
//播放时间累加器
self.accumulator = 0.0;
// Start animating after the new animated image has been set.
[self updateShouldAnimate];
if (self.shouldAnimate) {
[self startAnimating];
}
[self.layer setNeedsDisplay];
}
}
- 关键方法 设置CADisplayLink的frameInterval
- (void)startAnimating
{
//使用CADisplayLink来播放GIF动画
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
// 1、frameInterval : Defines how many display frames must pass between each time the display link fires.
// 2、先求出gif中每帧图片的播放时间,求出这些播放时间的最大公约数,
// 3、将这个最大公约数*刷新速率,再与1比取最大值,该值作为frameInterval。
// 4、将GIF动画的每帧图片显示时间除以帧显示时间的最大公约数,得到单位时间内GIF动画的每个帧显示时间的比例,然后再乘以屏幕刷新速率kDisplayRefreshRate作为displayLink.frameInterval,正好可以用displayLink调用刷新方法的频率来保证GIF动画的帧图片展示时间 frame delays的间隔比例,使GIF动画的效果能够正常显示。
self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
self.displayLink.paused = NO;
} else {
[super startAnimating];
}
}
- 关键方法 播放GIF动画
该方法关键点在于accumulator累加器的使用和displayLink.frameInterval的计算,涉及一些简单的数学过程
- (void)displayDidRefresh:(CADisplayLink *)displayLink
{
// If for some reason a wild call makes it through when we shouldn't be animating, bail.
// Early return!
if (!self.shouldAnimate) {
FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
return;
}
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;
if (self.needsDisplayWhenImageBecomesAvailable) {
[self.layer setNeedsDisplay];
self.needsDisplayWhenImageBecomesAvailable = NO;
}
//frameInterval:Defines how many display frames must pass between each time the display link fires
//duration :duration of the display frame
//displayLink.duration * displayLink.frameInterval是每个display link fires之间的时间间隔
self.accumulator += displayLink.duration * displayLink.frameInterval;
//从前面的startAnimating方法中displayLink.frameInterval的计算过程可以知道,
//GIF动画中的帧图片的展示时间都是delayTime都是displayLink.duration * displayLink.frameInterval的倍数关系,
//也就是说一个GIF动画帧图片的展示时间至少是一个display link fires的时间间隔。
//以下数据是使用FLAnimatedImage的Demo项目的第一个GIF动画的播放信息打印出来的。
//按照Demo中的打印数据来说,第0帧图片的展示时间是14个display link fires的时间间隔,而1,2,3帧图片都是只有一个display link fires的时间间隔。
//所以累加器self.accumulator的意义在于累加display link fires的时间间隔,并与帧图片的delayTime做比较,如果小于delayTime说明该帧图片还需要继续展示,否则该帧图片结束展示。
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.050000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.100000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.150000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.200000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.250000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.300000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.350000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.400000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.450000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.500000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.550000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.600000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.650000, delayTime-->0.700000
// currentFrameIndex-->0, duration--->0.016667, frameInterval-->3, accumulator-->0.700000, delayTime-->0.700000
// currentFrameIndex-->1, duration--->0.016667, frameInterval-->3, accumulator-->0.050000, delayTime-->0.050000
// currentFrameIndex-->2, duration--->0.016667, frameInterval-->3, accumulator-->0.050000, delayTime-->0.050000
// currentFrameIndex-->3, duration--->0.016667, frameInterval-->3, accumulator-->0.050000, delayTime-->0.050000
// 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) {
// 播放到结尾,循环次数减1
// If we've looped the number of times that this animated image describes, stop looping.
self.loopCountdown--;
if (self.loopCompletionBlock) {
self.loopCompletionBlock(self.loopCountdown);
}
// 循环次数为0,停止播放,退出方法
if (self.loopCountdown == 0) {
[self stopAnimating];
return;
}
//重置帧图片索引,继续从头开始播放gif动画
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 {
FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
#if defined(DEBUG) && DEBUG
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
[self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)displayLink.duration * displayLink.frameInterval];
}
#endif
}
} else {
//取不到需要的信息直接开始下一张图片播放
self.currentFrameIndex++;
}
}
总结
- FLAnimatedImage就是负责生产数据是生产者。
- FLAnimatedImageView负责消费数据是消费者。
参考
https://developer.apple.com/library/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/
http://engineering.flipboard.com/2014/05/animated-gif/
https://github.com/Flipboard/FLAnimatedImage
http://blog.ibireme.com/2015/11/02/ios_image_tips/
http://blog.ibireme.com/2015/11/02/mobile_image_benchmark/
作者:要上班的斌哥
链接:https://www.jianshu.com/p/10644979f01c
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
简介
FLAnimatedImage 是 Flipboard 团队开发的在它们 App 中渲染 GIF 图片使用的库。 后来 Flipboard 将 FLAnimatedImage 开源出来供大家使用。本文章主要是介绍FLAnimatedImage框架的GIF动画加载和播放流程,旨在说明流程和主要细节点。
ios原有加载缺陷分析
大家知道在 iOS 中处理过 GIF 图片, 如果通过原生系统提供的能力, 可能只有两种方式。 并且这两种方式都不是专门针对于 GIF 的解决方案,更像是一种 hack。
第一种方式, UIImage 虽然提供了一种创建连续切换的动画图片的能力, 但这个能力更像是为了那些简单动画而服务的。 比如加载数据时候显示的 loading 图片。 如果将 GIF 图片通过这种能力来显示,会带来诸多问题。
第二种方式,可能是大家用的最多的了。 就是创建一个 UIWebView 然后在这里面把 GIF 显示出来。 但从原理上来想, UIWebView 并不是为了显示 GIF 图片而生的。
gif渲染原理分析
为什么说 FLAnimatedImage 相对于 iOS 原生的几种 hack 方式更趋近于最佳实践呢? 咱们简单聊聊 FLAnimatedImage 渲染 GIF 图片的原理。FLAnimatedImage 会有两个线程同时在运转。 其中一个线程负责渲染 GIF 的每一帧的图片内容(所谓的渲染,大体上就是加载 GIF 文件数据,然后抽取出来当前需要哪一帧)。这个加载图片的过程是在异步线程进行的。
然后 FLAnimatedImage 会有一个内存区域专门放置这些渲染好的帧。 这时候,在主线程中的 ImageView 会根据当前需要,从这个内存区域中读取相应的帧。这是一个典型的生产者-消费者问题。
FLAnimatedImage
FLAnimatedImage项目的流程比较简单,FLAnimatedImage就是负责GIF数据的处理,然后提供给FLAnimatedImageView一个UIImage对象。FLAnimatedImageView拿到UIImage对象显示出来就可以了。
实例
使用FLAnimatedImage 加载GIF,有三个关键函数:使用FLAnimatedImage处理GIF动画数据,使用FLAnimatedImageView展示FLAnimatedImage处理后的动画数据。
- 使用NSData初始化FLAnimatedImage,然后将FLAnimatedImage赋值给FLAnimatedImageView
if (!self.imageView1) {
self.imageView1 = [[FLAnimatedImageView alloc] init];
self.imageView1.contentMode = UIViewContentModeScaleAspectFill;
self.imageView1.clipsToBounds = YES;
}
[self.view addSubview:self.imageView1];
self.imageView1.frame = CGRectMake(0.0, 120.0, self.view.bounds.size.width, 447.0);
NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"gif"];
NSData *data1 = [NSData dataWithContentsOfURL:url1];
FLAnimatedImage *animatedImage1 = [FLAnimatedImage animatedImageWithGIFData:data1];
self.imageView1.animatedImage = animatedImage1;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
2.使用URL初始化FLAnimatedImage,然后将FLAnimatedImage赋值给FLAnimatedImageView
if (!self.imageView2) {
self.imageView2 = [[FLAnimatedImageView alloc] init];
self.imageView2.contentMode = UIViewContentModeScaleAspectFill;
self.imageView2.clipsToBounds = YES;
}
[self.view addSubview:self.imageView2];
self.imageView2.frame = CGRectMake(0.0, 577.0, 379.0, 447.0);
NSURL *url2 = [NSURL URLWithString:@"https://cloud.githubusercontent.com/assets/1567433/10417835/1c97e436-7052-11e5-8fb5-69373072a5a0.gif"];
[self loadAnimatedImageWithURL:url2 completion:^(FLAnimatedImage *animatedImage) {
self.imageView2.animatedImage = animatedImage;
}];
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
FLAnimatedImage项目介绍
FLAnimatedImage项目采用了“生产者和消费者”模型来处理这个GIF动画的播放问题。一个线程负责生产数据,另一个线程负责消费数据。生产者FLAnimatedImage负责提供帧UIImage对象,消费者FLAnimatedImageView负责显示该UIImage对象。
FLAnimatedImage接口介绍
@property (nonatomic, strong, readonly) UIImage *posterImage;//GIF动画的封面帧图片
@property (nonatomic, assign, readonly) CGSize size; //GIF动画的封面帧图片的尺寸
@property (nonatomic, assign, readonly) NSUInteger loopCount; //GIF动画的循环播放次数
@property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // GIF动画中的每帧图片的显示时间集合
@property (nonatomic, assign, readonly) NSUInteger frameCount; //GIF动画的帧数量
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; //当前被缓存的帧图片的总数量
@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // 允许缓存多少帧图片
// Intended to be called from main thread synchronously; will return immediately.
// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling.
// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache.
// 取出对应索引的帧图片
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index;
// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size
// 计算该帧图片的尺寸
+ (CGSize)sizeForImage:(id)image;
// 初始化方法
// On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged.
- (instancetype)initWithAnimatedGIFData:(NSData *)data;
// Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default.
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER;
+ (instancetype)animatedImageWithGIFData:(NSData *)data;
//初始化数据
@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
FLAnimatedImage函数解析
关键方法解析
a、对传进来的数据进行合法性判断,至少不能为nil。
b、初始化对应的变量,用于存储各类辅助数据。
c、将传进来的数据处理成图片数据,其中设置。kCGImageSourceShouldCache为NO,可以避免系统对图片进行缓存。
d、从数据中读取图片类型,判断该图片是不是GIF动画类型。
e、读取GIF动画中的动画信息,包括动画循环次数,有几帧图片等。
f、遍历GIF动画中的所有帧图片,取出并保存帧图片的播放信息,设置GIF动画的封面帧图片
g、根据设置或者GIF动画的占用内存大小,与缓存策略对比,确认缓存策略。
FLAnimatedImageView接口
@interface FLAnimatedImageView : UIImageView
// Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`.
// And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`.
@property (nonatomic, strong) FLAnimatedImage *animatedImage;//设置GIF动画数据
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);//GIF动画播放一次之后的回调Block
@property (nonatomic, strong, readonly) UIImage *currentFrame;//GIF动画当前显示的帧图片
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;//GIF动画当前显示的帧图片索引
// The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes.
// To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode.
@property (nonatomic, copy) NSString *runLoopMode;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
FLAnimatedImageView解析:
关键对象解析:
a、判断新旧FLAnimatedImage对象是否一致,一致就不需要继续操作了
b、设置GIF动画的封面帧图片,当前帧索引,GIF动画的循环播放次数,播放时间累加器
c、更新是否发起动画的标志位,判断是否启动GIF动画
d、刷新View的layer
参考:http://swiftcafe.io/2016/12/08/fl-image/
http://engineering.flipboard.com/2014/05/animated-gif/

浙公网安备 33010602011771号