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

相關文章
相關標籤/搜索