Kingfisher 3.x 學習(一)

序言

Kingfisher是喵神的一個異步下載和緩存圖片的Swift庫,類似于OC 的SDWebImage
中文簡介github地址 最近才開始學習swift, 所以對于之前的swift 版本不是很了解,直接以最新版本來學習了 由于個人水平有限 如有錯誤還望包涵

一、Kingfisher的架構

閱讀他人優秀代碼是一個提高自身代碼水平很好的方法。花了幾天的時間,看了Kingfisher的源代碼,里面包含的很多知識點,讓我受益匪淺。3.x版本相比與之前的版本,一個重要的改變就是protocol的靈活運用,更加面向協議編程。當然還有其他很多知識,比如多線程,枚舉,閉包,Extension 等等應用。 Kingfisher中CoreExtensionHelpers 三個目錄結構 共20個文件

Core
image.swift 文件內部對 UIImage 以及 NSData 進行了拓展, 包含判定圖片類型、圖片解碼以及Gif數據處理等操作
Indicator.swift 圖片加載時loading指示
ImageCache.swift 主要負責將加載過的圖片緩存至本地。
ImageDownloader.swift 負責下載網絡圖片。
ImagePrefetcher.swift 可用于提前指定一些圖片下載
ImageProcessor.swift 可用于將下載的數據合成圖片對象
CacheSerializer.swift 可用于圖像對象序列化成圖像數據存儲到磁盤緩存和從磁盤緩存將圖片數據反序列化成圖像對象。
RequestModifier.swift 下載圖像請求修改器。
ImageTransition.swift 過渡動畫效果 使用UIViewAnimationOptions動畫效果
KingfisherManager.swift Kingfisher 管理控制類,擁有圖片下載及緩存功能
KingfisherOptionsInfo.swift 枚舉KingfisherOptionsInfoItem 配置 Kingfisher 行為的參數,包括 是否自定義緩存對象 是否自定義下載器 是否過渡動畫 是否設置下載低優先級 是否強制刷新 是否僅獲取緩存圖片 是否僅緩存至內存、是否允許圖像后臺解碼等設置。
Filter.swift 圖像過濾器
Resource.swift 記錄了圖片的下載地址和緩存Key。
Kingfisher.swift 添加KingfisherCompatible通用協議 kf新屬性

Extension
ImageView+Kingfisher.swift UIButton+Kingfisher.swift NSButton+KingfisherUIImageView UIButton NSButton 進行了拓展 主要用于提供 Kingfisher 的外部接口。

Helpers
String+MD5.swift 負責圖片緩存時對文件名進行MD5加密操作。
Box.swift 一個簡單泛型類
ThreadHelper.swift中的 dispatch_async_safely_main_queue 函數接受一個閉包 利用 NSThread.isMainThread 判斷并將其放置在主線程中執行

二、Kingfisher.swift

主要文件ImageView+Kingfisher,KingfisherManager,ImageCache,ImageDownloader,廢話不多說直接代碼學習

