Kingfisher學(xué)習(xí)筆記

Kingfisher

Kingfisher是一個(gè)使用Swift編寫的用于下載和緩存圖片的iOS庫,是作者王巍受SDWebImage的啟發(fā)開發(fā)了這個(gè)純Swift的庫。Kingfisher的完整特性可以從下面的鏈接中獲取,本文主要是學(xué)習(xí)項(xiàng)目源碼的一些心得。
https://github.com/onevcat/Kingfisher

kf_xxx >> kf.xxx

參考 [RxCocoa] Move from rx_ prefix to a rx. proxy (for Swift 3 update ?

傳統(tǒng)的Objective-C語法對(duì)于擴(kuò)展的method,推薦使用kf_xxx方式命名,但是這種命名不太符合Swift的風(fēng)格,且看起來很丑。因此越來越多的Swift項(xiàng)目開始參考LazySequence模式,使用類似下面的風(fēng)格:

myArray.map { ... }
myArray.lazy.map { ... }

Kingfisher也由kf_xxx轉(zhuǎn)向了kf.xxx風(fēng)格,如imageView.kf.indicatorType = .activity。要實(shí)現(xiàn)這個(gè)機(jī)制,需要以下幾步:

  • 實(shí)現(xiàn)一個(gè)作為Proxy的class/struct

    這個(gè)Proxy僅將真實(shí)的對(duì)象包裹起來,不做任何實(shí)際的操作。Kingfisher使用的Kingfisher這個(gè)class,定義如下:

    public final class Kingfisher<Base> {
        public let base: Base
        public init(_ base: Base) {
            self.base = base
        }
    }
    
  • 定義一個(gè)Protocol用于提供.kf的方法

    Kingfisher是使用了KingfisherCompatible,它有兩個(gè)關(guān)鍵點(diǎn):

    • 定義一個(gè)名為kf的property。這不是必須的,實(shí)際上KingfisherCompatible可以為一個(gè)空protocol,只要我們實(shí)現(xiàn)了下一步即可。
    • 提供一個(gè)kf的默認(rèn)實(shí)現(xiàn),返回一個(gè)新建的Proxy對(duì)象,即Kingfisher對(duì)象

    定義如下:

     /**
     A type that has Kingfisher extensions.
     */
    public protocol KingfisherCompatible {
        associatedtype CompatibleType
        var kf: CompatibleType { get }
    }
    
    public extension KingfisherCompatible {
        public var kf: Kingfisher<Self> {
            get { return Kingfisher(self) }
        }
    }
    

    討論

    個(gè)人覺得,KingfisherCompatible采用下面的定義方式更好,即刪除Protocol中kf的聲明,僅在Extension中提供一個(gè)默認(rèn)的Implementation。因?yàn)槿魧?code>kf的聲明留在Protocol,那么其他Type繼承這個(gè)Protocol后,是可以修改kf的Implementation!從作者的意圖上講,他應(yīng)該不希望這種事情發(fā)生。

    public protocol KingfisherCompatible {
    }
    
    public extension KingfisherCompatible {
        public var kf: Kingfisher<Self> {
            get { return Kingfisher(self) }
        }
    }
    
  • 將Protocol加載到所需的Base類上
    定義一個(gè)Base類的extension即可。比如使用下面的代碼后,我們就可以通過使用諸如imageView.kf的代碼了。

    extension Image: KingfisherCompatible { }
    extension ImageView: KingfisherCompatible { }
    extension Button: KingfisherCompatible { }
    
  • 通過Extension + where Base實(shí)現(xiàn)Base類的特定代碼

    比如實(shí)現(xiàn)下面的代碼后,我們就可以通過調(diào)用imageView.kf.webURL獲取imageView的webURL了。
    需要注意的是,使用objc_getAssociatedObjectobjc_setAssociatedObject時(shí),一定與base關(guān)聯(lián),而不是self!因?yàn)閺?code>.kf的實(shí)現(xiàn)中也可以知道,每次調(diào)用imageView.kf時(shí),實(shí)際上返回的都是一個(gè)全新Kingfisher

    extension Kingfisher where Base: ImageView {
        public var webURL: URL? {
            return objc_getAssociatedObject(base, &lastURLKey) as? URL
        }
        
        fileprivate func setWebURL(_ url: URL?) {
            objc_setAssociatedObject(base, &lastURLKey, url, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    

ImageDownloader

ImageDownloader是這個(gè)庫的兩大核心之一,另一個(gè)是Cache。

ImageDownloader represents a downloading manager for requesting the image with a URL from server.

ImageFetchLoad

它是ImageDownloader的一個(gè)內(nèi)部class,主要用于避免一個(gè)URL的資源被同時(shí)下載多次。定義如下:

class ImageFetchLoad {
    var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
    var responseData = NSMutableData()

    var downloadTaskCount = 0
    var downloadTask: RetrieveImageDownloadTask?
    var cancelSemaphore: DispatchSemaphore?
}

這個(gè)類通過ImageDownloader.fetchLoads與某個(gè)URL掛鉤。對(duì)于一個(gè)URL,如果用戶反復(fù)下載,

  • 若正在下載這個(gè)URL的資源,則用戶試圖再次下載時(shí),ImageDownloader不會(huì)真的下載,而是令I(lǐng)mageFetchLoad.downloadTaskCount++。

  • 若上一次的下載已經(jīng)結(jié)束,這個(gè)URL會(huì)被再次下載。因?yàn)橐淮蜗螺d結(jié)束時(shí)會(huì)通過processImage >> cleanFetchLoad將這個(gè)URL對(duì)應(yīng)的ImageFetchLoad清除。

Properties

fetchLoads

是一個(gè)[URL: ImageFetchLoad],如上所說,用于記錄URL和ImageFetchLoad的關(guān)系。

存在3個(gè)不同的DispatchQueue

  1. barrierQueue

    • concurrent
    • 用于thread safe地讀寫fetchLoads。

    討論
    concurrent + barrier的組合可以用來解決多線程的讀寫問題時(shí),通常是如下的代碼,對(duì)寫操作使用barrier,對(duì)于讀操作

  • processQueue:
    • concurrent
    • 數(shù)據(jù)下載完成后,用于在后臺(tái)處理數(shù)據(jù),避免阻塞mian thread
  • cancelQueue
    • serial
    • 跟ImageFetchLoad.cancelSemaphore有關(guān)。從目前來看,僅當(dāng)一個(gè)URL的fetchLoad被創(chuàng)建 && 還沒來得及開始下載(downloadTaskCount == 0)時(shí),若該URL又有一個(gè)下載請(qǐng)求過來了,我們會(huì)在cancelQueue中讓其等待起來。若之前的那個(gè)下載請(qǐng)求失敗了,才會(huì)啟動(dòng)本次的下載。
if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
    if fetchLoad.cancelSemaphore == nil {
        fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
    }
    cancelQueue.async {
        _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
        fetchLoad.cancelSemaphore = nil
        prepareFetchLoad()
    }
} else {
    prepareFetchLoad()
}
private func callCompletionHandlerFailure(error: Error, url: URL) {
    guard let downloader = downloadHolder, let fetchLoad = downloader.fetchLoad(for: url) else {
        return
    }

    // We need to clean the fetch load first, before actually calling completion handler.
    cleanFetchLoad(for: url)

    var leftSignal: Int
    repeat {
        leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0
    } while leftSignal != 0

    for content in fetchLoad.contents {
        content.options.callbackDispatchQueue.safeAsync {
            content.callback.completionHandler?(nil, error as NSError, url, nil)
        }
    }
}

sessionHandler

用于實(shí)現(xiàn)URLSessionDataDelegate,并避免由于session導(dǎo)致的retain cycle。https://github.com/onevcat/Kingfisher/issues/235

APIs

downloadImage()

對(duì)外的API主要是這個(gè)方法,定義如下:

    /**
     Download an image with a URL and option.
     
     - parameter url:               Target URL.
     - parameter retrieveImageTask: The task to cooporate with cache. Pass `nil` if you are not trying to use downloader and cache.
     - parameter options:           The options could control download behavior. See `KingfisherOptionsInfo`.
     - parameter progressBlock:     Called when the download progress updated.
     - parameter completionHandler: Called when the download progress finishes.
     
     - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
     */
    @discardableResult
    open func downloadImage(with url: URL,
                       retrieveImageTask: RetrieveImageTask? = nil,
                       options: KingfisherOptionsInfo? = nil,
                       progressBlock: ImageDownloaderProgressBlock? = nil,
                       completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?

ImageCache

ImageCache是這個(gè)庫的兩大核心之一,另一個(gè)是ImageDownloader。

ImageCache represents both the memory and disk cache system of Kingfisher. While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create your own cache object and configure it as your need. You could use an ImageCache object to manipulate memory and disk cache for Kingfisher.

ImageCache的實(shí)現(xiàn)機(jī)制基本上跟SDWebImage的SDImageCache一模一樣。

Cache添加和訪問機(jī)制

ImageCache用到了2個(gè)單獨(dú)的DispatchQueue,以免堵塞主線程。

  • ioQueue
    • serial
    • 用于處理disk讀寫相關(guān)的操作。
  • processQueue
    • concurrent
    • 用于對(duì)下載的數(shù)據(jù)做decode。

Cache的添加機(jī)制

對(duì)外的API是這個(gè)方法,定義如下:

    /**
    Store an image to cache. It will be saved to both memory and disk. It is an async operation.
    
    - parameter image:             The image to be stored.
    - parameter original:          The original data of the image.
                                   Kingfisher will use it to check the format of the image and optimize cache size on disk.
                                   If `nil` is supplied, the image data will be saved as a normalized PNG file.
                                   It is strongly suggested to supply it whenever possible, to get a better performance and disk usage.
    - parameter key:               Key for the image.
    - parameter identifier:        The identifier of processor used. If you are using a processor for the image, pass the identifier of
                                   processor to it.
                                   This identifier will be used to generate a corresponding key for the combination of `key` and processor.
    - parameter toDisk:            Whether this image should be cached to disk or not. If false, the image will be only cached in memory.
    - parameter completionHandler: Called when store operation completes.
    */
    open func store(_ image: Image,
                      original: Data? = nil,
                      forKey key: String,
                      processorIdentifier identifier: String = "",
                      cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
                      toDisk: Bool = true,
                      completionHandler: (() -> Void)? = nil)

添加步驟如下:

  1. 首先將圖片緩存到內(nèi)存中。Kingfisher使用了系統(tǒng)自帶的NSCache作為內(nèi)存的緩存,添加緩存時(shí),以key(圖片的網(wǎng)址)作為key,image作為value。
  2. toDisk為true,則在ioQueue中將圖片緩存到硬盤中。步驟如下:
    1. 首先將參數(shù)中的imageUIImage/NSImage轉(zhuǎn)化為Data,參數(shù)中的original在這個(gè)過程中會(huì)起到輔助作用,用于判斷圖片的類型(PNG,GIF,JPG等)。
    2. 然后將上面生成的Data存儲(chǔ)到硬盤中,文件名為圖片網(wǎng)址的md5值。

Cache的訪問機(jī)制

對(duì)外的API是這個(gè)方法,定義如下:

    /**
    Get an image for a key from memory or disk.
    
    - parameter key:               Key for the image.
    - parameter options:           Options of retrieving image. If you need to retrieve an image which was 
                                   stored with a specified `ImageProcessor`, pass the processor in the option too.
    - parameter completionHandler: Called when getting operation completes with image result and cached type of 
                                   this image. If there is no such key cached, the image will be `nil`.
    
    - returns: The retrieving task.
    */
    @discardableResult
    open func retrieveImage(forKey key: String,
                               options: KingfisherOptionsInfo?,
                     completionHandler: ((Image?, CacheType) -> Void)?) -> RetrieveImageDiskTask?

訪問步驟如下:

  1. 首先以圖片的網(wǎng)址為key,去內(nèi)存的緩存中搜索,若存在,則直接返回這個(gè)值,并退出函數(shù);若不存在,則在硬盤上進(jìn)行搜索。
  2. 以緩存文件夾的Path + 圖片網(wǎng)址的md5值 生成一個(gè)文件路徑,讀取該路徑的數(shù)據(jù),并生成圖片。若成功了,則將這個(gè)圖片緩存到內(nèi)存中,以便下次訪問,然后返回這個(gè)圖片;若失敗了,則返回nil。

Cache清理機(jī)制

清理的時(shí)機(jī)

ImageCache在init方法中監(jiān)聽了下列事件:

  • UIApplicationDidReceiveMemoryWarning:清空內(nèi)存的所有緩存,調(diào)用clearMemoryCache()。
  • UIApplicationWillTerminate:清理硬盤的緩存,調(diào)用:cleanExpiredDiskCache()。
  • UIApplicationDidEnterBackground:清理硬盤的緩存,調(diào)用backgroundCleanExpiredDiskCache()

硬盤緩存的清理機(jī)制

  1. 使用FileManager.contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: FileManager.DirectoryEnumerationOptions = [])遍歷整個(gè)緩存文件夾,獲取到每個(gè)文件的這些屬性值:.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey。需要注意的是,這里獲取的是文件的訪問時(shí)間,而不是修改時(shí)間?。⊿DWebImage是獲取修改時(shí)間)。
  2. 對(duì)比"訪問時(shí)間"和緩存的有效時(shí)間,刪除過期的圖片。這里可以看出,Kingfisher使用“訪問時(shí)間”,而不是“修改時(shí)間”是更加合理的。畢竟,按SDWebImage的邏輯,若一個(gè)圖片很早之前就下載了,即便最近一直在被頻繁訪問,還是會(huì)被清除。
  3. 經(jīng)過上一輪的清理,若緩存圖片的總體size依然大于用戶設(shè)定的上限(如果設(shè)置了),那么就將剩下的圖片按“訪問時(shí)間”排序,逐個(gè)刪除舊的圖片,直到總體size小于用戶上限的一半為止。

