Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教
- Kingfisher 源码解析之使用
- Kingfisher 源码解析之 Options 解释
- Kingfisher 源码解析之加载流程
- Kingfisher 源码解析之加载动图
- Kingfisher 源码解析之 ImageCache
- Kingfisher 源码解析之 Processor 和 CacheSerializer
- Kingfisher 源码解析之 ImagePrefetcher
Kingfisher 加载 GIF 的两种使用方式
使用 UIImageView
1let imageView = UIImageView()2imageView.kf.setImage(with: URL(string: "gif_url")!)使用 AnimatedImageView,AnimatedImageView 继承自 UIImageView
1let imageView = AnimatedImageView()2imageView.kf.setImage(with: URL(string: "gif_url")!)
Kingfisher 内部是如何处理的
看了上面 2 个显示 GIF 的方法,我们可能下面 2 个疑问,如果你对下面 2 个问题很清楚,本篇文章你可以跳过了
- 加载 GIF 图和加载普通图片的使用方式是一样的,它是怎么做到如果是 GIF 图就显示 GIF 图,是普通图片就是现实普通图片的
- 使用 UIImageView 和 AnimatedImageView 的调用方式也是一样的,这 2 中加载方式是否不同
我们先来看第一个问题,Kingfisher 是如何区分 GIF 图和普通图片的,这个问题分 3 种情况
- 图片通过 Resource(通过网络下载的)或者 ImageDataProvider 提供的
- 图片是从缓存中内存缓存中加载的
- 图片是从磁盘缓存中加载的
首先来看第一种情况,在这之前,先来看下Kingfisher中配置项的这个配置public var processor: ImageProcessor = DefaultImageProcessor.default,这个配置是提供网络下载完成或者加载完成本地 Data 之后,会调用processor的func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把 Data 转换成 UIImage,而 processor 的默认值是DefaultImageProcessor,在DefaultImageProcessor该方法的实现会调用下面这个方法
1 | public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { |
2 | var image: KFCrossPlatformImage? |
3 | switch data.kf.imageFormat { |
4 | case .JPEG: |
5 | image = KFCrossPlatformImage(data: data, scale: options.scale) |
6 | case .PNG: |
7 | image = KFCrossPlatformImage(data: data, scale: options.scale) |
8 | case .GIF: |
9 | image = KingfisherWrapper.animatedImage(data: data, options: options) |
10 | case .unknown: |
11 | image = KFCrossPlatformImage(data: data, scale: options.scale) |
12 | } |
13 | return image |
14 | } |
在这个方法里会先判断图片的类型,判断的方式是取 data 的前 8 个字节,感兴趣的话,可以去源码里看下,这里就不贴了,如果是 GIF 图的话KingfisherWrapper.animatedImage这个方法
1 | public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { |
2 | let info: [String: Any] = [ |
3 | kCGImageSourceShouldCache as String: true, |
4 | kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF |
5 | ] |
6 | guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else { |
7 | return nil |
8 | } |
9 | //这里去掉了Macos下的处理 |
10 | var image: KFCrossPlatformImage? |
11 | if options.preloadAll || options.onlyFirstFrame { |
12 | guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else { |
13 | return nil |
14 | } |
15 | if options.onlyFirstFrame { |
16 | image = animatedImage.images.first |
17 | } else { |
18 | let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration |
19 | image = .animatedImage(with: animatedImage.images, duration: duration) |
20 | } |
21 | image?.kf.animatedImageData = data |
22 | } else { |
23 | image = KFCrossPlatformImage(data: data, scale: options.scale) |
24 | var kf = image?.kf |
25 | kf?.imageSource = imageSource |
26 | kf?.animatedImageData = data |
27 | } |
28 | return image |
29 | } |
这个方法时展示 GIF 的核心逻辑,下面详细介绍下这个方法
首先把 data 转成 CGImageSource,然后判断options.preloadAll || options.onlyFirstFrame 的值,其中 onlyFirstFrame 默认值为 false,若为 false 则只加载第一帧,preloadAll 这个值,在我们使用imageView.kf.setImage时,则取决于 imageView 的func shouldPreloadAllAnimation()函数的返回值,此函数是 Kingfisher 给 UIImageView 扩展的方法,在 UIImageVIew 中一直返回 true
1 | @objc extension KFCrossPlatformImageView { |
2 | func shouldPreloadAllAnimation() -> Bool { return true } |
3 | } |
也就是说在默认情况下,在上面的方法里会把imageSource转换成GIFAnimatedImage类的实例,而在这个类的实例里,做了获取 GIF 图的每一帧,并获取每一帧的时间然后加起来,最后通过UIImage.animatedImage(with: [images], duration: duration)生成一个动图的 image 实例,然后把 image 赋值给imageView.image
下面把 imageSource 转成 animatedImage 的代码,忽略了较多的异常情况
1 | let options: [String: Any] = [ |
2 | kCGImageSourceShouldCache as String: true, |
3 | kCGImageSourceTypeIdentifierHint as String:kUTTypeGIF |
4 | ] |
5 | //把data转换成imageSource |
6 | let imageSource = CGImageSourceCreateWithData(data as CFData, options as CFDictionary)! |
7 | //获取GIF的总帧数 |
8 | let frameCount = CGImageSourceGetCount(imageSource) |
9 | var images = [UIImage]() |
10 | var gifDuration = 0.0 |
11 | for i in 0..<frameCount { |
12 | //获取第i帧的图片,并把图片添加到数组里去 |
13 | let cgImage = CGImageSourceCreateImageAtIndex(imageSource, i, options as CFDictionary)! |
14 | images.append( UIImage(cgImage: cgImage, scale: 1, orientation: .up)) |
15 | //若只有一帧,把动画时间设置成无限大,否则的话获取每一帧的时间 |
16 | if frameCount == 1 { |
17 | gifDuration = Double.infinity |
18 | }else { |
19 | //获取每一帧的属性, |
20 | let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as! [String: Any] |
21 | //获取属性中的GIF信息,以及获取信息中的时间 |
22 | let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as! [String: Any] |
23 | let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber |
24 | let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber |
25 | let duration = unclampedDelayTime ?? delayTime |
26 | gifDuration += duration?.doubleValue ?? 0.1 |
27 | } |
28 | } |
29 | imageView.image = UIImage.animatedImage(with: images, duration: gifDuration) |
接着看第二种情况,若是从内存缓存中加载的,缓存的就是动图,所以是直接加载的
最后看第三种情况,若是从磁盘中缓存的,Kingfisher 又是如何处理的,在这之前,先来看下Kingfisher中配置项的这个配置public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default,这个配置是提供当从磁盘中读取完数据之后,把数据反序列化为 UIImage,会调用cacheSerializer的public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把 Data 反序列化为 UIImage,而 cacheSerializer 的默认值是DefaultCacheSerializer,在DefaultCacheSerializer该方法的实现也会调用public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage?这个方法,下面就是跟第一种情况的逻辑一样了
下面来看 AnimatedImageView 是如何加载 GIF 图的,上面说 imageView 的shouldPreloadAllAnimation一直返回 true,而 AnimatedImageView 重写了此函数,并返回 false,因此option.preloadAll等于 false,所以会走 else 里的逻辑,把 data 转成 image,利用关联属性,给 image 添加了两个属性imageSource:CGImageSource和animatedImageData:Data,并对其进行赋值
到现在为止,我们还是没有看到 AnimatedImageView 是如何展示 GIF 图的。接着往下看
AnimatedImageView 重写了 image 的 didSet,而上面的方法返回后,会对 imageView.image 进行赋值,正好触发了 image 的 didSet,在这里开启了一个 CADisplayLink 和 Animator。
Animator 为 imageView 提供动图的数据,每一帧的图片以及时间,需要注意的是,它并不会一次加载好所有帧的图片,默认情况下,只是先加载前 10 帧,剩下的等需要的再去加载
CADisplayLink,在每次屏幕刷新的时候,去判断是否需要展示新的一帧图片,若需要,则刷新 imageView
这里刷新是调用self.layer.setNeedsDisplay(),而调用此方法,系统会调用layer.delegate里的open func display(_ layer: CALayer),而 UIView 的 layer.delegate 是自己本身,所以会调用 AnimatedImageView 重写的 display 方法,这是我最开始没有想明白的地方
1 | override open func display(_ layer: CALayer) { |
2 | if let currentFrame = animator?.currentFrameImage { |
3 | layer.contents = currentFrame.cgImage |
4 | } else { |
5 | layer.contents = image?.cgImage |
6 | } |
7 | } |
UIImageView 和 AnimatedImageView 在展示 GIF 图有什么不同
AnimatedImageView 支持一下 5 点特性,而 UIImageView 都不支持
repeatCount:循环次数autoPlayAnimatedImage:是否自动开始播放framePreloadCount:预加载的帧数backgroundDecode:是否在后台解码runLoopMode:GIF 播放所在的 runLoopMode
并且 AnimatedImageView 由于不用同时解码所有帧的图形数据,所以更节省内存,但是由于多了一些计算所以会比较浪费 CPU