運行demo 下面有這么一段代碼:

    let url = URL(string:"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.row + 1).jpg")!
    (cell as! CollectionViewCell).cellImageView.kf.setImage(with: url,
                                           placeholder: nil,
                                           options: [.transition(.fade(1))],
                                           progressBlock: { receivedSize, totalSize in
                                            print("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
            },
                                           completionHandler: { image, error, cacheType, imageURL in
                                            print("\(indexPath.row + 1): Finished")
        })

首先調用的UIImageViewkf屬性 之前是調用UIImageViewExtension中的kf_setImage ,現已棄用 那kf 屬性是如何實現的?
下面是Kingfisher.swift源碼

  • 自定義了不同平臺下的一些類型別名 swift中的typealias 相當于OC中的typedef
#if os(macOS)
    import AppKit
    public typealias Image = NSImage
    public typealias Color = NSColor
    public typealias ImageView = NSImageView
    typealias Button = NSButton
#else
    import UIKit
    public typealias Image = UIImage
    public typealias Color = UIColor
    #if !os(watchOS)
    public typealias ImageView = UIImageView
    typealias Button = UIButton
    #endif
#endif
  • 申明了泛型類Kingfisher 實現了一個簡單構造器,其中上面的cellImageView就是base屬性
public final class Kingfisher<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

  • 申明KingfisherCompatible協議 有一個可讀屬性kf 其類型是關聯類型
/**
 A type that has Kingfisher extensions.
 */

public protocol KingfisherCompatible {
    associatedtype CompatibleType
    var kf: CompatibleType { get }
}
  • KingfisherCompatible協議的實現 屬性kf關聯Kingfisher類型 返回一個Kingfisher實例 base 參數就是傳入的self
public extension KingfisherCompatible {
    public var kf: Kingfisher<Self> {
        get { return Kingfisher(self) }
    }
}
  • Image ImageView Button 遵守 KingfisherCompatible 協議 所以上邊的self參數就是遵守了協議的類型 因此base屬性即cellImageView
extension Image: KingfisherCompatible { }
#if !os(watchOS)
extension ImageView: KingfisherCompatible {}
extension Button: KingfisherCompatible { }
#endif

三、ImageView+Kingfisher

現在來說說setImage這個方法的實現 這個方法是在Kingfisher的Extension 中實現 并且要求Base屬于UIImageView類型 即where Base: ImageView 由于kf 屬性關聯了Kingfisher
所以可以調用(cell as! CollectionViewCell).cellImageView.kf.setImage
Extensions目錄下的三個文件都是類似實現的 這里就以ImageView+Kingfisher.swift為例
下面方法是外部使用Kingfisher最頻繁也是最重要的方法
第一個參數Resource是一個URL遵守的Protocol,一般傳入圖片的URL,不可為空
第二個參數placeholder是一個默認的占位圖,可為空
第三個參數KingfisherOptionsInfo 是個枚舉數組,配置Kingfisher下載圖片的一些操作行為
第四個參數DownloadProgressBlock是個下載進度閉包,可以用于更新下載UI
第五個參數completionHandler是個下載完成閉包,閉包參數包含圖片,錯誤,緩存類型,URL 信息

extension Kingfisher where Base: ImageView {
    /**
     Set an image with a resource, a placeholder image, options, progress handler and completion handler.
     
     - parameter resource:          Resource object contains information such as `cacheKey` and `downloadURL`.
     - parameter placeholder:       A placeholder image when retrieving the image at URL.
     - parameter options:           A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
     - parameter progressBlock:     Called when the image downloading progress gets updated.
     - parameter completionHandler: Called when the image retrieved and set.
     
     - returns: A task represents the retrieving process.
     
     - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread.
     The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method.
     */
    @discardableResult 忽略返回值警告
    public func setImage(with resource: Resource?,
                         placeholder: Image? = nil,
                         options: KingfisherOptionsInfo? = nil,
                         progressBlock: DownloadProgressBlock? = nil,
                         completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
    {
        當傳入的resource為空時 使用guard語句提前退出 Resource是一個協議 URL遵守此協議 Resource有兩個屬性 cacheKey和downloadURL
        guard let resource = resource else {
            base.image = placeholder
            completionHandler?(nil, nil, .none, nil)
            return .empty
        }
        圖片加載過程中是否顯示placeholder
        var options = options ?? KingfisherEmptyOptionsInfo
        if !options.keepCurrentImageWhileLoading {
            base.image = placeholder
        }
        如果indicator存在,開啟轉圈動畫 indicator 通過屬性關聯存取
        let maybeIndicator = indicator
        maybeIndicator?.startAnimatingView()
        關聯屬性綁定下載的URL
        setWebURL(resource.downloadURL)

        默認開啟加載所有GIF圖片數據,顯示GIF 動態圖片
        if base.shouldPreloadAllGIF() {
            options.append(.preloadAllGIFData)
        }
        調用KingfisherManager的方法來獲取圖片
        let task = KingfisherManager.shared.retrieveImage(
            with: resource,
            options: options,
            progressBlock: { receivedSize, totalSize in
                下載進度回調
                if let progressBlock = progressBlock {
                    progressBlock(receivedSize, totalSize)
                }
            },
            completionHandler: {[weak base] image, error, cacheType, imageURL in
                確保線程安全
                DispatchQueue.main.safeAsync {
                    確保返回的圖片與URL對應一致
                    guard let strongBase = base, imageURL == self.webURL else {
                        return
                    }
                    self.setImageTask(nil)
                    沒有圖片返回停止動畫返回錯誤
                    guard let image = image else {
                        maybeIndicator?.stopAnimatingView()
                        completionHandler?(nil, error, cacheType, imageURL)
                        return
                    }
                    是否需要過渡動畫 transitionItem 為 options中第一個.transition
                    需要過渡動畫需要滿足以下情況
                    1.transitionItem存在且不為.transition(.none)
                    2.options.forceTransition存在 或者 cacheType == .none 
                    guard let transitionItem = options.firstMatchIgnoringAssociatedValue(.transition(.none)),
                        case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
                    {
                        maybeIndicator?.stopAnimatingView()
                        strongBase.image = image
                        completionHandler?(image, error, cacheType, imageURL)
                        return
                    }
                    過渡動畫
                    #if !os(macOS)
                        UIView.transition(with: strongBase, duration: 0.0, options: [],
                                          animations: { maybeIndicator?.stopAnimatingView() },
                                          completion: { _ in
                                            UIView.transition(with: strongBase, duration: transition.duration,
                                                              options: [transition.animationOptions, .allowUserInteraction],
                                                              animations: {
                                                                // Set image property in the animation.
                                                                設置圖片,如果是自定義動畫 在定義動畫回調中設置圖片,代碼在ImageTransition.swift
                                                                transition.animations?(strongBase, image)
                                                              },
                                                              completion: { finished in
                                                                動畫結束回調
                                                                transition.completion?(finished)
                                                                completionHandler?(image, error, cacheType, imageURL)
                                                              })
                                          })
                    #endif
                }
            })
        setImageTask(task)
   return task
    }
    /**
     Cancel the image download task bounded to the image view if it is running.
     Nothing will happen if the downloading has already finished.
     */
    取消下載
    public func cancelDownloadTask() {
        imageTask?.downloadTask?.cancel()
    }
}

ImageView+Kingfisher 中的WebUR indicatorType indicator imageTask 屬性均使用屬性關聯技術實現數據的存取

四 、KingfisherManager

該類是Kingfisher唯一的一個管理調度類。這個類有下載和緩存兩大功能模塊 主要包含了兩個屬性 兩個方法
public var cache: ImageCache 圖片緩存屬性
public var downloader: ImageDownloader 圖片下載屬性
func downloadAndCacheImage 下載并且緩存圖片方法
func tryToRetrieveImageFromCache 獲取緩存圖片

ImageView+Kingfisher中最后圖片的獲取就是由KingfisherManager的單例實現的retrieveImage

  • 外部調用獲取圖片方法
func retrieveImage(with resource: Resource,
        options: KingfisherOptionsInfo?,
        progressBlock: DownloadProgressBlock?,
        completionHandler: CompletionHandler?) -> RetrieveImageTask{
        let task = RetrieveImageTask()
        if let options = options, options.forceRefresh {
             強制刷新 從網絡獲取圖片
            _ = downloadAndCacheImage(
                with: resource.downloadURL,
                forKey: resource.cacheKey,
                retrieveImageTask: task,
                progressBlock: progressBlock,
                completionHandler: completionHandler,
                options: options)
        } else {
            從緩存獲取圖片
            tryToRetrieveImageFromCache(
                forKey: resource.cacheKey,
                with: resource.downloadURL,
                retrieveImageTask: task,
                progressBlock: progressBlock,
                completionHandler: completionHandler,
                options: options)
        }
        return task
    }
  • 下載并且緩存圖片的方法
   func downloadAndCacheImage(with url: URL,
                             forKey key: String,
                      retrieveImageTask: RetrieveImageTask,
                          progressBlock: DownloadProgressBlock?,
                      completionHandler: CompletionHandler?,
                                options: KingfisherOptionsInfo?) -> RetrieveImageDownloadTask?
    {
        獲取下載器 并開啟下載 
        let options = options ?? KingfisherEmptyOptionsInfo
        let downloader = options.downloader
        return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
            progressBlock: { receivedSize, totalSize in
                progressBlock?(receivedSize, totalSize)
            },
            completionHandler: { image, error, imageURL, originalData in

                let targetCache = options.targetCache
                if let error = error, error.code == KingfisherError.notModified.rawValue {
                    // Not modified. Try to find the image from cache.
                    // (The image should be in cache. It should be guaranteed by the framework users.)
                   如果有錯誤并且沒有修改過URL 返回緩存圖片
                    targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
                        completionHandler?(cacheImage, nil, cacheType, url)
                    })
                    return
                }
                緩存圖片 
                if let image = image, let originalData = originalData {
                    targetCache.store(image,
                                      original: originalData,
                                      forKey: key,
                                      processorIdentifier:options.processor.identifier,
                                      cacheSerializer: options.cacheSerializer,
                                      toDisk: !options.cacheMemoryOnly,
                                      completionHandler: nil)
                }

                completionHandler?(image, error, .none, url)

            })
    }
  • 優先從緩存獲取圖片,如緩存中沒有,在從網絡獲取圖片
    func tryToRetrieveImageFromCache(forKey key: String,
                                       with url: URL,
                              retrieveImageTask: RetrieveImageTask,
                                  progressBlock: DownloadProgressBlock?,
                              completionHandler: CompletionHandler?,
                                        options: KingfisherOptionsInfo?)
    {
        打破下面diskTask內部閉包保持的循環引用,完成之后取消磁盤任務引用,避免循環引用,釋放內存
        let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
            // Break retain cycle created inside diskTask closure below
            retrieveImageTask.diskRetrieveTask = nil
            completionHandler?(image, error, cacheType, imageURL)
        }
        
        let targetCache = options?.targetCache ?? cache
        let diskTask = targetCache.retrieveImage(forKey: key, options: options,
            completionHandler: { image, cacheType in
                if image != nil {
                     成功返回圖片
                    diskTaskCompletionHandler(image, nil, cacheType, url)
                } else if let options = options, options.onlyFromCache {
                    返回失敗 并且設置只從緩存獲取圖片 返回沒有緩存錯誤
                    let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
                    diskTaskCompletionHandler(nil, error, .none, url)
                } else {
                    返回失敗 再從網絡下載圖片
                    self.downloadAndCacheImage(
                        with: url,
                        forKey: key,
                        retrieveImageTask: retrieveImageTask,
                        progressBlock: progressBlock,
                        completionHandler: diskTaskCompletionHandler,
                        options: options)
                }
            }
        )
        retrieveImageTask.diskRetrieveTask = diskTask
    }