ImageProcessor

An ImageProcessor would be used to convert some downloaded data to an image.

它是一個(gè)Protocol,用于將下載的數(shù)據(jù)轉(zhuǎn)換為image,定義如下:

public protocol ImageProcessor {
    /// Identifier of the processor.
    var identifier: String { get }
    
    /// Process an input `ImageProcessItem` item to an image for this processor.
    ///
    /// - parameter item:    Input item which will be processed by `self`
    /// - parameter options: Options when processing the item.
    ///
    /// - returns: The processed image.
    ///
    /// - Note: The return value will be `nil` if processing failed while converting data to image.
    ///         If input item is already an image and there is any errors in processing, the input 
    ///         image itself will be returned.
    /// - Note: Most processor only supports CG-based images. 
    ///         watchOS is not supported for processers containing filter, the input image will be returned directly on watchOS.
    func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image?
}

Kingfisher提供了以下默認(rèn)的實(shí)現(xiàn)。

DefaultImageProcessor

The default processor. It convert the input data to a valid image. Images of .PNG, .JPEG and .GIF format are supported. If an image is given, DefaultImageProcessor will do nothing on it and just return that image.

public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
    switch item {
    case .image(let image):
        return image
    case .data(let data):
        return Kingfisher<Image>.image(
            data: data,
            scale: options.scaleFactor,
            preloadAllAnimationData: options.preloadAllAnimationData,
            onlyFirstFrame: options.onlyLoadFirstFrame)
    }
}

