GIF调研之FLAnimatedImage

总体思路:

FLAnimatedImage就是负责gif数据解析,用imageIO解码,根据gif大小制定缓存策略,异步解码。
FLAnimatedImageView负责数据消费,displaylink播放每一帧图片。
抱怨一句:源码好多字段相近例如cacheFramesForIndex和cacheFrameIndexes等等等,无奈对字段脸盲。mmp
本文先介绍FLAnimatedImage


用法:

用法很简单,拿到gifData初始化成FLAnimatedImage赋值给animatedImage。

let animatedImage = FLAnimatedImage.init(animatedGIFData: data)
self.backGroundImageView.animatedImage = animatedImage

FLAnimatedImage

先看下他多如牛毛的属性,否则后面会很蒙。也可以看到后面回来查看

//FLAnimatedImage.h
@property (nonatomic, strong, readonly) UIImage *posterImage; //GIF动画的封面帧图片
@property (nonatomic, assign, readonly) CGSize size; //GIF动画的封面帧图片的尺寸
@property (nonatomic, assign, readonly) NSUInteger loopCount; //GIF动画的循环播放次数,0为无线播放
@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;  // 允许缓存多少帧图片
//FLAnimatedImage.m
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; //最优缓存尺寸
@property (nonatomic, assign, readonly, getter=isPredrawingEnabled) BOOL predrawingEnabled; // 是否预绘制,提高性能
@property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes;
//保存着缓存帧map{“帧index”:“图片”}
@property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // 被缓存了的帧索引例如{@(1),@(2)}
@property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // 被解码了的帧索引集合
@property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet;// 所有帧的索引集合,比如有三帧就是@{@(0),@(1),@(2)}

初始化方法:

- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled

解释:

  • 1 .用ImageIO创建gif图片源CGImageSourceRef,并设置不启用系统缓存。(因为这里要用自己的缓存策略)
  • 2 .用ImageIO获取gif图片属性,_loopCount(循环次数,0为无限循环)、imageCount gif包含的帧数。
  • 3.解析单张图片,获取帧图片CGImageRef,设置封面帧(gif第一帧),封面帧的尺寸,封面帧的UIImage数据,保存到cachedFramesForIndexes、cachedFrameIndexes属性中。获取单帧图片属性delayTime(延迟时间)保存到属性delayTimesForIndexes中。注意这里只是解码了第一帧图片,其他帧只是获取了图片属性,是为了避免CPU过度消耗。
    注:在取延迟时间的时候有个小细节,尽量去取非减速的时间 kCGImagePropertyGIFUnclampedDelayTime,没有再去取kCGImagePropertyGIFDelayTime。这里原因是由于gif为了得到更快的播放速度回把delay设置的很低,有些浏览器会做降速处理。
  • 4.根据gifdata的大小设置缓存机制、属性为frameCacheSizeOptimal,如图。


    未命名文件.png

gif核心代码及注释:

// 容错判断
    BOOL hasData = ([data length] > 0);
    if (!hasData) {
        FLLog(FLLogLevelError, @"No animated GIF data supplied.");
        return nil;
    }
    
    self = [super init];
    if (self) {
        //保存原始data数据
        _data = data;
        //是否预绘制,推升性能
        _predrawingEnabled = isPredrawingEnabled;
        
        // 初始化属性的数组
        _cachedFramesForIndexes = [[NSMutableDictionary alloc] init];
        _cachedFrameIndexes = [[NSMutableIndexSet alloc] init];
        _requestedFrameIndexes = [[NSMutableIndexSet alloc] init];

        //创建图片源
        _imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
                                                   (__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
        //获取图片源
        if (!_imageSource) {
            FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
            return nil;
        }
        //获取图片类型
        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;
        }
        
        // 获取循环次数
        // Note: 0 表示动画无线重复
        // 图片属性字典示例:
        // {
        //     FileSize = 314446;
        //     "{GIF}" = {
        //         HasGlobalColorMap = 1;
        //         LoopCount = 0;
        //     };
        // }
        //获取图片属性字典
        NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
        //获取循环次数
        _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
        // 遍历图像帧
        size_t imageCount = CGImageSourceGetCount(_imageSource);
        NSUInteger skippedFrameCount = 0;
        //创建字典保存单帧图片延迟数据
        NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
        for (size_t i = 0; i < imageCount; i++) {
            @autoreleasepool {
                //获取每一帧图片
                CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
                if (frameImageRef) {
                    //转成UIImage
                    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) {
                        // 设置封面图片
                        if (!self.posterImage) {
                            //保存封面第一帧图片及尺寸
                            _posterImage = frameImage;
                            _size = _posterImage.size;
                            //记住封面索引的位置
                            _posterImageFrameIndex = I;
                            //保存到内存缓存中
                            [self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
                            
                            [self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
                        }
                        
                        // 获取延迟时间
                        // Note: 这里是以秒为单位的‘kcfnumberfloat32类型’
                        // frame属性示例、注意这里指的是单张图片:
                        // {
                        //     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];
                        
                        // 尽量去取非减速的时间kCGImagePropertyGIFUnclampedDelayTime没有再取kCGImagePropertyGIFDelayTime。这里原因是由于gif为了得到更快的播放速度回把delay设置的很低,有些浏览器会做降速处理。
                        NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
                        if (!delayTime) {
                            delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
                        }
                        // 如果我们没有从属性中获取到延迟时间,就取kDelayTimeIntervalDefault或者将第一帧的值保留下来
                        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)];
                            }
                        }
                        // 如果delayTime小于kFLAnimatedImageDelayTimeIntervalMinimum,那么都应设置成kDelayTimeIntervalDefault默认值,这点应该是RFC规定的是不能少于40ms吧。
                        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];
        //存储帧数
        _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) {
            FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties);
        } else {
        }

