美文网首页图文/图像处理
GIF调研之FLAnimatedImage

GIF调研之FLAnimatedImage

作者: 我是繁星 | 来源:发表于2018-08-23 19:39 被阅读0次

    总体思路:

    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

        本文链接:https://www.haomeiwen.com/subject/wczdiftx.html