Kingfisher源碼解析之加載動(dòng)圖

Kingfisher源碼解析系列,由于水平有限,哪里有錯(cuò),肯請(qǐng)不吝賜教

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內(nèi)部是如何處理的

看了上面2個(gè)顯示GIF的方法,我們可能下面2個(gè)疑問(wèn),如果你對(duì)下面2個(gè)問(wèn)題很清楚,本篇文章你可以跳過(guò)了

  • 加載GIF圖和加載普通圖片的使用方式是一樣的,它是怎么做到如果是GIF圖就顯示GIF圖,是普通圖片就是現(xiàn)實(shí)普通圖片的
  • 使用UIImageView和AnimatedImageView的調(diào)用方式也是一樣的,這2中加載方式是否不同
    我們先來(lái)看第一個(gè)問(wèn)題,Kingfisher是如何區(qū)分GIF圖和普通圖片的,這個(gè)問(wèn)題分3種情況
  1. 圖片通過(guò)Resource(通過(guò)網(wǎng)絡(luò)下載的)或者ImageDataProvider提供的
  2. 圖片是從緩存中內(nèi)存緩存中加載的
  3. 圖片是從磁盤(pán)緩存中加載的

首先來(lái)看第一種情況,在這之前,先來(lái)看下Kingfisher中配置項(xiàng)的這個(gè)配置public var processor: ImageProcessor = DefaultImageProcessor.default,這個(gè)配置是提供網(wǎng)絡(luò)下載完成或者加載完成本地Data之后,會(huì)調(diào)用processorfunc process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把Data轉(zhuǎn)換成UIImage,而processor的默認(rèn)值是DefaultImageProcessor,在DefaultImageProcessor該方法的實(shí)現(xiàn)會(huì)調(diào)用下面這個(gè)方法

   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
    }

在這個(gè)方法里會(huì)先判斷圖片的類型,判斷的方式是取data的前8個(gè)字節(jié),感興趣的話,可以去源碼里看下,這里就不貼了,如果是GIF圖的話KingfisherWrapper.animatedImage這個(gè)方法

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
}

這個(gè)方法時(shí)展示GIF的核心邏輯,下面詳細(xì)介紹下這個(gè)方法
首先把data轉(zhuǎn)成CGImageSource,然后判斷options.preloadAll || options.onlyFirstFrame 的值,其中onlyFirstFrame默認(rèn)值為false,若為false則只加載第一幀,preloadAll這個(gè)值,在我們使用imageView.kf.setImage時(shí),則取決于imageView的func shouldPreloadAllAnimation()函數(shù)的返回值,此函數(shù)是Kingfisher給UIImageView擴(kuò)展的方法,在UIImageVIew中一直返回true

@objc extension KFCrossPlatformImageView {
    func shouldPreloadAllAnimation() -> Bool { return true }
}

也就是說(shuō)在默認(rèn)情況下,在上面的方法里會(huì)把imageSource轉(zhuǎn)換成GIFAnimatedImage類的實(shí)例,而在這個(gè)類的實(shí)例里,做了獲取GIF圖的每一幀,并獲取每一幀的時(shí)間然后加起來(lái),最后通過(guò)UIImage.animatedImage(with: [images], duration: duration)生成一個(gè)動(dòng)圖的image實(shí)例,然后把image賦值給imageView.image