缓存策略:

//如果没有提供默认值,则根据gif选择默认值
        if (optimalFrameCacheSize == 0) {
            // 计算最优帧缓存大小:试着根据预测的图像尺寸去选择一个较大的缓冲区窗口
            // 这只依赖于图像的大小和帧数
            //  获取gif大小(M),这里用每行的字节数*高度*图片数量/1M的字节
            CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (self.frameCount - skippedFrameCount) / MEGABYTE;
            //根据gif大小判断缓存策略:
            if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) {
                //如果小于10M,所有帧都可以缓存
                _frameCacheSizeOptimal = self.frameCount;
            } else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) {
                //小于75M
                //这个值并不依赖于设备内存,因为如果我们不包吃
                // 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 {
                // 更大
                //预计的大小超过了建立缓存的限制,进入时就设置成低内存模式
                _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory;
            }
        } else {
            // 用提供的缓存模式
            _frameCacheSizeOptimal = optimalFrameCacheSize;
        }
        // 无论如何,在帧数上限制最佳的缓存大小
        _frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount);

取gif对应的帧数

- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index
  • 1.查看是否所有帧都缓存了,如果有的话直接cachedFramesForIndexes中取图片
  • 2.如果没有,保存当前需要解码的帧索引,根据初始化方法确认的缓存策略找到需要缓存的索引集合,这里对需要缓存的定义为从当前解码帧开始往后查5位,知道末尾帧。举个栗子,


    未命名文件-2.png
  • 3.从结合中排除cachedFrameIndexes(已缓存的帧数结合)、requestedFrameIndexes(已解码的帧数集合)、posterImageFrameIndex(封面帧的位置)、因为这些都是已经解码过的帧索引。
  • 4.拿着索引集合去解码吧,从gif图片源(初始化的时候保存的)中解码对应索引的帧,并保存到cachedFramesForIndexes(缓存数据dic)、cachedFrameIndexes(缓存帧索引集)中。
  • 5.最后调用purgeFrameCacheIfNeeded方法根据策略清楚缓存,这时候缓存策略字段frameCacheSizeOptimal(最优缓存帧数)的意义就体现出来了,cachedFramesForIndexes(缓存数据dic)、cachedFrameIndexes(缓存帧索引集)的count不会超过frameCacheSizeOptimal。
    源码如下:
- (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.
    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.
    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
    // 如果缓存的图片小于总图片数
    if ([self.cachedFrameIndexes count] < self.frameCount) {
        //找到需要缓存的索引集合
        NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache];
        //除去已经缓存下来的帧图片索引
        [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];
        }
    }
    
    // 得到关键帧图片
    UIImage *image = self.cachedFramesForIndexes[@(index)];
    
    // 根据策略清楚缓存
    [self purgeFrameCacheIfNeeded];
    
    return image;
}

总结:其实FLAnimatedImage的好处就在于会可以根据内存占用的大小,动态改变内存缓存策略,在gif过大的情况下,可以牺牲cpu性能来保证内存的低占用率。

如有问题,欢迎指教呀!

你可能感兴趣的:(GIF调研之FLAnimatedImage)