Kingfisher 3.x 学习 (二)

一、ImageDownloader

Kingfisher中,该类主要负责图片的网络下载,其实现原理是基于系统的URLSession ,实现它的代理方法。下面是几个主要部分:

  • ImageFetchLoad
  • URLSession的配置
  • 下载方法
  • 取消下载
  • URLSession 代理方法
  • 下载某张特定图片
1. ImageFetchLoad
   class ImageFetchLoad {
        var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
        var responseData = NSMutableData()
        var downloadTaskCount = 0
        var downloadTask: RetrieveImageDownloadTask?
    }

ImageFetchLoad 是一个嵌套类。它处理了一个URL下载数据,能够记录同一个URL下载任务次数。其中的contents属性是一个元组数组,该元组包含两个部分:CallbackPairKingfisherOptionsInfoKingfisherOptionsInfo就是传入的配置参数,而CallbackPair也是一个元组,它包含了传入的两个闭包。ImageDownloaderProgressBlock 能够在每次接收到数据时调用,可以用来显示进度条,ImageDownloaderCompletionHandler在数据接收完成之后会被调用。里面还有一个responseData属性,能够把每次获取到的数据存储起来。那么ImageDownloader这个类有什么作用呢?通常情况下,ImageDownloader往往要处理多个URL的下载任务,它的fetchLoads属性是一个[URL: ImageFetchLoad]类型的字典,存储不同 URL 及其 ImageFetchLoad 之间的对应关系。
下面是根据URL获取ImageFetchLoad 的方法

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

这里使用 barrierQueue 来操作,利用 sync阻塞当前线程,完成 ImageFetchLoad 读操作后再返回。这样当读取 ImageFetchLoad 的时候,保证ImageFetchLoad 不会同时在被写,导致数据错误

2.URLSession的配置

来看一下ImageDownloader的构造器方法

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作为代理的,但会造成内存泄漏 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.下载方法

这是外部调用ImageDownloader最常用的方法 配置好请求参数: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闭包回调 :
根据传过来的fetchLoad 是否开启下载任务。若没有根据session 生成 dataTask,在进一步包装成RetrieveImageDownloadTask,传给fetchLoaddownloadTask属性 配置好任务优先级,开启下载任务,如果已开启下载,下载次数加1,设置传给外部的retrieveImageTaskdownloadTask

       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()
                
                // Hold self while the task is executing.
               //下载期间确保sessionHandler 持有 ImageDownloader
                self.sessionHandler.downloadHolder = self
            }
            //下载次数加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 读写安全,根据传入的URL获取对应的ImageFetchLoad 设置callbackPair并更新contents ,开启下载

4.取消下载
  func cancelDownloadingTask(_ task: RetrieveImageDownloadTask) {
        barrierQueue.sync {
            if let URL = task.internalTask.originalRequest?.url, let imageFetchLoad = self.fetchLoads[URL] {
                更新下载次数
                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
        }
         //返回状态码判断 
        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)
        }
        //继续请求数据
        completionHandler(.allow)
    }

下载过程中接收到数据

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        guard let downloader = downloadHolder else {
            return
        }
        //添加数据到指定ImageFetchLoad
        if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
            fetchLoad.responseData.append(data)
            //下载进度回调
            if let expectedLength = dataTask.response?.expectedContentLength {
                for content in fetchLoad.contents {
                    DispatchQueue.main.async {
                        content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
                    }
                }
            }
        }
    }

下载结束

    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)
}
协议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)
        }
    }
}
处理图片数据
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 状态码 没有图像数据下载
                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
                }
                 //返回不是图片数据 或者数据被破坏
                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.下载某张特定图片

在 ```ImageDownloader```中有一个delegate属性  ```open weak var delegate: ImageDownloaderDelegate?```
你可以创建一个```ImageDownloader```,设置好delegate,调用下面方法,并且实现代理方法,就能下载这张图片

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```的大部分功能都已经提及,还有一些细节
结构体```RetrieveImageDownloadTask``` 是对```URLSessionDataTask```的进一层包装
有```cancel ```方法供外部调用

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```能够进行内存缓存和磁盘缓存。内存缓存由```NSCache```实现,磁盘缓存采用将image 转化成data ,加上FileManager操作文件完成。下面是主要实现功能

- 缓存路径管理
- 缓存的添加与删除
- 缓存的获取
- 缓存的清除
- 缓存状态检查

