Kingfisher源码解析之加载动图

Kingfisher源码解析系列,因为水平有限,哪里有错,肯请不吝赐教数组

Kingfisher加载GIF的两种使用方式

  1. 使用UIImageView
    let imageView = UIImageView()
    imageView.kf.setImage(with: URL(string: "gif_url")!)
    复制代码
  2. 使用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种状况
  1. 图片经过Resource(经过网络下载的)或者ImageDataProvider提供的
  2. 图片是从缓存中内存缓存中加载的
  3. 图片是从磁盘缓存中加载的

首先来看第一种状况,在这以前,先来看下Kingfisher中配置项的这个配置public var processor: ImageProcessor = DefaultImageProcessor.default,这个配置是提供网络下载完成或者加载完成本地Data以后,会调用processorfunc process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把Data转换成UIImage,而processor的默认值是DefaultImageProcessor,在DefaultImageProcessor该方法的实现会调用下面这个方法bash

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中一直返回trueapp

@objc extension KFCrossPlatformImageView {
    func shouldPreloadAllAnimation() -> Bool { return true }
}
复制代码

也就是说在默认状况下,在上面的方法里会把imageSource转换成GIFAnimatedImage类的实例,而在这个类的实例里,作了获取GIF图的每一帧,并获取每一帧的时间而后加起来,最后经过UIImage.animatedImage(with: [images], duration: duration)生成一个动图的image实例,而后把image赋值给imageView.imageide

下面把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)
复制代码

接着看第二种状况,如果从内存缓存中加载的,缓存的就是动图,因此是直接加载的oop

最后看第三种状况,如果从磁盘中缓存的,Kingfisher又是如何处理的,在这以前,先来看下Kingfisher中配置项的这个配置public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default,这个配置是提供当从磁盘中读取完数据以后,把数据反序列化为UIImage,会调用cacheSerializerpublic func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把Data反序列化为UIImage,而cacheSerializer的默认值是DefaultCacheSerializer,在DefaultCacheSerializer该方法的实现也会调用public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage?这个方法,下面就是跟第一种状况的逻辑同样了post

下面来看AnimatedImageView是如何加载GIF图的,上面说imageView的shouldPreloadAllAnimation一直返回true,而AnimatedImageView重写了此函数,并返回false,所以option.preloadAll等于false,因此会走else里的逻辑,把data转成image,利用关联属性,给image添加了两个属性imageSource:CGImageSourceanimatedImageData:Data,并对其进行赋值fetch

到如今为止,咱们仍是没有看到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在展现GIF图有什么不一样

AnimatedImageView支持一下5点特性,而UIImageView都不支持

  1. repeatCount:循环次数
  2. autoPlayAnimatedImage:是否自动开始播放
  3. framePreloadCount:预加载的帧数
  4. backgroundDecode:是否在后台解码
  5. runLoopMode:GIF播放所在的runLoopMode

而且AnimatedImageView因为不用同时解码全部帧的图形数据,因此更节省内存,可是因为多了一些计算因此会比较浪费CPU

相关文章
相关标签/搜索