Kingfisher源码解析之加载动图
Kingfisher源码解析系列,由于水平有限,哪里有错,肯请不吝赐教
- Kingfisher源码解析之使用
- Kingfisher源码解析之Options解释
- Kingfisher源码解析之加载流程
- Kingfisher源码解析之加载动图
- Kingfisher源码解析之ImageCache
- Kingfisher源码解析之Processor和CacheSerializer
- Kingfisher源码解析之ImagePrefetcher
Kingfisher加载GIF的两种使用方式
- 使用UIImageView
let imageView = UIImageView() imageView.kf.setImage(with: URL(string: "gif_url")!)
- 使用AnimatedImageView,AnimatedImageView继承自UIImageView
let imageView = AnimatedImageView() imageView.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
该方法的实现会调用下面这个方法
public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
var image: KFCrossPlatformImage?
switch data.kf.imageFormat {
case .JPEG:
image = KFCrossPlatformImage(data: data, scale: options.scale)
case .PNG:
image = KFCrossPlatformImage(data: data, scale: options.scale)
case .GIF:
image = KingfisherWrapper.animatedImage(data: data, options: options)
case .unknown:
image = KFCrossPlatformImage(data: data, scale: options.scale)
}
return image
}
在这个方法里会先判断图片的类型,判断的方式是取data的前8个字节,感兴趣的话,可以去源码里看下,这里就不贴了,如果是GIF图的话KingfisherWrapper.animatedImage
这个方法
public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
let info: [String: Any] = [
kCGImageSourceShouldCache as String: true,
kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
]
guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
return nil
}
//这里去掉了Macos下的处理
var image: KFCrossPlatformImage?
if options.preloadAll || options.onlyFirstFrame {
guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else {
return nil
}
if options.onlyFirstFrame {
image = animatedImage.images.first
} else {
let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration
image = .animatedImage(with: animatedImage.images, duration: duration)
}
image?.kf.animatedImageData = data
} else {
image = KFCrossPlatformImage(data: data, scale: options.scale)
var kf = image?.kf
kf?.imageSource = imageSource
kf?.animatedImageData = data
}
return image
}
这个方法时展示GIF的核心逻辑,下面详细介绍下这个方法
首先把data转成CGImageSource,然后判断options.preloadAll || options.onlyFirstFrame
的值,其中onlyFirstFrame默认值为false,若为false则只加载第一帧,preloadAll这个值,在我们使用imageView.kf.setImage
时,则取决于imageView的func shouldPreloadAllAnimation()
函数的返回值,此函数是Kingfisher给UIImageView扩展的方法,在UIImageVIew中一直返回true
@objc extension KFCrossPlatformImageView {
func shouldPreloadAllAnimation() -> Bool { return true }
}
也就是说在默认情况下,在上面的方法里会把imageSource
转换成GIFAnimatedImage
类的实例,而在这个类的实例里,做了获取GIF图的每一帧,并获取每一帧的时间然后加起来,最后通过UIImage.animatedImage(with: [images], duration: duration)
生成一个动图的image实例,然后把image赋值给imageView.image
下面把imageSource转成animatedImage的代码,忽略了较多的异常情况
let options: [String: Any] = [
kCGImageSourceShouldCache as String: true,
kCGImageSourceTypeIdentifierHint as String:kUTTypeGIF
]
//把data转换成imageSource
let imageSource = CGImageSourceCreateWithData(data as CFData, options as CFDictionary)!
//获取GIF的总帧数
let frameCount = CGImageSourceGetCount(imageSource)
var images = [UIImage]()
var gifDuration = 0.0
for i in 0..<frameCount {
//获取第i帧的图片,并把图片添加到数组里去
let cgImage = CGImageSourceCreateImageAtIndex(imageSource, i, options as CFDictionary)!
images.append( UIImage(cgImage: cgImage, scale: 1, orientation: .up))
//若只有一帧,把动画时间设置成无限大,否则的话获取每一帧的时间
if frameCount == 1 {
gifDuration = Double.infinity
}else {
//获取每一帧的属性,
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as! [String: Any]
//获取属性中的GIF信息,以及获取信息中的时间
let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as! [String: Any]
let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
let duration = unclampedDelayTime ?? delayTime
gifDuration += duration?.doubleValue ?? 0.1
}
}
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方法,这是我最开始没有想明白的地方
override open func display(_ layer: CALayer) {
if let currentFrame = animator?.currentFrameImage {
layer.contents = currentFrame.cgImage
} else {
layer.contents = image?.cgImage
}
}
UIImageView和AnimatedImageView有什么不同
AnimatedImageView支持一下5点特性,而UIImageView都不支持
-
repeatCount
:循环次数 -
autoPlayAnimatedImage
:是否自动开始播放 -
framePreloadCount
:预加载的帧数 -
backgroundDecode
:是否在后台解码 -
runLoopMode
:GIF播放所在的runLoopMode
并且AnimatedImageView由于不用同时解码所有帧的图形数据,所以更节省内存,但是由于多了一些计算所以会比较浪费CPU