ImageDownloader 在Kingfisher
中,该类主要负责图片的网络下载,其实现原理是基于系统的URLSession
,实现它的代理方法。下面是几个主要部分:
ImageFetchLoad
URLSession的配置
下载方法
取消下载
URLSession 代理方法
下载某张特定图片
ImageFetchLoad 1
2
3
4
5
6
class ImageFetchLoad {
var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
var responseData = NSMutableData()
var downloadTaskCount = 0
var downloadTask: RetrieveImageDownloadTask?
}
ImageFetchLoad
是一个嵌套类。它处理了一个URL
下载数据,能够记录同一个URL
下载任务次数。其中的contents
属性是一个元组数组,该元组包含两个部分:CallbackPair
,KingfisherOptionsInfo
。KingfisherOptionsInfo
就是传入的配置参数,而 CallbackPair
也是一个元组,它包含了传入的两个闭包。ImageDownloaderProgressBlock
能够在每次接收到数据时调用,可以用来显示进度条,ImageDownloaderCompletionHandler
在数据接收完成之后会被调用。里面还有一个responseData
属性,能够把每次获取到的数据存储起来。那么 ImageDownloader
这个类有什么作用呢?通常情况下,ImageDownloader
往往要处理多个 URL
的下载任务,它的 fetchLoads
属性是一个 [URL: ImageFetchLoad]
类型的字典,存储不同 URL
及其 ImageFetchLoad
之间的对应关系。 下面是根据 URL
获取 ImageFetchLoad
的方法
1
2
3
4
5
func fetchLoad(for url: URL) -> ImageFetchLoad? {
var fetchLoad: ImageFetchLoad?
barrierQueue.sync { fetchLoad = fetchLoads[url] }
return fetchLoad
}
这里使用 barrierQueue
来操作,利用 sync
阻塞当前线程,完成 ImageFetchLoad
读操作后再返回。这样当读取 ImageFetchLoad
的时候,保证ImageFetchLoad 不会同时在被写,导致数据错误
URLSession的配置 来看一下 ImageDownloader
的构造器方法
1
2
3
4
5
6
7
8
9
10
11
12
13
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
作为代理的,但会造成内存泄漏 issue
1
2
3
4
/// 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.
下载方法 这是外部调用 ImageDownloader
最常用的方法 配置好请求参数:Time 、URL、 URLRequest ,确保请求的前提条件 主要是 setup
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
,传给 fetchLoad
的 downloadTask
属性 配置好任务优先级,开启下载任务,如果已开启下载,下载次数加1,设置传给外部的 retrieveImageTask
的 downloadTask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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
,开启下载
取消下载 1
2
3
4
5
6
7
8
9
10
11
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()
}
}
}
}
URLSession 代理方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}
1
2
3
4
5
6
7
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 处理会话认证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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) }
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}
下载某张特定图片 在 ImageDownloader
中有一个 delegate
属性 open weak var delegate:ImageDownloaderDelegate?
你可以创建一个 ImageDownloader
,设置好delegate,调用下面方法,并且实现代理方法,就能下载这张图片
1
2
3
4
5
6
7
8
9
10
11
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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// 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
方法供外部调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
内部的属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//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加密字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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加密获得
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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值移除缓存图片,如果需要移除磁盘缓存,删除对应文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
}
1
2
3
4
5
6
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
}
1
2
3
4
5
6
7
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)
}
缓存的清除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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()
}
}
}
}
当应用程序在进入后台的时候,可以自动检测过期缓存文件,并在后台完成清理操作,实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
字典,删除过期缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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?()
}
}
}
缓存的状态检查
1
2
3
4
public struct CacheCheckResult {
public let cached: Bool
public let cacheType: CacheType?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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查找缓存文件
1
2
3
4
5
6
7
8
9
10
11
12
/**
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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
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获取加密后的缓存路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
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
格式
1
2
3
4
5
6
7
8
9
10
11
12
13
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
动态图片
1
2
3
4
5
6
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)
}