五、 KingfisherOptionsInfo

上面代碼多次用到options這個參數,它的參數類型是KingfisherOptionsInfo是一個類型別名
public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]
KingfisherOptionsInfoItem 是一個枚舉 配置 Kingfisher所有功能行為 下面是詳細中文注釋

public enum KingfisherOptionsInfoItem {

    這個成員的關聯值是一個ImageCache對象。 Kingfisher使用指定的緩存對象處理 相關業務,包括試圖檢索緩存圖像和存儲下載的圖片。
    case targetCache(ImageCache)

    這個成員的關聯值應該是一個ImageDownloader對象。Kingfisher將使用這個下載器下載的圖片。
    case downloader(ImageDownloader)

    如果從網絡下載的圖片 Kingfisher將使用“ImageTransition這個枚舉動畫。從內存或磁盤緩存時默認過渡不會發生。如果需要,設置ForceTransition
    case transition(ImageTransition)

    有關“浮動”值將被設置為圖像下載任務的優先級。值在0.0 ~ 1.0之間。如果沒有設置這個選項,默認值(“NSURLSessionTaskPriorityDefault”)將被使用。
    case downloadPriority(Float)

    如果設置,將忽略緩存,開啟一個下載任務的資源
    case forceRefresh
    
    如果設置 即使緩存的圖片也將開啟過渡動畫
    case forceTransition

