Kingfisher是喵神寫的一個異步下載和緩存圖片的Swift庫,github上將近3k的Star,相信不需要我再安利了。它的中文簡介在這里,github地址在這里。
本次我們研究的是最新的基于Swift 4
Kingfisher的文檔非常完備,我先大致看了一下,然后下載源碼,跑了一下demo。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(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_setImage顯然是UIImageView的一個extension方法,既然是暴露出來供庫的使用者調(diào)用的,應該就是抽象層面最高的。于是我command+click進去看了一下,它長這個樣子,有點長,讓我們分析下
@discardableResult // 改關(guān)鍵字意思是聲明,告訴編譯器此方法可以不用接收返回值。
public func setImage(with resource: Resource?,
placeholder: Placeholder? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: DownloadProgressBlock? = nil,
completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
{
guard let resource = resource else {
self.placeholder = placeholder
setWebURL(nil)
completionHandler?(nil, nil, .none, nil)
return .empty
}
var options = KingfisherManager.shared.defaultOptions + (options ?? KingfisherEmptyOptionsInfo)
let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet { // Always set placeholder while there is no image/placehoer yet.
self.placeholder = placeholder
}
let maybeIndicator = indicator
maybeIndicator?.startAnimatingView()
setWebURL(resource.downloadURL)
if base.shouldPreloadAllAnimation() {
options.append(.preloadAllAnimationData)
}
/**
代碼塊A (方便解說, 將代碼拆開, 后面同樣標記,標識同一位置代碼)
/
}
第一個參數(shù) resource Resource協(xié)議里面包含了兩個屬性,cacheKey和downloadURL,cacheKey就是原URL的完整字符串,之后會作為緩存的鍵使用(內(nèi)存緩存直接使用cacheKey作為NSCache的鍵,文件緩存把cacheKey進行MD5加密后的字符串作為緩存文件名)
第二個參數(shù)類型KingfisherOptionsInfo?是什么呢?它是一個類型別名:public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem],而KingfisherOptionsInfoItem是一個enum:
public enum KingfisherOptionsInfoItem {
case targetCache(ImageCache)
case downloader(ImageDownloader)
case transition(ImageTransition)
case downloadPriority(Float)
case forceRefresh
case forceTransition
case cacheMemoryOnly
case onlyFromCache
case backgroundDecode
case callbackDispatchQueue(DispatchQueue?)
case scaleFactor(CGFloat)
case preloadAllAnimationData
case requestModifier(ImageDownloadRequestModifier)
case processor(ImageProcessor)
case cacheSerializer(CacheSerializer)
case keepCurrentImageWhileLoading
case onlyLoadFirstFrame
case cacheOriginalImage
}
這個枚舉的每個枚舉項都有關(guān)聯(lián)值,包含了很多信息
TargetCache指定一個緩存器(ImageCache的一個實例),Downloader指定一個下載器(ImageDownloader的一個實例),Transition指定顯示圖片的動畫效果(提供淡入和從上下左右進入這5種效果,也可以傳入自定義效果)。
第三個參數(shù)類型是DownloadProgressBlock,也是一個別名:
public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> ())
實際上是一個閉包類型,具體會在什么時候調(diào)用待會兒會看到。第四個參數(shù)類型CompletionHandler也一樣是個閉包類型的別名:
public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> ())
這個看名字就知道會在操作結(jié)束之后調(diào)用。
返回類型是RetrieveImageTask,它是長這樣的:
public class RetrieveImageTask {
public static let empty = RetrieveImageTask()
var cancelledBeforeDownloadStarting: Bool = false
public var downloadTask: RetrieveImageDownloadTask?
/**
Cancel current task. If this task is already done, do nothing.
*/
public func cancel() {
if let downloadTask = downloadTask {
downloadTask.cancel()
} else {
cancelledBeforeDownloadStarting = true
}
}
}
簡單來說它就是一個接收圖片的任務
/**
代碼塊A
/
let task = KingfisherManager.shared.retrieveImage(
with: resource,
options: options,
progressBlock: { receivedSize, totalSize in
guard resource.downloadURL == self.webURL else {
return
}
if let progressBlock = progressBlock {
progressBlock(receivedSize, totalSize)
}
},
completionHandler: {[weak base] image, error, cacheType, imageURL in
DispatchQueue.main.safeAsync {
maybeIndicator?.stopAnimatingView()
guard let strongBase = base, imageURL == self.webURL else {
completionHandler?(image, error, cacheType, imageURL)
return
}
self.setImageTask(nil)
guard let image = image else {
completionHandler?(nil, error, cacheType, imageURL)
return
}
guard let transitionItem = options.lastMatchIgnoringAssociatedValue(.transition(.none)),
case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
{
self.placeholder = nil
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
self.placeholder = nil
UIView.transition(with: strongBase, duration: transition.duration,
options: [transition.animationOptions, .allowUserInteraction],
animations: {
// Set image property in the animation.
transition.animations?(strongBase, image)
},
completion: { finished in
transition.completion?(finished)
completionHandler?(image, error, cacheType, imageURL)
})
})
#endif
}
})
setImageTask(task)
return task
KingfisherManager 是個單利, swift 創(chuàng)建單利十分簡單
public static let shared = KingfisherManager()
KingfisherManager 的單利調(diào)用了 retrieveImage 它整合了下載和緩存兩大功能,先看一下完整的方法簽名, 認為是整個KingfisherManager的核心:
@discardableResult
public func retrieveImage(with resource: Resource,
options: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask
{
// 新建任務
let task = RetrieveImageTask()
let options = currentDefaultOptions + (options ?? KingfisherEmptyOptionsInfo)
//若強制刷新則聯(lián)網(wǎng)下載并緩存
if 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
}
分析從緩存中獲取 tryToRetrieveImageFromCache
//不強制刷新則從緩存中取
func tryToRetrieveImageFromCache(forKey key: String,
with url: URL,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo)
{
let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
completionHandler?(image, error, cacheType, imageURL)
}
func handleNoCache() {
if options.onlyFromCache {
let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
diskTaskCompletionHandler(nil, error, .none, url)
return
}
self.downloadAndCacheImage(
with: url,
forKey: key,
retrieveImageTask: retrieveImageTask,
progressBlock: progressBlock,
completionHandler: diskTaskCompletionHandler,
options: options)
}
let targetCache = options.targetCache
// First, try to get the exactly image from cache
targetCache.retrieveImage(forKey: key, options: options) { image, cacheType in
// If found, we could finish now.
if image != nil {
diskTaskCompletionHandler(image, nil, cacheType, url)
return
}
// If not found, and we are using a default processor, download it!
let processor = options.processor
guard processor != DefaultImageProcessor.default else {
handleNoCache()
return
}
// If processor is not the default one, we have a chance to check whether
// the original image is already in cache.
let optionsWithoutProcessor = options.removeAllMatchesIgnoringAssociatedValue(.processor(processor))
targetCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { image, cacheType in
// If we found the original image, there is no need to download it again.
// We could just apply processor to it now.
guard let image = image else {
handleNoCache()
return
}
guard let processedImage = processor.process(item: .image(image), options: options) else {
diskTaskCompletionHandler(nil, nil, .none, url)
return
}
targetCache.store(processedImage,
original: nil,
forKey: key,
processorIdentifier:options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: !options.cacheMemoryOnly,
completionHandler: nil)
diskTaskCompletionHandler(processedImage, nil, .none, url)
}
}
}
開始下載任務
上次說到了downloadAndCacheImage這個方法,看名字就知道既要下載圖片又要緩存圖片,它的方法體是這樣的:
@discardableResult
func downloadAndCacheImage(with url: URL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
{
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
downLoader 調(diào)用了downloadImage方法然后在completionHandler這個完成閉包中做緩存相關(guān)的操作,我們先不管緩存,先去downloadImage(downloader是它的一個實例)里看看downloadImage這個方法,它是長這樣的:
@discardableResult
open func downloadImage(with url: URL,
retrieveImageTask: RetrieveImageTask? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: ImageDownloaderProgressBlock? = nil,
completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
{
if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
return nil
}
// 設置請求超時時間
let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
// We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
// 創(chuàng)建request 忽略本地和遠程的緩存數(shù)據(jù),直接從原始地址下
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
// 請求和響應是順序的, 也就是說請求–>得到響應后,再請求
request.httpShouldUsePipelining = requestsUsePipelining
if let modifier = options?.modifier {
guard let r = modifier.modified(for: request) else {
completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
return nil
}
request = r
}
// There is a possiblility that request modifier changed the url to `nil` or empty.
// 請求被修改空的可能性
guard let url = request.url, !url.absoluteString.isEmpty else {
completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
return nil
}
var downloadTask: RetrieveImageDownloadTask?
setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
if fetchLoad.downloadTask == nil {
let dataTask = session.dataTask(with: request)
fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
dataTask.resume()
self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
// Hold self while the task is executing.
self.sessionHandler.downloadHolder = self
}
fetchLoad.downloadTaskCount += 1
downloadTask = fetchLoad.downloadTask
retrieveImageTask?.downloadTask = downloadTask
}
return downloadTask
}
里面有setup 方法 這個方法之前的部分都是發(fā)送網(wǎng)絡請求之前的處理
func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
func prepareFetchLoad() {
barrierQueue.sync(flags: .barrier) {
let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
fetchLoads[url] = loadObjectForURL
if let session = session {
started(session, loadObjectForURL)
}
}
}
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()
}
}
這個fetchLoads是一個以URL為鍵,ImageFetchLoad為值的Dictionary,ImageFetchLoad是ImageDownloader中的一個內(nèi)部類,它的聲明如下
class ImageFetchLoad {
var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
var responseData = NSMutableData()
var downloadTaskCount = 0
var downloadTask: RetrieveImageDownloadTask?
var cancelSemaphore: DispatchSemaphore?
}
//先是用圖片的URL去self.fetchLoads里取對應的ImageFetchLoad, 如果沒有的話就以當前URL為鍵創(chuàng)建一個,然后把傳過來的progressBlock和completionHandler打包成一個元組,和options組成新元素, 添加到ImageFetchLoad里的contents數(shù)組中, 準備好之后,在閉包里面開始下載
if fetchLoad.downloadTask == nil {
let dataTask = session.dataTask(with: request)
fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
dataTask.resume()
self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
// Hold self while the task is executing.
self.sessionHandler.downloadHolder = self
}
fetchLoad.downloadTaskCount += 1
downloadTask = fetchLoad.downloadTask
retrieveImageTask?.downloadTask = downloadTask
這里使用了NSURLSession,是iOS7之后比較主流的用于網(wǎng)絡請求的API(iOS7以前多使用NSURLConnection)
ImageDownloaderSessionHandler 實現(xiàn)URLSessionDataDelegate 代理
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let downloader = downloadHolder else {
return
}
if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
//向fetchLoads[URL].responseData添加一條響應數(shù)據(jù)
fetchLoad.responseData.append(data)
if let expectedLength = dataTask.response?.expectedContentLength {
for content in fetchLoad.contents {
//依次調(diào)用fetchLoads的contents中的所有過程回調(diào)
DispatchQueue.main.async {
content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
}
}
}
}
}
這個函數(shù)會在接收到數(shù)據(jù)的時候被調(diào)用
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let url = task.originalRequest?.url else {
return
}
guard error == nil else {
callCompletionHandlerFailure(error: error!, url: url)
return
}
// 處理加工
processImage(for: task, url: url)
}
這個方法是在請求完成之后調(diào)用
其中processImage 具體是
private func processImage(for task: URLSessionTask, url: URL) {
guard let downloader = downloadHolder else {
return
}
// We are on main queue when receiving this.
// 下載完成后處理
downloader.processQueue.async {
guard let fetchLoad = downloader.fetchLoad(for: url) else {
return
}
self.cleanFetchLoad(for: url) // 清除舊的url
let data: Data?
let fetchedData = fetchLoad.responseData as Data
if let delegate = downloader.delegate {
data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url)
} else {
data = fetchedData
}
// Cache the processed images. So we do not need to re-process the image if using the same processor.
// Key is the identifier of processor.
var imageCache: [String: Image] = [:]
for content in fetchLoad.contents {
let options = content.options
let completionHandler = content.callback.completionHandler
let callbackQueue = options.callbackDispatchQueue
let processor = options.processor
var image = imageCache[processor.identifier]
if let data = data, image == nil { // 將data 轉(zhuǎn)成image
image = processor.process(item: .data(data), options: options)
// Add the processed image to cache.
// If `image` is nil, nothing will happen (since the key is not existing before).
imageCache[processor.identifier] = image
}
if let image = image {
//下載完成后可以進行的自定義操作,用戶可以自行指定delegate
downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
if options.backgroundDecode {
let decodedImage = image.kf.decoded
callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
} else {
callbackQueue.safeAsync { completionHandler?(image, nil, url, data) }
}
} else {
//不能生成圖片,返回304狀態(tài)碼,表示圖片沒有更新,可以直接使用緩存
if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil)
completionHandler?(nil, notModified, url, nil)
continue
}
//不能生成圖片,報BadData錯誤
let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
}
}
}
}
主要的委托方法都看完了,最后還有一個跟身份認證有關(guān)的:
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let downloader = downloadHolder else {
return
}
downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler)
}
func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//一般用于SSL/TLS協(xié)議(https)
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
//在白名單中的域名做特殊處理,忽視警告
if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(.useCredential, credential)
return
}
}
//默認處理
completionHandler(.performDefaultHandling, nil)
}
trustedHosts是ImageDownloader里聲明的一個字符串集合,應該就是類似于一個白名單,放到里面的域名是可以信任的。
緩存模塊
我們是從KingfisherManager中的downloadAndCacheImage為入口進入到下載模塊的,緩存模塊也從這里進入。再貼一下downloadAndCacheImage吧:
@discardableResult
func downloadAndCacheImage(with url: URL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
{
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
// 在下載完圖片之后的完成閉包中(會在下載請求結(jié)束后調(diào)用),如果服務器返回狀態(tài)碼notModified,說明服務器圖片未更新,我們可以從緩存中取得圖片數(shù)據(jù),就是調(diào)用retrieveImage 我們進入到retrieveImage查看
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.)
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)
if options.cacheOriginalImage {
let defaultProcessor = DefaultImageProcessor.default
if let originaliImage = defaultProcessor.process(item: .data(originalData), options: options) {
targetCache.store(originaliImage,
original: originalData,
forKey: key,
processorIdentifier: defaultProcessor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: !options.cacheMemoryOnly,
completionHandler: nil)
}
}
}
completionHandler?(image, error, .none, url)
})
}
// 在下載完圖片之后的完成閉包中(會在下載請求結(jié)束后調(diào)用),如果服務器返回狀態(tài)碼notModified,說明服務器圖片未更新,我們可以從緩存中取得圖片數(shù)據(jù),就是調(diào)用retrieveImage 我們進入到retrieveImage查看
- 給完成閉包進行解包,若為空則提前返回:
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
return nil
}
- 如果內(nèi)存中有緩存則直接從內(nèi)存中取圖片;再判斷圖片是否需要解碼,若需要,則先解碼再調(diào)用完成閉包,否則直接調(diào)用完成閉包:
@discardableResult
open func retrieveImage(forKey key: String,
options: KingfisherOptionsInfo?,
completionHandler: ((Image?, CacheType) -> ())?) -> RetrieveImageDiskTask?
{
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
return nil
}
var block: RetrieveImageDiskTask?
let options = options ?? KingfisherEmptyOptionsInfo
if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
options.callbackDispatchQueue.safeAsync {
completionHandler(image, .memory)
}
} else {
var sSelf: ImageCache! = self
block = DispatchWorkItem(block: {
// Begin to load image from disk
if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
if options.backgroundDecode {
sSelf.processQueue.async {
let result = image.kf.decoded
sSelf.store(result,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil)
options.callbackDispatchQueue.safeAsync {
completionHandler(result, .memory)
sSelf = nil
}
}
} else {
sSelf.store(image,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil
)
options.callbackDispatchQueue.safeAsync {
completionHandler(image, .disk)
sSelf = nil
}
}
} else {
// No image found from either memory or disk
options.callbackDispatchQueue.safeAsync {
completionHandler(nil, .none)
sSelf = nil
}
}
})
sSelf.ioQueue.async(execute: block!)
}
return block
}
- 如果內(nèi)存中沒有緩存,則從文件中取圖片,并判斷是否需要進行解碼,若需要則先解碼再將它緩存到內(nèi)存中然后執(zhí)行完成閉包,否則直接緩存到內(nèi)存中然后執(zhí)行完成閉包
獲取圖片就是這樣了,這個方法里調(diào)用了store這個方法,顯然是用來緩存圖片的,來看一下它的具體邏輯:
memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
緩存到內(nèi)存中
如果方法參數(shù)toDisk為true則先將其緩存到文件(如果圖片數(shù)據(jù)存在并能被正確解析的話),然后調(diào)用完成閉包:
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)
{
let computedKey = key.computedKey(with: identifier)
memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
func callHandlerInMainQueue() {
if let handler = completionHandler {
DispatchQueue.main.async {
handler()
}
}
}
if toDisk {
ioQueue.async {
if let data = serializer.data(with: image, original: original) {
if !self.fileManager.fileExists(atPath: self.diskCachePath) {
do {
try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {}
}
self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
}
callHandlerInMainQueue()
}
} else {
callHandlerInMainQueue()
}
}
- 整個緩存邏輯就是這樣
ImageCache中還有一個刪除過期緩存的方法
fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
// 一些準備工作,取緩存路徑,過期時間等:
let diskCacheURL = URL(fileURLWithPath: diskCachePath)
let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
var cachedFiles = [URL: URLResourceValues]()
var urlsToDelete = [URL]()
var diskCacheSize: UInt = 0
// 遍歷緩存圖片(跳過隱藏文件和文件夾),如果圖片過期,則加入待刪除隊列:
fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
// 一些準備工作,取緩存路徑,過期時間等:
let diskCacheURL = URL(fileURLWithPath: diskCachePath)
let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
var cachedFiles = [URL: URLResourceValues]()
var urlsToDelete = [URL]()
var diskCacheSize: UInt = 0
for fileUrl in (try? fileManager.contentsOfDirectory(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)) ?? [] {
do {
let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
// If it is a Directory. Continue to next file URL.
//跳過目錄
if resourceValues.isDirectory == true {
continue
}
// If this file is expired, add it to URLsToDelete
//若文件最新更新日期超過過期日期,則放入待刪除隊列
if !onlyForCacheSize,
let expiredDate = expiredDate,
let lastAccessData = resourceValues.contentAccessDate,
(lastAccessData as NSDate).laterDate(expiredDate) == expiredDate
{
urlsToDelete.append(fileUrl)
continue
}
if let fileSize = resourceValues.totalFileAllocatedSize {
diskCacheSize += UInt(fileSize)
if !onlyForCacheSize {
cachedFiles[fileUrl] = resourceValues
}
}
} catch _ { }
}
return (urlsToDelete, diskCacheSize, cachedFiles)
}
若剩余緩存內(nèi)容超過預設的最大緩存尺寸,則刪除存在時間較長的緩存,并將已刪除圖片的URL也加大刪除隊列中(為了一會兒的廣播),直到緩存尺寸到達預設最大尺寸的一半:
open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {
// Do things in cocurrent io queue
ioQueue.async {
var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
for fileURL in URLsToDelete {
do {
try self.fileManager.removeItem(at: fileURL)
} catch _ { }
}
// 若當前緩存內(nèi)容超過預設的最大緩存尺寸,則先將文件根據(jù)時間排序(舊的在前),然后開始循環(huán)刪除,直到尺寸降到最大緩存尺寸的一半。
if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
let targetSize = self.maxDiskCacheSize / 2
// Sort files by last modify date. We want to clean from the oldest files.
let sortedFiles = cachedFiles.keysSortedByValue {
resourceValue1, resourceValue2 -> Bool in
if let date1 = resourceValue1.contentAccessDate,
let date2 = resourceValue2.contentAccessDate
{
return date1.compare(date2) == .orderedAscending
}
// Not valid date information. This should not happen. Just in case.
return true
}
for fileURL in sortedFiles {
do {
try self.fileManager.removeItem(at: fileURL)
} catch { }
URLsToDelete.append(fileURL)
if let fileSize = cachedFiles[fileURL]?.totalFileAllocatedSize {
diskCacheSize -= UInt(fileSize)
}
if diskCacheSize < targetSize {
break
}
}
}
DispatchQueue.main.async {
// //將已刪除的所有文件名進行廣播
if URLsToDelete.count != 0 {
let cleanedHashes = URLsToDelete.map { $0.lastPathComponent }
NotificationCenter.default.post(name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
}
handler?()
}
}
}
在主線程廣播已刪除的緩存圖片,如果有傳入完成閉包的話,就調(diào)用它:
緩存模塊的主要內(nèi)容就這些了,其他還有一些輔助方法像計算緩存尺寸啊、圖片的排序啊、把圖片URL進行MD5加密作為緩存文件名啊等等,具體寫了,有興趣的同學可以直接去看源碼。在UIImage+Extension文件中還有一些處理圖片的擴展方法,諸如標準化圖片格式、GIF圖片的存儲、GIF圖片的展示等等,這些都算是一些套路上的東西,正確調(diào)用蘋果給的API就好了.
Kingfisher中還用到了很多小技巧,比如對關(guān)聯(lián)對象(Associated Object)的使用,解決了extension不能擴展存儲屬性的問題:
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)
}
//全局變量,用來作為關(guān)聯(lián)對象(估計是因為extension里面不能添加儲存屬性,只能通過關(guān)聯(lián)對象配合計算屬性和方法的方式來hack)
總結(jié):
- 文件操作相關(guān)知識(遍歷文件、跳過隱藏文件、按日期排序文件等等)
- 圖片處理相關(guān)知識(判斷圖片格式、處理GIF等等)
- MD5摘要算法(這個我并沒有仔細看)
- Associated Object的運用
- Swift中關(guān)于enum和模式匹配的優(yōu)雅用法
由于時間長促,下次改善排版,盡可能詳細闡述每個方法