Kingfisher 3.x 學(xué)習(xí) (二)

一、ImageDownloader

Kingfisher中,該類主要負責(zé)圖片的網(wǎng)絡(luò)下載往扔,其實現(xiàn)原理是基于系統(tǒng)的URLSession ,實現(xiàn)它的代理方法。下面是幾個主要部分:

  • ImageFetchLoad
  • URLSession的配置
  • 下載方法
  • 取消下載
  • URLSession 代理方法
  • 下載某張?zhí)囟▓D片
1. ImageFetchLoad
   class ImageFetchLoad {
        var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
        var responseData = NSMutableData()
        var downloadTaskCount = 0
        var downloadTask: RetrieveImageDownloadTask?
    }

ImageFetchLoad 是一個嵌套類熊户。它處理了一個URL下載數(shù)據(jù)萍膛,能夠記錄同一個URL下載任務(wù)次數(shù)。其中的contents屬性是一個元組數(shù)組嚷堡,該元組包含兩個部分:CallbackPair蝗罗,KingfisherOptionsInfoKingfisherOptionsInfo就是傳入的配置參數(shù)蝌戒,而CallbackPair也是一個元組串塑,它包含了傳入的兩個閉包。ImageDownloaderProgressBlock 能夠在每次接收到數(shù)據(jù)時調(diào)用北苟,可以用來顯示進度條桩匪,ImageDownloaderCompletionHandler在數(shù)據(jù)接收完成之后會被調(diào)用。里面還有一個responseData屬性,能夠把每次獲取到的數(shù)據(jù)存儲起來友鼻。那么ImageDownloader這個類有什么作用呢傻昙?通常情況下,ImageDownloader往往要處理多個URL的下載任務(wù)彩扔,它的fetchLoads屬性是一個[URL: ImageFetchLoad]類型的字典妆档,存儲不同 URL 及其 ImageFetchLoad 之間的對應(yīng)關(guān)系。
下面是根據(jù)URL獲取ImageFetchLoad 的方法

    func fetchLoad(for url: URL) -> ImageFetchLoad? {
        var fetchLoad: ImageFetchLoad?
        barrierQueue.sync { fetchLoad = fetchLoads[url] }
        return fetchLoad
    }

這里使用 barrierQueue 來操作借杰,利用 sync阻塞當前線程过吻,完成 ImageFetchLoad 讀操作后再返回。這樣當讀取 ImageFetchLoad 的時候,保證ImageFetchLoad 不會同時在被寫,導(dǎo)致數(shù)據(jù)錯誤

2.URLSession的配置

來看一下ImageDownloader的構(gòu)造器方法

public init(name: String) {
        if name.isEmpty {
            fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
        }
        
        barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\\(name)", attributes: .concurrent)
        processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\\(name)", attributes: .concurrent)
   
        sessionHandler = ImageDownloaderSessionHandler()
        // Provide a default implement for challenge responder.
        authenticationChallengeResponder = sessionHandler
        session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: .main)
    }

可以看到sessionsessionConfigurationsessionHandler來配置的纤虽。其中sessionConfiguration是個open的屬性乳绕,可以在外部自定義。delegate確不是ImageDownloader而是sessionHandler
這里喵神也有解釋逼纸,以前確實是ImageDownloader作為代理的洋措,但會造成內(nèi)存泄漏 issue

/// Delegate class for `NSURLSessionTaskDelegate`.
/// The session object will hold its delegate until it gets invalidated.
/// If we use `ImageDownloader` as the session delegate, it will not be released.
/// So we need an additional handler to break the retain cycle.
3.下載方法

這是外部調(diào)用ImageDownloader最常用的方法 配置好請求參數(shù):Time 、URL杰刽、 URLRequest 菠发,確保請求的前提條件 主要是setup方法

