总体思路:
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,如图。
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位,知道末尾帧。举个栗子,
- 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性能来保证内存的低占用率。
如有问题,欢迎指教呀!