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/