func downloadImage(with url: URL,
              retrieveImageTask: RetrieveImageTask?,
                        options: KingfisherOptionsInfo?,
                  progressBlock: ImageDownloaderProgressBlock?,
              completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
    {
        if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
            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.
        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
        request.httpShouldUsePipelining = requestsUsePipeling

        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 {...}
        return downloadTask
    }

setup閉包回調(diào) :
根據(jù)傳過來的fetchLoad 是否開啟下載任務(wù)。若沒有根據(jù)session 生成 dataTask,在進一步包裝成RetrieveImageDownloadTask贺嫂,傳給fetchLoaddownloadTask屬性 配置好任務(wù)優(yōu)先級滓鸠,開啟下載任務(wù),如果已開啟下載第喳,下載次數(shù)加1糜俗,設(shè)置傳給外部的retrieveImageTaskdownloadTask

       setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
            if fetchLoad.downloadTask == nil {
                let dataTask = session.dataTask(with: request)
                //設(shè)置下載任務(wù)
                fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
                //設(shè)置下載任務(wù)優(yōu)先級
                dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
                 //開啟下載任務(wù)
                dataTask.resume()
                
                // Hold self while the task is executing.
               //下載期間確保sessionHandler 持有 ImageDownloader
                self.sessionHandler.downloadHolder = self
            }
            //下載次數(shù)加1
            fetchLoad.downloadTaskCount += 1
            downloadTask = fetchLoad.downloadTask
            retrieveImageTask?.downloadTask = downloadTask
        }
   // A single key may have multiple callbacks. Only download once.
    func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: ((URLSession, ImageFetchLoad) -> Void)) {

        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)
            }
        }
    }

首先barrierQueue.sync 確保ImageFetchLoad 讀寫安全,根據(jù)傳入的URL獲取對應(yīng)的ImageFetchLoad 設(shè)置callbackPair并更新contents 曲饱,開啟下載

4.取消下載
  func cancelDownloadingTask(_ task: RetrieveImageDownloadTask) {
        barrierQueue.sync {
            if let URL = task.internalTask.originalRequest?.url, let imageFetchLoad = self.fetchLoads[URL] {
                更新下載次數(shù)
                imageFetchLoad.downloadTaskCount -= 1
                if imageFetchLoad.downloadTaskCount == 0 {
                    task.internalTask.cancel()
                }
            }
        }
    }
5.URLSession 代理方法

下載過程中接收Response

 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        //下載過程中確保ImageDownloader 一直持有 
        guard let downloader = downloadHolder else {
            completionHandler(.cancel)
            return
        }
         //返回狀態(tài)碼判斷 
        if let statusCode = (response as? HTTPURLResponse)?.statusCode,
           let url = dataTask.originalRequest?.url,
            !(downloader.delegate ?? downloader).isValidStatusCode(statusCode, for: downloader)
        {
            let error = NSError(domain: KingfisherErrorDomain,
                                code: KingfisherError.invalidStatusCode.rawValue,
                                userInfo: [KingfisherErrorStatusCodeKey: statusCode, NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
             //返回錯誤 首先清除ImageFetchLoad 
            callCompletionHandlerFailure(error: error, url: url)
        }
        //繼續(xù)請求數(shù)據(jù)
        completionHandler(.allow)
    }

下載過程中接收到數(shù)據(jù)

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        guard let downloader = downloadHolder else {
            return
        }
        //添加數(shù)據(jù)到指定ImageFetchLoad
        if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
            fetchLoad.responseData.append(data)
            //下載進度回調(diào)
            if let expectedLength = dataTask.response?.expectedContentLength {
                for content in fetchLoad.contents {
                    DispatchQueue.main.async {
                        content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
                    }
                }
            }
        }
    }

