iOS GIF动画加载框架-FLAnimatedImage解读

FLAnimatedImage 是由Flipboard开源的iOS平台上播放GIF动画的一个优秀解决方案,在内存占用和播放体验都有不错的表现。
本文章主要是介绍FLAnimatedImage框架的GIF动画加载和播放流程,旨在说明流程和主要细节点,大家可以参考流程进行源码解读并调试,相信可以得到大量有用信息。
文章不免有不足或者错误之处,请大家在下方评论指出,我会尽快修正 l-(>-<)-l 。

FLAnimatedImage简单流程图

FLAnimatedImage项目的流程比较简单,FLAnimatedImage就是负责GIF数据的处理,然后提供给FLAnimatedImageView一个UIImage对象。FLAnimatedImageView拿到UIImage对象显示出来就可以了。


FLAnimatedImage简单流程图

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代码结构

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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容