    如果設置,Kingfisher只會在內存中緩存值而不是磁盤
    case cacheMemoryOnly

    如果設置 Kingfisher只會從緩存中加載圖片
    case onlyFromCache
    
    在使用之前在后臺線程解碼圖像
    case backgroundDecode
    
    當從緩存檢索圖像時 這個成員的關聯值將被用作目標隊列的調度時回調。如果沒 有設置, Kingfisher將使用主要quese回調
    case callbackDispatchQueue(DispatchQueue?)
    
    將檢索到的圖片數據轉換成一個圖時 這個成員變量將被用作圖片縮放因子。圖像分辨率,而不是屏幕尺寸。你可能處理時需要指定正確的縮放因子@2x或@3x Retina圖像。
    case scaleFactor(CGFloat)
    
    是否所有的GIF應該加載數據。默認false,只顯示GIF中第一張圖片。如果true,所有的GIF數據將被加載到內存中進行解碼。這個選項主要是用于內部的兼容性。你不應該把直接設置它。“AnimatedImageView”不會預加載所有數據,而一個正常的圖像視圖(“UIImageView”或“NSImageView”)將加載所有數據。選擇使用相應的圖像視圖類型而不是設置這個選項。
    case preloadAllGIFData
  
    發送請求之前用于改變請求。這是最后的機會你可以修改請求。您可以修改請求一些定制的目的,如添加身份驗證令牌頭,進行基本的HTTP身份驗證或類似的url映射。原始請求默認情況下將沒有任何修改
    case requestModifier(ImageDownloadRequestModifier)
    