下載結(jié)束

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        // URL 一致性判斷
        guard let url = task.originalRequest?.url else {
            return
        }
        // error 判斷
        guard error == nil else {
            callCompletionHandlerFailure(error: error!, url: url)
            return
        }
        //圖片處理
        processImage(for: task, url: url)
    }
    ```
會話需要認證 
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    guard let downloader = downloadHolder else {
        return
    }
    
    downloader.authenticationChallengeResponder?.downloader(downloader, didReceive: challenge, completionHandler: completionHandler)
}
協(xié)議AuthenticationChallengeResponsable 處理會話認證 

public protocol AuthenticationChallengeResponsable: class {
/**
Called when an session level authentication challenge is received.
This method provide a chance to handle and response to the authentication challenge before downloading could start.

 - parameter downloader:        The downloader which receives this challenge.
 - parameter challenge:         An object that contains the request for authentication.
 - parameter completionHandler: A handler that your delegate method must call.
 
 - Note: This method is a forward from `URLSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `NSURLSessionDelegate`.
 */
func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

}

extension AuthenticationChallengeResponsable {

func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

    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)
}

}

返回錯誤信息

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.
       //清除ImageDownloader
    cleanFetchLoad(for: url)
 
    for content in fetchLoad.contents {
        content.options.callbackDispatchQueue.safeAsync {
            content.callback.completionHandler?(nil, error as NSError, url, nil)
        }
    }
}
處理圖片數(shù)據(jù)
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
        }
        //首先清除ImageDownloader
        self.cleanFetchLoad(for: url)
        
        let data = fetchLoad.responseData as Data
        
        // 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 image == nil {
               //合成圖片
                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 {
                 
                downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
                
                if options.backgroundDecode {
                   //后臺編碼
                    let decodedImage = image.kf.decoded(scale: options.scaleFactor)
                    callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
                } else {
                    callbackQueue.safeAsync { completionHandler?(image, nil, url, data) }
                }
                
            } else {
                 // 304 狀態(tài)碼 沒有圖像數(shù)據(jù)下載
                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
                }
                 //返回不是圖片數(shù)據(jù) 或者數(shù)據(jù)被破壞
                let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
                callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
            }
        }
    }
}
Data->Image 方法

static func image(data: Data, scale: CGFloat, preloadAllGIFData: Bool) -> Image? {
var image: Image?

    #if os(macOS)
        switch data.kf.imageFormat {
        case .JPEG: image = Image(data: data)
        case .PNG: image = Image(data: data)
        case .GIF: image = Kingfisher<Image>.animated(with: data, scale: scale, duration: 0.0, preloadAll: preloadAllGIFData)
        case .unknown: image = Image(data: data)
        }
    #else
        switch data.kf.imageFormat {
        case .JPEG: image = Image(data: data, scale: scale)
        case .PNG: image = Image(data: data, scale: scale)
        case .GIF: image = Kingfisher<Image>.animated(with: data, scale: scale, duration: 0.0, preloadAll: preloadAllGIFData)
        case .unknown: image = Image(data: data, scale: scale)
        }
    #endif
    
    return image
}
#####6.下載某張?zhí)囟▓D片

在 ```ImageDownloader```中有一個delegate屬性  ```open weak var delegate: ImageDownloaderDelegate?```
你可以創(chuàng)建一個```ImageDownloader```,設(shè)置好delegate悠抹,調(diào)用下面方法,并且實現(xiàn)代理方法扩淀,就能下載這張圖片

open func downloadImage(with url: URL,
options: KingfisherOptionsInfo? = nil,
progressBlock: ImageDownloaderProgressBlock? = nil,
completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
{
return downloadImage(with: url,
retrieveImageTask: nil,
options: options,
progressBlock: progressBlock,
completionHandler: completionHandler)
}


/// Protocol of ImageDownloader.
public protocol ImageDownloaderDelegate: class {
/**
Called when the ImageDownloader object successfully downloaded an image from specified URL.

- parameter downloader: The `ImageDownloader` object finishes the downloading.
- parameter image:      Downloaded image.
- parameter url:        URL of the original request URL.
- parameter response:   The response object of the downloading process.
*/
func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?)


/**
Check if a received HTTP status code is valid or not. 
By default, a status code between 200 to 400 (excluded) is considered as valid.
If an invalid code is received, the downloader will raise an .invalidStatusCode error.
It has a `userInfo` which includes this statusCode and localizedString error message.
 
- parameter code: The received HTTP status code.
- parameter downloader: The `ImageDownloader` object asking for validate status code.
 
- returns: Whether this HTTP status code is valid or not.
 
- Note: If the default 200 to 400 valid code does not suit your need, 
        you can implement this method to change that behavior.
*/
func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool

}