下面把imageSource轉(zhuǎn)成animatedImage的代碼,忽略了較多的異常情況

    let options: [String: Any] = [
        kCGImageSourceShouldCache as String: true,
        kCGImageSourceTypeIdentifierHint as String:kUTTypeGIF
    ]
    //把data轉(zhuǎn)換成imageSource
    let imageSource = CGImageSourceCreateWithData(data as CFData, options as CFDictionary)!
    //獲取GIF的總幀數(shù)
    let frameCount = CGImageSourceGetCount(imageSource)
    var images = [UIImage]()
    var gifDuration = 0.0
    for i in 0..<frameCount {
        //獲取第i幀的圖片,并把圖片添加到數(shù)組里去
        let cgImage = CGImageSourceCreateImageAtIndex(imageSource, i, options as CFDictionary)!
        images.append( UIImage(cgImage: cgImage, scale: 1, orientation: .up))
        //若只有一幀,把動(dòng)畫(huà)時(shí)間設(shè)置成無(wú)限大,否則的話獲取每一幀的時(shí)間
        if frameCount == 1 {
            gifDuration = Double.infinity
        }else {
            //獲取每一幀的屬性,
            let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as! [String: Any]
            //獲取屬性中的GIF信息,以及獲取信息中的時(shí)間
            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)

接著看第二種情況,若是從內(nèi)存緩存中加載的,緩存的就是動(dòng)圖,所以是直接加載的

最后看第三種情況,若是從磁盤(pán)中緩存的,Kingfisher又是如何處理的,在這之前,先來(lái)看下Kingfisher中配置項(xiàng)的這個(gè)配置public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default,這個(gè)配置是提供當(dāng)從磁盤(pán)中讀取完數(shù)據(jù)之后,把數(shù)據(jù)反序列化為UIImage,會(huì)調(diào)用cacheSerializerpublic func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把Data反序列化為UIImage,而cacheSerializer的默認(rèn)值是DefaultCacheSerializer,在DefaultCacheSerializer該方法的實(shí)現(xiàn)也會(huì)調(diào)用public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage?這個(gè)方法,下面就是跟第一種情況的邏輯一樣了

下面來(lái)看AnimatedImageView是如何加載GIF圖的,上面說(shuō)imageView的shouldPreloadAllAnimation一直返回true,而AnimatedImageView重寫(xiě)了此函數(shù),并返回false,因此option.preloadAll等于false,所以會(huì)走else里的邏輯,把data轉(zhuǎn)成image,利用關(guān)聯(lián)屬性,給image添加了兩個(gè)屬性imageSource:CGImageSourceanimatedImageData:Data,并對(duì)其進(jìn)行賦值

到現(xiàn)在為止,我們還是沒(méi)有看到AnimatedImageView是如何展示GIF圖的。接著往下看
AnimatedImageView重寫(xiě)了image的didSet,而上面的方法返回后,會(huì)對(duì)imageView.image進(jìn)行賦值,正好觸發(fā)了image的didSet,在這里開(kāi)啟了一個(gè)CADisplayLink和Animator。

Animator為imageView提供動(dòng)圖的數(shù)據(jù),每一幀的圖片以及時(shí)間,需要注意的是,它并不會(huì)一次加載好所有幀的圖片,默認(rèn)情況下,只是先加載前10幀,剩下的等需要的再去加載

CADisplayLink,在每次屏幕刷新的時(shí)候,去判斷是否需要展示新的一幀圖片,若需要,則刷新imageView

這里刷新是調(diào)用self.layer.setNeedsDisplay(),而調(diào)用此方法,系統(tǒng)會(huì)調(diào)用layer.delegate里的open func display(_ layer: CALayer),而UIView的layer.delegate是自己本身,所以會(huì)調(diào)用AnimatedImageView重寫(xiě)的display方法,這是我最開(kāi)始沒(méi)有想明白的地方

   override open func display(_ layer: CALayer) {
        if let currentFrame = animator?.currentFrameImage {
            layer.contents = currentFrame.cgImage
        } else {
            layer.contents = image?.cgImage
        }
    }

UIImageView和AnimatedImageView有什么不同

AnimatedImageView支持一下5點(diǎn)特性,而UIImageView都不支持

  1. repeatCount:循環(huán)次數(shù)
  2. autoPlayAnimatedImage:是否自動(dòng)開(kāi)始播放
  3. framePreloadCount:預(yù)加載的幀數(shù)
  4. backgroundDecode:是否在后臺(tái)解碼
  5. runLoopMode:GIF播放所在的runLoopMode

并且AnimatedImageView由于不用同時(shí)解碼所有幀的圖形數(shù)據(jù),所以更節(jié)省內(nèi)存,但是由于多了一些計(jì)算所以會(huì)比較浪費(fèi)CPU

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,656評(píng)論 1 32
  • 1.系統(tǒng)UIImageView 多張圖片組成動(dòng)畫(huà) /** * UIImageView 動(dòng)畫(huà) * Memor...
    zhengelababy閱讀 9,283評(píng)論 3 6
  • 寫(xiě)在前面的 一顆傷心死掉的橘子樹(shù) 不拘一世之利以為己私分,不以王天下為已處顯。顯則明。萬(wàn)物一府,死生同狀。 扯淡結(jié)...
    d4d98020ef88閱讀 5,823評(píng)論 10 17
  • 1.自定義控件 a.繼承某個(gè)控件 b.重寫(xiě)initWithFrame方法可以設(shè)置一些它的屬性 c.在layouts...
    圍繞的城閱讀 3,703評(píng)論 2 4
  • 眾所周知,iOS默認(rèn)是不支持gif類型圖片的顯示的,但是我們項(xiàng)目中常常是需要顯示gif為動(dòng)態(tài)圖片。那腫么辦?第三方...
    smalldu閱讀 11,949評(píng)論 9 53

友情鏈接更多精彩內(nèi)容