下面是```ImageCache```内部的属性: 
//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```用来管理内存缓存,```ioQueue``` 用来进行硬盘队列操作。由于硬盘存取操作相比于内存存取耗时,避免造成线程阻塞需单独开辟线程进行相应操作。```fileManager```用于文件管理。```diskCachePath```用于设置文件的存储路径。```maxCachePeriodInSecond```,最大的磁盘缓存时间,默认一周。```maxDiskCacheSize```最大的磁盘缓存大小。  ```processQueue```用于执行图片的 decode 操作。```default``` 为  ```ImageCache``` 类的单例,在Swift 中,调用 ```static let``` 可以直接创建一个单例,系统会自动调用```dispatch_once```。

####缓存路径相关的几个方法

- 根据key,serializer, options获取磁盘图片
- 根据key获取磁盘图片数据
- 根据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
}

}

####缓存的添加与删除
主要外部调用方法```store```,首先对传入的 URL Key 和 processorIdentifier 做简单拼接成computedKey,设置内存缓存。然后根据是否磁盘缓存 进一步处理,其中调用```CacheSerializer ``` 的 ```func data(with image: Image, original: Data?) -> Data?```方法,根据Data 获取图片类型,将image序列化成data 存入文件,其中path 是computedKey经过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)
{
//内存缓存
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 {
                        不存在磁盘缓存文件夹 创建 默认在 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()
    }
}
根据存入的key值移除缓存图片,如果需要移除磁盘缓存,删除对应文件

open func removeImage(forKey key: String,
processorIdentifier identifier: String = "",
fromDisk: Bool = true,
completionHandler: (() -> Void)? = nil)
{
根据key移除内存缓存
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 {
                根据key移除磁盘缓存
                try self.fileManager.removeItem(atPath: self.cachePath(forComputedKey: computedKey))
            } catch _ {}
            callHandlerInMainQueue()
        }
    } else {
        callHandlerInMainQueue()
    }
}
####缓存的获取
根据```key``` 获得缓存图片 首先从内存缓存中获取,如果无内存缓存,再判断磁盘缓存。如果有,从磁盘中获取缓存文件,将图片```data```反序列化成```image```,在返回之前判断了是否需要后台编码,做了内存缓存。这里返回的```RetrieveImageDiskTask``` 是一个```DispatchWorkItem```,相当于OC的```dispatch_block_t```,它定义了获取磁盘缓存并进行内存缓存的操作闭包,放在ioQueue中异步执行,确保了外部在操作过程中一直持有该缓存操作,相当于```ImageDownloader```的```RetrieveImageDownloadTask```,并且在返回之前都将sSelf置为nil,释放了内存。因为该闭包属于逃逸闭包,必需在闭包中显式地引用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
    首先判断内存缓存是否存在
    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)
                        内存缓存
                        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
}
  从内存中获取图片
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)
}
####缓存的清除
手动清除所有内存缓存和磁盘缓存
@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()
            }
        }
    }
}
当应用程序在进入后台的时候,可以自动检测过期缓存文件,并在后台完成清理操作,实现代码如下:
@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数组,磁盘缓存大小和缓存文件字典, 进行缓存删除操作。 通过```FileManager ```的```enumerator```方法遍历出所有缓存文件,如果文件最后一次访问日期比当前时间减去一周时间还要早,将该文件```fileUrl```添加到```urlsToDelete```数组。计算缓存文件大小,以```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到删除数组
                            urlsToDelete.append(fileUrl)
                            continue
                        }
                    }
                }

                if let fileSize = resourceValues.totalFileAllocatedSize {
                    //更新缓存大小
                    diskCacheSize += UInt(fileSize)
                    if !onlyForCacheSize {
                        // 缓存文件字典对应
                        cachedFiles[fileUrl] = resourceValues
                    }
                }
            } catch _ { }
        }
    }
    
    return (urlsToDelete, diskCacheSize, cachedFiles)
}
根据上面获取的```urlsToDelete```数组,```diskCacheSize```磁盘缓存大小和```cachedFiles```字典,删除过期缓存 。

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 _ { }
        }
        //磁盘缓存大小超过自定义最大缓存
        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?()
        }
    }
}
###缓存的状态检查

- 缓存类型结构体

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

- 根据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)
}
```
  • 根据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)
            }
        }
    }
    ```
- 根据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
该类用于将磁盘图片数据反序列化成图片对象以及将图片对象序列化成图片数据。具体功能由```Image```文件实现
 Image 序列化 Data。通过Data获取图片format,返回不同格式下图片。能实现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图片数据,只显示GIF中的第一张图片,true:将所有图片数据加载到内存,显示GIF动态图片 

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)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容