extension ImageDownloaderDelegate {
public func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) {}

public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
    return (200..<400).contains(code)
}

}



到這里為止楔敌,```ImageDownloader```的大部分功能都已經(jīng)提及,還有一些細節(jié)
結(jié)構(gòu)體```RetrieveImageDownloadTask``` 是對```URLSessionDataTask```的進一層包裝
有```cancel ```方法供外部調(diào)用

public struct RetrieveImageDownloadTask {
let internalTask: URLSessionDataTask

/// Downloader by which this task is intialized.
public private(set) weak var ownerDownloader: ImageDownloader?

/**
 Cancel this download task. It will trigger the completion handler with an NSURLErrorCancelled error.
 */
public func cancel() {
    ownerDownloader?.cancelDownloadingTask(self)
}

/// The original request URL of this download task.
public var url: URL? {
    return internalTask.originalRequest?.url
}

/// The relative priority of this download task. 
/// It represents the `priority` property of the internal `NSURLSessionTask` of this download task.
/// The value for it is between 0.0~1.0. Default priority is value of 0.5.
/// See documentation on `priority` of `NSURLSessionTask` for more about it.
public var priority: Float {
    get {
        return internalTask.priority
    }
    set {
        internalTask.priority = newValue
    }
}

}


## 二、ImageCache
在```Kingfisher```中驻谆,```ImageCache```能夠進行內(nèi)存緩存和磁盤緩存卵凑。內(nèi)存緩存由```NSCache```實現(xiàn),磁盤緩存采用將image 轉(zhuǎn)化成data ,加上FileManager操作文件完成旺韭。下面是主要實現(xiàn)功能

- 緩存路徑管理
- 緩存的添加與刪除
- 緩存的獲取
- 緩存的清除
- 緩存狀態(tài)檢查

下面是```ImageCache```內(nèi)部的屬性: 
//Memory
fileprivate let memoryCache = NSCache<NSString, AnyObject>()

/// The largest cache cost of memory cache. The total cost is pixel count of 
/// all cached images in memory.
/// Default is unlimited. Memory cache will be purged automatically when a 
/// memory warning notification is received.
open var maxMemoryCost: UInt = 0 {
    didSet {
        self.memoryCache.totalCostLimit = Int(maxMemoryCost)
    }
}

//Disk
fileprivate let ioQueue: DispatchQueue
fileprivate var fileManager: FileManager!

///The disk cache location.
open let diskCachePath: String

/// The default file extension appended to cached files.
open var pathExtension: String?

/// The longest time duration in second of the cache being stored in disk. 
/// Default is 1 week (60 * 60 * 24 * 7 seconds).
open var maxCachePeriodInSecond: TimeInterval = 60 * 60 * 24 * 7 //Cache exists for 1 week

/// The largest disk size can be taken for the cache. It is the total 
/// allocated size of cached files in bytes.
/// Default is no limit.
open var maxDiskCacheSize: UInt = 0

fileprivate let processQueue: DispatchQueue

/// The default cache.
public static let `default` = ImageCache(name: "default")

/// Closure that defines the disk cache path from a given path and cacheName.
public typealias DiskCachePathClosure = (String?, String) -> String

