Kingfisher源碼閱讀(一)

Kingfisher是喵神寫的一個異步下載和緩存圖片的Swift庫,github上將近3k的Star,相信不需要我再安利了。它的中文簡介在這里,github地址在這里

我始終覺得編程的精髓是抽象和模塊化。閱讀別人的代碼也應該先從大處著眼,從抽象層面最高的地方開始,自頂向下地逐模塊閱讀。我花了一個白天加兩個晚上認真地讀了一遍Kingfisher,加了一些中文注釋,本系列比較詳細地記錄了閱讀過程,所以可能會顯得有點啰嗦。

Kingfisher的文檔非常完備,我先大致看了一下,然后下載源碼,跑了一下demo。demo中有這么一段:

cell.cellImageView.kf_setImageWithURL(URL, placeholderImage: nil,
                                                optionsInfo: [.Transition(ImageTransition.Fade(1))],
                                              progressBlock: { receivedSize, totalSize in
                                                  print("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
                                              },
                                          completionHandler: { image, error, cacheType, imageURL in
                                                  print("\(indexPath.row + 1): Finished")
                                              }
    )

這個kf_setImageWithURL顯然是UIImage的一個extension方法,既然是暴露出來供庫的使用者調用的,應該就是抽象層面最高的。于是我command+click進去看了一下,它長這個樣子:

public func kf_setImageWithURL(URL: NSURL,
                      placeholderImage: UIImage?,
                           optionsInfo: KingfisherOptionsInfo?,
                         progressBlock: DownloadProgressBlock?,
                     completionHandler: CompletionHandler?) -> RetrieveImageTask
    {
        return kf_setImageWithResource(Resource(downloadURL: URL),
                            placeholderImage: placeholderImage,
                                 optionsInfo: optionsInfo,
                               progressBlock: progressBlock,
                           completionHandler: completionHandler)
    }

主要就是把傳過來的URL包裝成了一個Resource,然后調用kf_setImageWithResource方法。Resource里面包含了兩個屬性,cacheKeydownloadURL,cacheKey就是原URL的完整字符串,之后會作為緩存的鍵使用(內存緩存直接使用cacheKey作為NSCache的鍵,文件緩存把cacheKey進行MD5加密后的字符串作為緩存文件名)。下面再看看這個kf_setImageWithResource方法,它是這個UIImageView+Kingfisher.swift里的核心方法,其他還有一些提供給用戶使用的kf_setImageWithXXX的方法到最后都會調用它。kf_setImageWithResource里有這一句:

let task = KingfisherManager.sharedManager.retrieveImageWithResource(...)

它使用了KingfisherManager這個類,而這個類看名字就知道是整個庫的一個管理調度類。KingfisherManager.sharedManager,顯然是取KingfisherManaget的一個單例,Swift中的單例模式非常簡單,因為有let可以聲明imutable的屬性,不用擔心線程安全問題,只要在 KingfisherManager.swift里像這樣寫就行:

private let instance = KingfisherManager()
public class KingfisherManager {
    public class var sharedManager: KingfisherManager {
        return instance
    }
    ...
}

KingfisherManager的單例調用了retrieveImageWithResource,它整合了下載和緩存兩大功能,先看一下完整的方法簽名:

public func retrieveImageWithResource(resource: Resource,
        optionsInfo: KingfisherOptionsInfo?,
        progressBlock: DownloadProgressBlock?,
        completionHandler: CompletionHandler?) -> RetrieveImageTask

第一個參數類型Resource之前已經說過了,第二個參數類型KingfisherOptionsInfo?是什么呢?它是一個類型別名:public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem],而KingfisherOptionsInfoItem是一個enum

public enum KingfisherOptionsInfoItem {
    case Options(KingfisherOptions)
    case TargetCache(ImageCache)
    case Downloader(ImageDownloader)
    case Transition(ImageTransition)
}

這個枚舉的每個枚舉項都有關聯值,包含了很多信息。KingfisherOptions是一個自定義的Options,就是一個遵守OptionSetType協議的struct,里面有一些選項,可以對下載和緩存時的一些行為進行配置。TargetCache指定一個緩存器(ImageCache的一個實例),Downloader指定一個下載器(ImageDownloader的一個實例),Transition指定顯示圖片的動畫效果(提供淡入和從上下左右進入這5種效果,也可以傳入自定義效果)。

第三個參數類型是DownloadProgressBlock,也是一個別名:

//下載進度(參數:接收尺寸, 總尺寸)
public typealias DownloadProgressBlock = ((receivedSize: Int64, totalSize: Int64) -> ())`

實際上是一個閉包類型,具體會在什么時候調用待會兒會看到。第四個參數類型CompletionHandler也一樣是個閉包類型的別名:

public typealias CompletionHandler = ((image: UIImage?, error: NSError?, cacheType: CacheType, imageURL: NSURL?) -> ())

這個看名字就知道會在操作結束之后調用。

返回類型是RetrieveImageTask,它是長這樣的:

public class RetrieveImageTask {
    
    // If task is canceled before the download task started (which means the `downloadTask` is nil),
    // the download task should not begin.
    var cancelled: Bool = false
    
    var diskRetrieveTask: RetrieveImageDiskTask?
    var downloadTask: RetrieveImageDownloadTask?
    
    /**
    Cancel current task. If this task does not begin or already done, do nothing.
    */
    public func cancel() {
        // From Xcode 7 beta 6, the `dispatch_block_cancel` will crash at runtime.
        // It fixed in Xcode 7.1.
        // See https://github.com/onevcat/Kingfisher/issues/99 for more.
        if let diskRetrieveTask = diskRetrieveTask {
            dispatch_block_cancel(diskRetrieveTask)
        }
        
        if let downloadTask = downloadTask {
            downloadTask.cancel()
        }
        
        cancelled = true
    }
}

簡單來說它就是一個接收圖片的任務,它的內部有三個屬性,cancelled是個表明任務是否被取消的flag,diskRetrieveTaskdownloadTask分別是“從磁盤獲取緩存圖片的任務”和“從網絡下載圖片的任務”,會分別在緩存模塊和下載模塊中用到,待會兒再細說。至于這個cancel()方法么就是把上面說的兩個任務都取消,然后把取消flag設置為true

看完了retrieveImageWithResource的方法簽名,現在來看一下完整的方法,這個方法我認為是整個KingfisherManager的核心:

public func retrieveImageWithResource(resource: Resource,
    optionsInfo: KingfisherOptionsInfo?,
    progressBlock: DownloadProgressBlock?,
    completionHandler: CompletionHandler?) -> RetrieveImageTask
{
    //新建任務
    let task = RetrieveImageTask()
    
    // There is a bug in Swift compiler which prevents to write `let (options, targetCache) = parseOptionsInfo(optionsInfo)`
    // It will cause a compiler error.
    //解析optionsInfo
    let parsedOptions = parseOptionsInfo(optionsInfo)
    let (options, targetCache, downloader) = (parsedOptions.0, parsedOptions.1, parsedOptions.2)
    
    //若強制刷新則聯網下載并緩存
    if options.forceRefresh {
        downloadAndCacheImageWithURL(resource.downloadURL,
            forKey: resource.cacheKey,
            retrieveImageTask: task,
            progressBlock: progressBlock,
            completionHandler: completionHandler,
            options: options,
            targetCache: targetCache,
            downloader: downloader)
    } else {
        //不強制刷新則從緩存中取
        let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
            // Break retain cycle created inside diskTask closure below
            //完成之后取消任務引用,避免循環引用,釋放內存
            task.diskRetrieveTask = nil
            completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
        }
        let diskTask = targetCache.retrieveImageForKey(resource.cacheKey, options: options,
            completionHandler: { image, cacheType in
                if image != nil {
                    diskTaskCompletionHandler(image: image, error: nil, cacheType:cacheType, imageURL: resource.downloadURL)
                } else {
                    //沒有緩存則聯網下載并緩存
                    self.downloadAndCacheImageWithURL(resource.downloadURL,
                        forKey: resource.cacheKey,
                        retrieveImageTask: task,
                        progressBlock: progressBlock,
                        completionHandler: diskTaskCompletionHandler,
                        options: options,
                        targetCache: targetCache,
                        downloader: downloader)
                }
            }
        )
        task.diskRetrieveTask = diskTask
    }
    
    return task
}

幾個重要的點我加了中文注釋,應該很好理解。現在先來看一下parseOptionsInfo這個方法,它是用來解析optionsInfo的:

func parseOptionsInfo(optionsInfo: KingfisherOptionsInfo?) -> (Options, ImageCache, ImageDownloader) {
    //3個默認值
    var options = KingfisherManager.DefaultOptions
    var targetCache = self.cache
    var targetDownloader = self.downloader
    //用戶沒有指定的話則使用默認下載器、默認緩存器和默認配置。
    guard let optionsInfo = optionsInfo else {
        return (options, targetCache, targetDownloader)
    }
 
    //匹配各個枚舉類型,進行分別處理。擴展方法kf-findFirstMatch和重載運算符“==”配合,寫得很優雅(把"=="換成自定義其他操作符就更好了,"=="有點不符合直覺)。
    if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)), case .Options(let optionsInOptionsInfo) = optionsItem {
        //如果選項包含后臺回調,則使用一個新線程,否則使用默認queue(主線程)
        let queue = optionsInOptionsInfo.contains(KingfisherOptions.BackgroundCallback) ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) : KingfisherManager.DefaultOptions.queue
        //默認比例是1
        let scale = optionsInOptionsInfo.contains(KingfisherOptions.ScreenScale) ? UIScreen.mainScreen().scale : KingfisherManager.DefaultOptions.scale
        //打包options
        options = (forceRefresh: optionsInOptionsInfo.contains(KingfisherOptions.ForceRefresh),
            lowPriority: optionsInOptionsInfo.contains(KingfisherOptions.LowPriority),
            cacheMemoryOnly: optionsInOptionsInfo.contains(KingfisherOptions.CacheMemoryOnly),
            shouldDecode: optionsInOptionsInfo.contains(KingfisherOptions.BackgroundDecode),
            queue: queue, scale: scale)
    }
    
    if let optionsItem = optionsInfo.kf_findFirstMatch(.TargetCache(self.cache)), case .TargetCache(let cache) = optionsItem {
        targetCache = cache
    }
    
    if let optionsItem = optionsInfo.kf_findFirstMatch(.Downloader(self.downloader)), case .Downloader(let downloader) = optionsItem {
        targetDownloader = downloader
    }
    
    return (options, targetCache, targetDownloader)
}

其中:

if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)), case .Options(let optionsInOptionsInfo) = optionsItem

這個寫法讓我一時沒反應過來,愣了好一會兒,后來想起來在WWDC視頻上看到過Swfit2關于模式匹配的一些新內容,喵神的寫法應該是跟下面這個寫法等效的,只是喵神的更加簡潔優雅:

if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)) {
    switch optionsItem {
    case .Options(let optionsInOptionsInfo):
    let queue = ...
    ...
    }
}

我把源代碼注釋掉,改成上面這種形式跑了一下,發現沒有問題。

然后kf_findFirstMatch(.Options(.None)這個方法又讓我糾結了一陣,它是對CollectionType的一個擴展(給協議加擴展方法也是Swift2新特性),長這樣的:

extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {
    func kf_findFirstMatch(target: Generator.Element) -> Generator.Element? {
        //取得target的索引
        let index = indexOf {
            e in
            //這個"==",上面已經重載過了,只要類型相等就返回true,所以如果target是.Options(.None),e只要是.Options(_)都可以匹配,返回.Options(_)的索引
            return e == target
        }
        return (index != nil) ? self[index!] : nil
    }
}

現在我加了注釋大家應該看得明白了,這個函數會返回跟target同類型的元素的索引。之前我想當然地認為這個函數應該返回跟target相等元素的索引,比如kf_findFirstMatch(.Options(.None),應該要返回匹配到的.Options(.None)的索引,然而實際上,只要匹配到任意一個.Options(_),就可以返回它的索引了。因為==被這樣重載了:

func == (a: KingfisherOptionsInfoItem, b: KingfisherOptionsInfoItem) -> Bool {
    switch (a, b) {
    case (.Options(_), .Options(_)): return true
    case (.TargetCache(_), .TargetCache(_)): return true
    case (.Downloader(_), .Downloader(_)): return true
    case (.Transition(_), .Transition(_)): return true
    default: return false
    }
}

怎么說呢,總覺得不太符合直覺,索性自定義一個新的運算符可能更合適些,不容易造成誤解。

好了,接著往下看retrieveImageWithResource這個方法。取得了optionstargetCachedownloader之后,就要判斷用戶是否指定強制刷新,如果是則直接聯網下載,否則先從緩存中取數據,若沒有緩存再聯網下載。這一段我個人認為也稍微有點不符合直覺(我真不是處女座),喵神把“聯網下載”那一段邏輯單獨封裝成一個方法,因為就算不需要強制刷新,但緩存中若沒有數據的話,在“從緩存中取數據”這個任務的結束閉包中也還要進行下載操作,所以顯然可以把“聯網下載”的邏輯提取出來進行復用。這樣子的話,“聯網下載”被提取成一個方法,方法名清晰易懂,但“提取緩存”卻還有那么一大段在那兒,顯得不太對稱。要是把提取緩存也封裝成一個方法,然后在retrieveImageWithResource里調用,可能可讀性更好一些:

if options.forceRefresh {
    //若用戶指定強制刷新則直接聯網下載并緩存
    downloadAndCacheImageWithURL(resource.downloadURL,
        forKey: resource.cacheKey,
        retrieveImageTask: task,
        progressBlock: progressBlock,
        completionHandler: completionHandler,
        options: options,
        targetCache: targetCache,
        downloader: downloader)
} else {
    //不強制刷新則嘗試從緩存中取,若無緩存則聯網下載并緩存
    tryToRetrieveImageFromCacheForKey(resource.cacheKey,
        withURL: resource.downloadURL,
        retrieveImageTask: task,
        progressBlock: progressBlock,
        completionHandler: completionHandler,
        options: options,
        targetCache: targetCache,
        downloader: downloader)
}

相應地,tryToRetrieveImageFromCacheForKey長這樣:

func tryToRetrieveImageFromCacheForKey(key: String,
    withURL URL: NSURL,
    retrieveImageTask: RetrieveImageTask,
    progressBlock: DownloadProgressBlock?,
    completionHandler: CompletionHandler?,
    options: Options,
    targetCache: ImageCache,
    downloader: ImageDownloader)
{
    let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
        // Break retain cycle created inside diskTask closure below
        //完成之后取消任務引用,避免循環引用,釋放內存
        retrieveImageTask.diskRetrieveTask = nil
        completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
    }
    let diskTask = targetCache.retrieveImageForKey(key, options: options,
        completionHandler: { image, cacheType in
            if image != nil {
                diskTaskCompletionHandler(image: image, error: nil, cacheType:cacheType, imageURL: URL)
            } else {
                //沒有緩存則聯網下載并緩存
                self.downloadAndCacheImageWithURL(URL,
                    forKey: key,
                    retrieveImageTask: retrieveImageTask,
                    progressBlock: progressBlock,
                    completionHandler: diskTaskCompletionHandler,
                    options: options,
                    targetCache: targetCache,
                    downloader: downloader)
            }
        }
    )
    retrieveImageTask.diskRetrieveTask = diskTask
}

到這里為止,我們對Kingfisher對整體架構已經有比較清晰的認識了,大概是這個樣子:

Kingfisher.png

喵神是我第一個知道的iOS領域的大牛,我是從后端轉iOS的嘛,之前看完蘋果官方的《The Swift Programming Language》之后,就入手了喵神的《Swifter》,看完受益匪淺。最近想找點優秀的源碼讀一讀,第一時間就想到了Kingfisher。其實之前我并沒有用過這個庫(因為要兼容iOS7),在項目中只是自己簡單封裝了一下異步下載和緩存的過程,而且我只做了內存緩存,雖然勉強夠用了,但看了Kingfisher之后實在是覺得自己寫得非常簡陋。讀完了之后忍不住想記錄下來,先小結一下讀了上面這部分的收獲吧:

  • 在系統設計方面有了一點心得
  • 對軟件項目的規范也有了直接的體會(我身邊沒有人給我這方面的指點,一直都是看書跟自己摸索)
  • Swift中關于enum和模式匹配的優雅用法讓我印象深刻

接下來我會繼續寫一下閱讀下載模塊和緩存模塊的過程,下載模塊中用到了很多GCD的新特性,緩存模塊主要是文件操作和對不同格式圖片的解碼操作等等,都非常值得學習。

下一篇地址:Kingfisher源碼閱讀(二)

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

推薦閱讀更多精彩內容

  • 序言 Kingfisher是喵神的一個異步下載和緩存圖片的Swift庫,類似于OC 的SDWebImage中文簡介...
    喬克_叔叔閱讀 6,210評論 7 25
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,232評論 4 61
  • 對誰都不要說,好嗎?清晨庭院的角落里花兒悄悄落淚的事。萬一這件事說出去傳到蜜蜂的耳朵里,它會像做了虧心事一樣飛回去...
    沒頭腦和不高閱讀 599評論 0 0
  • 伏,伏牛路小學的伏,這個伏字,左邊一個單人旁,右邊一個犬字。非常形象地寫出來了一個蓄勢待發的狗在隨時聽侯主人的命令...
    帶眼鏡閱讀 491評論 0 1
  • 。。
    彩虹1981閱讀 199評論 0 0