GeneralProcessor

private class。主要用于將兩個(gè)ImageProcessor合并起來。這個(gè)合并很精巧,它定義了一個(gè)內(nèi)部block p,初始化時(shí)p會(huì)hold住兩個(gè)舊的ImageProcessor,process時(shí)會(huì)逐一使用它們。

public extension ImageProcessor {
    
    /// Append an `ImageProcessor` to another. The identifier of the new `ImageProcessor` 
    /// will be "\(self.identifier)|>\(another.identifier)".
    ///
    /// - parameter another: An `ImageProcessor` you want to append to `self`.
    ///
    /// - returns: The new `ImageProcessor` will process the image in the order
    ///            of the two processors concatenated.
    public func append(another: ImageProcessor) -> ImageProcessor {
        let newIdentifier = identifier.appending("|>\(another.identifier)")
        return GeneralProcessor(identifier: newIdentifier) {
            item, options in
            if let image = self.process(item: item, options: options) {
                return another.process(item: .image(image), options: options)
            } else {
                return nil
            }
        }
    }
}

typealias ProcessorImp = ((ImageProcessItem, KingfisherOptionsInfo) -> Image?)

fileprivate struct GeneralProcessor: ImageProcessor {
    let identifier: String
    let p: ProcessorImp
    func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
        return p(item, options)
    }
}

