背景
之前蜗牛在瀑布流展示数据的时候发现gif多,而且每个gif很大的时候内存会暴涨,索性研究了一下图片的加载过程,对比了SDWebImage和FLAnimatedImage 对gif的处理过程,做一个小结
知识背景
关于缓存:
当我们通过imageNamed:
去读取一张本地的图片的时候,系统只是在Bundle那查找文件名,然后把这个文件放到UIImage里面返回,并没有做实际的文件读取和解码,当UIImage第一次显示到屏幕上时候,内部的解码方法才被调用,同时解码的结果会被保存待一个全局的缓存中去。在图片解码后,App第一次退到后台收到内存警告,该图片的缓存才会被清空。
当我们用imageWithData
去读取一张图片的时候,UIImage底层是通过调用ImageIO的CGImageSourceCreateWithData
方法去创建source对象的,这里的截图截自FLAnimatedImage
,可以传一个shouldCache字段,默认条件下,这个参数是YES。所以用imageWithData
时候也不能避免解码后图片的缓存,而解码发生的时机也是在图片第一次显示到屏幕上的时候。但是用这种方式解码数据是被缓存到CGImage内部,如果这个图片被释放,内部的解码数据也会被释放。
如何避免缓存
手动调用 CGImageSourceCreateWithData()
来创建图片,并把 ShouldCache
和 ShouldCacheImmediately
关掉。这么做会导致每次图片显示到屏幕时,解码方法都会被调用,造成很大的 CPU 占用。
如何提前解码
- 把图片用
CGContextDrawImage()
绘制到画布上,然后把画布的数据取出来当作图片。这也是常见的网络图片库的做法。解码消耗cpu资源 - 直接读取
1.
CGImageSourceCreateWithData(data)
创建 ImageSource。
2.
CGImageSourceCreateImageAtIndex(source)
创建一个未解码的 CGImage。
3.
CGImageGetDataProvider(image)
获取这个图片的数据源。
4.
CGDataProviderCopyData(provider)
从数据源获取直接解码的数据。
ImageIO
解码发生在最后一步,这样获得的数据是没有经过颜色类型转换的原生数据(比如灰度图像)。
SDWebImage对Gif的支持
无论图片是从disk寻找图片或者下载完成后sdwebImage
都会调用sd_animatedWithData:
如果是gif图片走gif的加载逻辑。
gif加载逻辑,这里注意两点:
1.当图像被解码后,解码数据是会被缓存的,而且被缓存在CGImage中,生命周期和image一致。
2.到此得到的image还是没有发生解码。
那么对于SDWebImage 中对于gif图的解码发生在什么时候呢,
decodedImageWithImage:
在SDWebImage中一共有三处调用
1. 加载完成的时候,只对非gif图片有效,并且由shouldDecompressImages
属性控制是否解码,默认为YES。
2. 从Disk读取image的时候,非gif都有效,且由shouldDecompressImages
属性控制。默认为YES。
3. 在下载中的时候。因为需要做图片的渐变出现的效果,sdwebImage会在didReceiveData
中对加了一部分的图片做解码并传递给业务方。对gif和非gif都有效,且由shouldDecompressImages
属性控制。默认为YES。
所以在默认条件下,如果下载的资源为gif时候,在下载的过程中会对gif资源进行decode,在从disk读取gif后,也会做一次decode,由于读取gif都是用CGImageSourceCreateWithData
(默认参数) 或者 imageWithData
,所以decode后会带有缓存。且缓存的生命周期和image绑定。所以解释了,在有大gif的条件下,为何默认条件下进入feed流会引起内存飙高,但是CPU的的比较低。
decodedImageWithImage:
内部做了判断,只解码非gif图片
默认条件下,所以默认条件下,gif的解码放到gif显示的时候,解码后的buffer会被系统cache,又由于SDWebImage带有cache缓存,会cache刚刚使用过的image,所以退出feed流页面后,还是内存还是居高不下,只有在手动清理webImage的cache后,内存才会下降。
SDWebImage对gif的处理
为什么SDWebImage对gif的处理效率低,而且对大的gif来说尤为明显。
我们知道一张图片从网络到显示,解码的过程必不可少,解码的过程必定需要消耗cpu资源,解码后必定会使得内存增大。如果提前解码缓存,cpu压力小,但是内存会高,如果不缓存,cpu压力大,内存使用少。这里的缓存包括代码的内存指定的内存缓存和ios系统对解码后的图片的缓存。
SDWebImage对gif的操作,即使关掉解码和存储到内存(sd里的memoryCache),解码数据还是会在展示的时候被系统缓存,所以性能不好。
FLAnimatedImageView对gif的处理
FLAnimatedImage 是由Flipboard开源的iOS平台上播放GIF动画的一个优秀解决方案,在内存占用和播放体验都有不错的表现。
FLAnimatedImageView
的源码结构非常简单,FLAnimatedImage
负责处理GIF,然后从缓存中提供给FLAnimatedImageView
当前需要显示的图像
关键方法解析
初始化
- 初始化缓存字典
- 初始化imageSource,根据
kCGImageSourceShouldCache
的官方文档描述, 所以设置kCGImageSourceShouldCache
为NO,可以避免系统对图片进行缓存,
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
.
- 判断是否gif
- 取出gif播放次数
- 遍历每帧图片
- 取出帧图片
- 取出的第一张图片为GIF动画的封面图片
- 取出帧图片的信息
- 取出帧图片的展示时间
- GIF动画缓存策略
- 确认最佳的GIF动画的解码后帧图片缓存数量
读取UIImage对象
- 对索引位置进行判断,避免出现越界情况
- 记录当前取出的帧图片的索引位置
- 判断GIF动画的帧图片的是否全部缓存下来了,因为有可能缓存策略是缓存所有的帧图片
- 根据缓存策略得到接下来需要缓存的帧图片索引,
- 除去已经缓存下来的帧图片索引
- 将需要缓存索引扔给其他线程进行解码装载,解码的过程是在其他线程,不会发生堵塞,解码也是通过
CGContextDrawImage
的方式进行解码。这个和SDWebImage一致。 - 取出帧图片
-
根据缓存策略清缓存
gif播放部分
gif播放部分在startAnimating
时候开开启CADisplayLink
,进行重绘。
这里有两点需要注意,
每次
CADisplayLink
回调取的都是cache里的图片,如果cache里面没有图片,就跳过这次绘制机会累加器的存在意义在与,避免反复去绘制同一帧
所以FLAnimatedImageView
和FLAnimatedImage
是标准的生产者和消费者的模式。FLAnimatedImage
开启一个子线程解码图片,FLAnimatedImageView
消费图片进行展示。
总结
其实SDWebImage
和FLAnimatedImageView
在gif的支持上,性能差别的主要原因有两个:
-
FLAnimatedImageView
存在一个缓存策略,每次也只解码一部分的帧数据,而且严格把控缓存数量。而SDWebImage
依赖于系统在展示的时候的统一的全部帧解码,缓存的生命周期不好进行细粒度的控制。 -
FLAnimatedImageView
解码的过程放到了子线程中,而SDWebImage
默认对gif不解码,所以解码发生在gif第一次显示时候,发生在主线程。
但是对于比较小且数量少的gif,其实两个性能差别不大,但是对于数量多或者gif本身比较大时候,性能差距会异常明显。
注意:最新的SDWebImage
可以pod导入SDWebImage/GIF
,对gif的处理自动支持了FLAnimatedImage