/// The default DiskCachePathClosure
public final class func defaultDiskCachePathClosure(path: String?, cacheName: String) -> String {
    let dstPath = path ?? NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
    return (dstPath as NSString).appendingPathComponent(cacheName)
}
其中:```memoryCache```用來管理內(nèi)存緩存氛谜,```ioQueue``` 用來進行硬盤隊列操作。由于硬盤存取操作相比于內(nèi)存存取耗時区端,避免造成線程阻塞需單獨開辟線程進行相應(yīng)操作值漫。```fileManager```用于文件管理。```diskCachePath```用于設(shè)置文件的存儲路徑织盼。```maxCachePeriodInSecond```,最大的磁盤緩存時間杨何,默認一周。```maxDiskCacheSize```最大的磁盤緩存大小沥邻。  ```processQueue```用于執(zhí)行圖片的 decode 操作危虱。```default``` 為  ```ImageCache``` 類的單例,在Swift 中,調(diào)用 ```static let``` 可以直接創(chuàng)建一個單例,系統(tǒng)會自動調(diào)用```dispatch_once```。

####緩存路徑相關(guān)的幾個方法

- 根據(jù)key,serializer, options獲取磁盤圖片
- 根據(jù)key獲取磁盤圖片數(shù)據(jù)
- 根據(jù)key 獲取md5加密字符串

extension ImageCache {

func diskImage(forComputedKey key: String, serializer: CacheSerializer, options: KingfisherOptionsInfo) -> Image? {
    if let data = diskImageData(forComputedKey: key) {
        return serializer.image(with: data, options: options)
    } else {
        return nil
    }
}

func diskImageData(forComputedKey key: String) -> Data? {
    let filePath = cachePath(forComputedKey: key)
    return (try? Data(contentsOf: URL(fileURLWithPath: filePath)))
}

func cacheFileName(forComputedKey key: String) -> String {
    if let ext = self.pathExtension {
      return (key.kf.md5 as NSString).appendingPathExtension(ext)!
    }
    return key.kf.md5
}

}