    下載完成時,處理器會將下載的數據轉換為一個圖像。如果緩存連接到下載器(當你正在使用KingfisherManager或圖像擴展方法),轉換后的圖像也將被緩存
    case processor(ImageProcessor)
    
    提供一個CacheSerializer 可用于圖像對象序列化成圖像數據存儲到磁盤緩存和從磁盤緩存將圖片數據反序列化成圖像對象
    case cacheSerializer(CacheSerializer)
    
    保持現有的圖像同時設置另一個圖像圖像視圖。通過設置這個選項,imageview的placeholder參數將被忽略和當前圖像保持同時加載新圖片
    case keepCurrentImageWhileLoading
}

下面是自定義<== 運算符 比較兩個KingfisherOptionsInfoItem 是否相等 相等返回true 否則返回false

precedencegroup ItemComparisonPrecedence {
    associativity: none
    higherThan: LogicalConjunctionPrecedence
}

infix operator <== : ItemComparisonPrecedence

// This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values.
func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool {
    switch (lhs, rhs) {
    case (.targetCache(_), .targetCache(_)): return true
    case (.downloader(_), .downloader(_)): return true
    case (.transition(_), .transition(_)): return true
    case (.downloadPriority(_), .downloadPriority(_)): return true
    case (.forceRefresh, .forceRefresh): return true
    case (.forceTransition, .forceTransition): return true
    case (.cacheMemoryOnly, .cacheMemoryOnly): return true
    case (.onlyFromCache, .onlyFromCache): return true
    case (.backgroundDecode, .backgroundDecode): return true
    case (.callbackDispatchQueue(_), .callbackDispatchQueue(_)): return true
    case (.scaleFactor(_), .scaleFactor(_)): return true
    case (.preloadAllGIFData, .preloadAllGIFData): return true
    case (.requestModifier(_), .requestModifier(_)): return true
    case (.processor(_), .processor(_)): return true
    case (.cacheSerializer(_), .cacheSerializer(_)): return true
    case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true
    default: return false
    }
}

下面是對CollectionType的一個擴展 返回匹配的第一個相同枚舉值 上面過渡動畫就有用到

public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
    func firstMatchIgnoringAssociatedValue(_ target: Iterator.Element) -> Iterator.Element? {
        return index { $0 <== target }.flatMap { self[$0] }
    }
    
    func removeAllMatchesIgnoringAssociatedValue(_ target: Iterator.Element) -> [Iterator.Element] {
        return self.filter { !($0 <== target) }
    }
}

KingfisherOptionsInfo中有很多的類似的屬性get方法 如下是關于圖片編碼的,默認返回DefaultCacheSerializer.default。如果要自定義圖片編碼,可以添加自定義CacheSerializerOptions數組

   public var cacheSerializer: CacheSerializer {
        if let item = firstMatchIgnoringAssociatedValue(.cacheSerializer(DefaultCacheSerializer.default)),
            case .cacheSerializer(let cacheSerializer) = item
        {
            return cacheSerializer
        }
        return DefaultCacheSerializer.default
    }

結束

至此 ,我們對Kingfisher對整體架構已經有比較清晰的認識了 如下圖所示

Kingfisher.png

由于源代碼比較多,一些注釋都寫在代碼部分,可能看起來有點怪 用簡書也有段時間,但這還是第一次自己寫文章 接下來我會繼續學習下載模塊和緩存模塊的過程等等 如有錯誤,希望大家不吝指正

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容