其他ImageProcessor

其他的ImageProcessor包括:

  • RoundCornerImageProcessor
  • ResizingImageProcessor
  • BlurImageProcessor
  • OverlayImageProcessor
  • TintImageProcessor
  • ColorControlsProcessor
  • BlackWhiteProcessor
  • CroppingImageProcessor

以上這些Processor,對(duì)于輸入的ImageProcessItem,如果item

  • 類型為image,則直接調(diào)用Image.kf的相關(guān)代碼
  • 類型為data,則先使用DefaultImageProcessor將其轉(zhuǎn)換為image,再調(diào)用Image.kf的相關(guān)代碼

Tips

Collection

  • Collection可以相加。
    如[1, 2, 3] + [4, 5] >> [1, 2, 3, 4, 5]
  • 通過在Extension中設(shè)定Collection的Iterator.Element,可以實(shí)現(xiàn)很多便利的方法。
    比如實(shí)現(xiàn)下面的extension后,我們就可以直接使用options.targetCache.
public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
    /// The target `ImageCache` which is used.
    public var targetCache: ImageCache {
        if let item = lastMatchIgnoringAssociatedValue(.targetCache(.default)),
            case .targetCache(let cache) = item
        {
            return cache
        }
        return ImageCache.default
    }
}
  • 巧用ArraySlice取代Array
    ImagePrefetcher的pendingResources用于記錄prefetchResources中還有多少?zèng)]有fetch,因此我們可以將它定義為ArraySlice,而非Array。這么做的好處在與可以避免沒必要的復(fù)制。