####緩存的添加與刪除
主要外部調(diào)用方法```store```,首先對傳入的 URL Key 和 processorIdentifier 做簡單拼接成computedKey扣讼,設(shè)置內(nèi)存緩存弥雹。然后根據(jù)是否磁盤緩存 進一步處理垃帅,其中調(diào)用```CacheSerializer ``` 的 ```func data(with image: Image, original: Data?) -> Data?```方法,根據(jù)Data 獲取圖片類型,將image序列化成data 存入文件剪勿,其中path 是computedKey經(jīng)過md5加密獲得

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)
{
//內(nèi)存緩存
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 {
            將image 序列化成 data
            if let data = serializer.data(with: image, original: original) {
                if !self.fileManager.fileExists(atPath: self.diskCachePath) {
                    do {
                        不存在磁盤緩存文件夾 創(chuàng)建 默認在 Library/Cache/com.onevcat.Kingfisher.ImageCache.default
                        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()
    }
}
根據(jù)存入的key值移除緩存圖片贸诚,如果需要移除磁盤緩存,刪除對應(yīng)文件

open func removeImage(forKey key: String,
processorIdentifier identifier: String = "",
fromDisk: Bool = true,
completionHandler: (() -> Void)? = nil)
{
根據(jù)key移除內(nèi)存緩存
let computedKey = key.computedKey(with: identifier)
memoryCache.removeObject(forKey: computedKey as NSString)

    func callHandlerInMainQueue() {
        if let handler = completionHandler {
            DispatchQueue.main.async {
                handler()
            }
        }
    }
    
    if fromDisk {
        ioQueue.async{
            do {
                根據(jù)key移除磁盤緩存
                try self.fileManager.removeItem(atPath: self.cachePath(forComputedKey: computedKey))
            } catch _ {}
            callHandlerInMainQueue()
        }
    } else {
        callHandlerInMainQueue()
    }
}
####緩存的獲取
根據(jù)```key``` 獲得緩存圖片 首先從內(nèi)存緩存中獲取厕吉,如果無內(nèi)存緩存酱固,再判斷磁盤緩存。如果有头朱,從磁盤中獲取緩存文件运悲,將圖片```data```反序列化成```image```,在返回之前判斷了是否需要后臺編碼髓窜,做了內(nèi)存緩存扇苞。這里返回的```RetrieveImageDiskTask``` 是一個```DispatchWorkItem```,相當于OC的```dispatch_block_t```,它定義了獲取磁盤緩存并進行內(nèi)存緩存的操作閉包寄纵,放在ioQueue中異步執(zhí)行,確保了外部在操作過程中一直持有該緩存操作脖苏,相當于```ImageDownloader```的```RetrieveImageDownloadTask```,并且在返回之前都將sSelf置為nil程拭,釋放了內(nèi)存。因為該閉包屬于逃逸閉包棍潘,必需在閉包中顯式地引用self 恃鞋。

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
    首先判斷內(nèi)存緩存是否存在
    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(scale: options.scaleFactor)
                        內(nèi)存緩存
                        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 {
                    內(nèi)存緩存
                    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)存中獲取圖片
open func retrieveImageInMemoryCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
    let options = options ?? KingfisherEmptyOptionsInfo
    let computedKey = key.computedKey(with: options.processor.identifier)
    
    return memoryCache.object(forKey: computedKey as NSString) as? Image
}
  從磁盤中獲取圖片
open func retrieveImageInDiskCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
    
    let options = options ?? KingfisherEmptyOptionsInfo
    let computedKey = key.computedKey(with: options.processor.identifier)
    
    return diskImage(forComputedKey: computedKey, serializer: options.cacheSerializer, options: options)
}
####緩存的清除
手動清除所有內(nèi)存緩存和磁盤緩存
@objc public func clearMemoryCache() {
    memoryCache.removeAllObjects()
}
/**
Clear disk cache. This is an async operation.

- parameter completionHander: Called after the operation completes.
*/
open func clearDiskCache(completion handler: (()->())? = nil) {
    ioQueue.async {
        do {
            try self.fileManager.removeItem(atPath: self.diskCachePath)
            try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
        } catch _ { }
        
        if let handler = handler {
            DispatchQueue.main.async {
                handler()
            }
        }
    }
}
當應(yīng)用程序在進入后臺的時候,可以自動檢測過期緩存文件亦歉,并在后臺完成清理操作恤浪,實現(xiàn)代碼如下:
@objc public func backgroundCleanExpiredDiskCache() {
    // if 'sharedApplication()' is unavailable, then return
    guard let sharedApplication = Kingfisher<UIApplication>.shared else { return }

    func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) {
        sharedApplication.endBackgroundTask(task)
        task = UIBackgroundTaskInvalid
    }
    
    var backgroundTask: UIBackgroundTaskIdentifier!
    backgroundTask = sharedApplication.beginBackgroundTask {
        endBackgroundTask(&backgroundTask!)
    }
    //清除過期的磁盤緩存
    cleanExpiredDiskCache {
        endBackgroundTask(&backgroundTask!)
    }
}
獲取過期的URL數(shù)組,磁盤緩存大小和緩存文件字典, 進行緩存刪除操作肴楷。 通過```FileManager ```的```enumerator```方法遍歷出所有緩存文件水由,如果文件最后一次訪問日期比當前時間減去一周時間還要早,將該文件```fileUrl```添加到```urlsToDelete```數(shù)組赛蔫。計算緩存文件大小砂客,以```fileUrl```為key,```resourceValues```為value,存入 ```cachedFiles```

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(timeIntervalSinceNow: -maxCachePeriodInSecond)
    // 緩存字典 URL : ResourceValue
    var cachedFiles = [URL: URLResourceValues]()
    var urlsToDelete = [URL]()
    var diskCacheSize: UInt = 0
    
    if let fileEnumerator = self.fileManager.enumerator(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: FileManager.DirectoryEnumerationOptions.skipsHiddenFiles, errorHandler: nil),
       let urls = fileEnumerator.allObjects as? [URL]
    {
        for fileUrl in urls {
            
            do {
                let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
                // If it is a Directory. Continue to next file URL.
                if resourceValues.isDirectory == true {
                    continue
                }
                
                if !onlyForCacheSize {
                    // If this file is expired, add it to URLsToDelete
                    if let lastAccessData = resourceValues.contentAccessDate {
                        if (lastAccessData as NSDate).laterDate(expiredDate) == expiredDate {
                            //添加過期URL到刪除數(shù)組
                            urlsToDelete.append(fileUrl)
                            continue
                        }
                    }
                }

                if let fileSize = resourceValues.totalFileAllocatedSize {
                    //更新緩存大小
                    diskCacheSize += UInt(fileSize)
                    if !onlyForCacheSize {
                        // 緩存文件字典對應(yīng)
                        cachedFiles[fileUrl] = resourceValues
                    }
                }
            } catch _ { }
        }
    }
    
    return (urlsToDelete, diskCacheSize, cachedFiles)
}
根據(jù)上面獲取的```urlsToDelete```數(shù)組呵恢,```diskCacheSize```磁盤緩存大小和```cachedFiles```字典鞠值,刪除過期緩存 。

