一、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
蝗罗,KingfisherOptionsInfo
。KingfisherOptionsInfo
就是傳入的配置參數(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)
}
可以看到session
是sessionConfiguration
和sessionHandler
來配置的纤虽。其中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
贺嫂,傳給fetchLoad
的downloadTask
屬性 配置好任務(wù)優(yōu)先級滓鸠,開啟下載任務(wù),如果已開啟下載第喳,下載次數(shù)加1糜俗,設(shè)置傳給外部的retrieveImageTask
的downloadTask
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)
}