public init(resources: [Resource],
       options: KingfisherOptionsInfo? = nil,
       progressBlock: PrefetcherProgressBlock? = nil,
       completionHandler: PrefetcherCompletionHandler? = nil)
{
    prefetchResources = resources
    pendingResources = ArraySlice(resources)
    。。。。
}


public func start()
{
    。。。
    let initialConcurentDownloads = min(self.prefetchResources.count, self.maxConcurrentDownloads)
    for _ in 0 ..< initialConcurentDownloads {
        if let resource = self.pendingResources.popFirst() {
            self.startPrefetching(resource)
        }
    }
}

DispatchQueue

safyAsync

直接調(diào)用DispatchQueue.async(block)時(shí),并不能保證block被執(zhí)行的時(shí)間,我們?cè)谥骶€程上調(diào)用async通常是為了更新UI,直接調(diào)用block會(huì)更及時(shí)。

extension DispatchQueue {
    // This method will dispatch the `block` to self.
    // If `self` is the main queue, and current thread is main thread, the block
    // will be invoked immediately instead of being dispatched.
    func safeAsync(_ block: @escaping ()->()) {
        if self === DispatchQueue.main && Thread.isMainThread {
            block()
        } else {
            async { block() }
        }
    }
}

ioQueue && processQueue

對(duì)于會(huì)消耗比較多時(shí)間的操作,比如文件的io和圖片的process,我們可以使用單獨(dú)的Queue來進(jìn)行操作,這樣可以避免阻塞主線程。
在使用這種線程時(shí),可以搭配.barrier來解決Reader-Writer問題。

QA:

  • ImageDownloader.barrierQueue是一個(gè)conccurrent queue,但每次使用都用.barrier在修飾,那concurrent的意義在哪里?為什么不直接定義為series queue?
最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 序言 Kingfisher是喵神的一個(gè)異步下載和緩存圖片的Swift庫,類似于OC 的SDWebImage中文簡介...
    喬克_叔叔閱讀 6,273評(píng)論 7 25
  • 技術(shù)無極限,從菜鳥開始,從源碼開始。 由于公司目前項(xiàng)目還是用OC寫的項(xiàng)目,沒有升級(jí)swift 所以暫時(shí)SDWebI...
    充滿活力的早晨閱讀 12,848評(píng)論 0 2
  • 5.SDWebImageDownloader 下面分析這個(gè)類看這個(gè)類的結(jié)構(gòu) 這個(gè)類的屬性比較多。 先看這個(gè)類的pu...
    充滿活力的早晨閱讀 1,220評(píng)論 0 0
  • 一、ImageDownloader 在Kingfisher中,該類主要負(fù)責(zé)圖片的網(wǎng)絡(luò)下載,其實(shí)現(xiàn)原理是基于系統(tǒng)的U...
    喬克_叔叔閱讀 1,482評(píng)論 5 5
  • 已婚婦女怎么個(gè)良法?賢妻良母。 怎么算賢妻?具備傳統(tǒng)美德。 但對(duì)這個(gè)時(shí)代的男人來說,不,應(yīng)該說在任何時(shí)代,老婆具備...
    6db8b93047b7閱讀 945評(píng)論 5 4

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