open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {

    // Do things in cocurrent io queue
    ioQueue.async {
        
        var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
        //清除過期的磁盤緩存 根據(jù)資源最后一次訪問的時間和 當前時間減去一周時間(自定義最長緩存存在時間)比較判斷是否過期
        for fileURL in URLsToDelete {
            do {
                try self.fileManager.removeItem(at: fileURL)
            } catch _ { }
        }
        //磁盤緩存大小超過自定義最大緩存
        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.
            //清除訪問次數(shù)少的文件
            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?()
        }
    }
}
###緩存的狀態(tài)檢查

- 緩存類型結(jié)構(gòu)體

public struct CacheCheckResult {
public let cached: Bool
public let cacheType: CacheType?
}

- 根據(jù)key判斷是否存在緩存以及緩存圖片類型

open func isImageCached(forKey key: String, processorIdentifier identifier: String = "") -> CacheCheckResult {
    
    let computedKey = key.computedKey(with: identifier)
    
    if memoryCache.object(forKey: computedKey as NSString) != nil {
        return CacheCheckResult(cached: true, cacheType: .memory)
    }
    
    let filePath = cachePath(forComputedKey: computedKey)
    
    var diskCached = false
    ioQueue.sync {
        diskCached = fileManager.fileExists(atPath: filePath)
    }

    if diskCached {
        return CacheCheckResult(cached: true, cacheType: .disk)
    }
    
    return CacheCheckResult(cached: false, cacheType: nil)
}
```
  • 根據(jù)key渗钉,processorIdentifier查找緩存文件
    /**
    Get the hash for the key. This could be used for matching files.
    
    - parameter key:        The key which is used for caching.
    - parameter identifier: The identifier of processor used. If you are using a processor for the image, pass the identifier of processor to it.
    
     - returns: Corresponding hash.
    */
    open func hash(forKey key: String, processorIdentifier identifier: String = "") -> String {
        let computedKey = key.computedKey(with: identifier)
        return cacheFileName(forComputedKey: computedKey)
    }
  • 計算緩存大小
    /**
    Calculate the disk size taken by cache. 
    It is the total allocated size of the cached files in bytes.
    
    - parameter completionHandler: Called with the calculated size when finishes.
    */
    open func calculateDiskCacheSize(completion handler: @escaping ((_ size: UInt) -> ())) {
        ioQueue.async {
            let (_, diskCacheSize, _) = self.travelCachedFiles(onlyForCacheSize: true)
            DispatchQueue.main.async {
                handler(diskCacheSize)
            }
        }
    }
    ```
- 根據(jù)key彤恶,identifier獲取加密后的緩存路徑

