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
里面包含了兩個屬性,cacheKey
和downloadURL
,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,diskRetrieveTask
和downloadTask
分別是“從磁盤獲取緩存圖片的任務”和“從網絡下載圖片的任務”,會分別在緩存模塊和下載模塊中用到,待會兒再細說。至于這個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
這個方法。取得了options
、targetCache
和downloader
之后,就要判斷用戶是否指定強制刷新,如果是則直接聯網下載,否則先從緩存中取數據,若沒有緩存再聯網下載。這一段我個人認為也稍微有點不符合直覺(我真不是處女座),喵神把“聯網下載”那一段邏輯單獨封裝成一個方法,因為就算不需要強制刷新,但緩存中若沒有數據的話,在“從緩存中取數據”這個任務的結束閉包中也還要進行下載操作,所以顯然可以把“聯網下載”的邏輯提取出來進行復用。這樣子的話,“聯網下載”被提取成一個方法,方法名清晰易懂,但“提取緩存”卻還有那么一大段在那兒,顯得不太對稱。要是把提取緩存
也封裝成一個方法,然后在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對整體架構已經有比較清晰的認識了,大概是這個樣子:
喵神是我第一個知道的iOS領域的大牛,我是從后端轉iOS的嘛,之前看完蘋果官方的《The Swift Programming Language》之后,就入手了喵神的《Swifter》,看完受益匪淺。最近想找點優秀的源碼讀一讀,第一時間就想到了Kingfisher。其實之前我并沒有用過這個庫(因為要兼容iOS7),在項目中只是自己簡單封裝了一下異步下載和緩存的過程,而且我只做了內存緩存,雖然勉強夠用了,但看了Kingfisher之后實在是覺得自己寫得非常簡陋。讀完了之后忍不住想記錄下來,先小結一下讀了上面這部分的收獲吧:
- 在系統設計方面有了一點心得
- 對軟件項目的規范也有了直接的體會(我身邊沒有人給我這方面的指點,一直都是看書跟自己摸索)
- Swift中關于
enum
和模式匹配的優雅用法讓我印象深刻
接下來我會繼續寫一下閱讀下載模塊和緩存模塊的過程,下載模塊中用到了很多GCD的新特性,緩存模塊主要是文件操作和對不同格式圖片的解碼操作等等,都非常值得學習。
下一篇地址:Kingfisher源碼閱讀(二)