Kingfisher源碼解析系列,由于水平有限,哪里有錯(cuò),肯請(qǐng)不吝賜教
- Kingfisher源碼解析之使用
- Kingfisher源碼解析之Options解釋
- Kingfisher源碼解析之加載流程
- Kingfisher源碼解析之加載動(dòng)圖
- 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內(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種情況
- 圖片通過(guò)Resource(通過(guò)網(wǎng)絡(luò)下載的)或者ImageDataProvider提供的
- 圖片是從緩存中內(nèi)存緩存中加載的
- 圖片是從磁盤(pán)緩存中加載的
首先來(lái)看第一種情況,在這之前,先來(lái)看下Kingfisher中配置項(xiàng)的這個(gè)配置public var processor: ImageProcessor = DefaultImageProcessor.default,這個(gè)配置是提供網(wǎng)絡(luò)下載完成或者加載完成本地Data之后,會(huì)調(diào)用processor的func 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)用cacheSerializer的public 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:CGImageSource和animatedImageData: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都不支持
-
repeatCount:循環(huán)次數(shù) -
autoPlayAnimatedImage:是否自動(dòng)開(kāi)始播放 -
framePreloadCount:預(yù)加載的幀數(shù) -
backgroundDecode:是否在后臺(tái)解碼 -
runLoopMode:GIF播放所在的runLoopMode
并且AnimatedImageView由于不用同時(shí)解碼所有幀的圖形數(shù)據(jù),所以更節(jié)省內(nèi)存,但是由于多了一些計(jì)算所以會(huì)比較浪費(fèi)CPU