/**
Get the cache path for the key.
It is useful for projects with UIWebView or anyone that needs access to the local file path.

- Note: This method does not guarantee there is an image already cached in the path. It just returns the path
  that the image should be.
  You could use `isImageCached(forKey:)` method to check whether the image is cached under that key.
*/
open func cachePath(forKey key: String, processorIdentifier identifier: String = "") -> String {
    let computedKey = key.computedKey(with: identifier)
    return cachePath(forComputedKey: computedKey)
}
open func cachePath(forComputedKey key: String) -> String {
    let fileName = cacheFileName(forComputedKey: key)
    return (diskCachePath as NSString).appendingPathComponent(fileName)
}
## 三、CacheSerializer
該類用于將磁盤圖片數(shù)據(jù)反序列化成圖片對象以及將圖片對象序列化成圖片數(shù)據(jù)。具體功能由```Image```文件實現(xiàn)
 Image 序列化 Data声离。通過Data獲取圖片format,返回不同格式下圖片芒炼。能實現(xiàn)PNG,JPEG抵恋,GIF圖片格式焕议,其他圖片格式默認返回PNG格式

public func data(with image: Image, original: Data?) -> Data? {
let imageFormat = original?.kf.imageFormat ?? .unknown

    let data: Data?
    switch imageFormat {
    case .PNG: data = image.kf.pngRepresentation()
    case .JPEG: data = image.kf.jpegRepresentation(compressionQuality: 1.0)
    case .GIF: data = image.kf.gifRepresentation()
    case .unknown: data = original ?? image.kf.normalized.kf.pngRepresentation()
    }
    
    return data
}
Data 序列化成Image。 如果是GIF圖片弧关,```preloadAllGIFData``` 用于判斷圖片顯示方式盅安。 false: 不會加載所有GIF圖片數(shù)據(jù),只顯示GIF中的第一張圖片世囊,true:將所有圖片數(shù)據(jù)加載到內(nèi)存别瞭,顯示GIF動態(tài)圖片 

public func image(with data: Data, options: KingfisherOptionsInfo?) -> Image? {
let scale = (options ?? KingfisherEmptyOptionsInfo).scaleFactor
let preloadAllGIFData = (options ?? KingfisherEmptyOptionsInfo).preloadAllGIFData

    return Kingfisher<Image>.image(data: data, scale: scale, preloadAllGIFData: preloadAllGIFData)
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市株憾,隨后出現(xiàn)的幾起案子蝙寨,更是在濱河造成了極大的恐慌,老刑警劉巖嗤瞎,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墙歪,死亡現(xiàn)場離奇詭異,居然都是意外死亡贝奇,警方通過查閱死者的電腦和手機虹菲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掉瞳,“玉大人毕源,你說我怎么就攤上這事∩孪埃” “怎么了霎褐?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長该镣。 經(jīng)常有香客問我冻璃,道長,這世上最難降的妖魔是什么拌牲? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任俱饿,我火速辦了婚禮,結(jié)果婚禮上塌忽,老公的妹妹穿的比我還像新娘拍埠。我一直安慰自己,他們只是感情好土居,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布枣购。 她就那樣靜靜地躺著嬉探,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棉圈。 梳的紋絲不亂的頭發(fā)上涩堤,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音分瘾,去河邊找鬼胎围。 笑死,一個胖子當著我的面吹牛德召,可吹牛的內(nèi)容都是我干的白魂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼上岗,長吁一口氣:“原來是場噩夢啊……” “哼福荸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起肴掷,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤敬锐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呆瞻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體台夺,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年痴脾,在試婚紗的時候發(fā)現(xiàn)自己被綠了谒养。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡明郭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丰泊,到底是詐尸還是另有隱情薯定,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布瞳购,位于F島的核電站话侄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏学赛。R本人自食惡果不足惜年堆,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盏浇。 院中可真熱鬧变丧,春花似錦、人聲如沸绢掰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至攻晒,卻和暖如春顾复,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鲁捏。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工芯砸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人给梅。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓假丧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親破喻。 傳聞我的和親對象是個殘疾皇子虎谢,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容