Kingfisher是喵神写的一个异步下载和缓存图片的Swift库,github上将近3k的Star,相信不需要我再安利了。它的中文简介在这里,github地址在这里。
本次我们研究的是最新的基于Swift 4
Kingfisher的文档非常完备,我先大致看了一下,然后下载源码,跑了一下demo。demo中有这么一段:
let url = URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.row + 1).jpg")!
_ = (cell as! CollectionViewCell).cellImageView.kf.setImage(with: url,
placeholder: nil,
options: [.transition(ImageTransition.fade(1))],
progressBlock: { receivedSize, totalSize in
print("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
},
completionHandler: { image, error, cacheType, imageURL in
print("\(indexPath.row + 1): Finished")
})
这个kf_setImage显然是UIImageView的一个extension方法,既然是暴露出来供库的使用者调用的,应该就是抽象层面最高的。于是我command+click进去看了一下,它长这个样子,有点长,让我们分析下
@discardableResult // 改关键字意思是声明,告诉编译器此方法可以不用接收返回值。
public func setImage(with resource: Resource?,
placeholder: Placeholder? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: DownloadProgressBlock? = nil,
completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
{
guard let resource = resource else {
self.placeholder = placeholder
setWebURL(nil)
completionHandler?(nil, nil, .none, nil)
return .empty
}
var options = KingfisherManager.shared.defaultOptions + (options ?? KingfisherEmptyOptionsInfo)
let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet { // Always set placeholder while there is no image/placehoer yet.
self.placeholder = placeholder
}
let maybeIndicator = indicator
maybeIndicator?.startAnimatingView()
setWebURL(resource.downloadURL)
if base.shouldPreloadAllAnimation() {
options.append(.preloadAllAnimationData)
}
/**
代码块A (方便解说, 将代码拆开, 后面同样标记,标识同一位置代码)
/
}
第一个参数 resource Resource协议里面包含了两个属性,cacheKey和downloadURL,cacheKey就是原URL的完整字符串,之后会作为缓存的键使用(内存缓存直接使用cacheKey作为NSCache的键,文件缓存把cacheKey进行MD5加密后的字符串作为缓存文件名)
第二个参数类型KingfisherOptionsInfo?是什么呢?它是一个类型别名:public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem],而KingfisherOptionsInfoItem是一个enum:
public enum KingfisherOptionsInfoItem {
case targetCache(ImageCache)
case downloader(ImageDownloader)
case transition(ImageTransition)
case downloadPriority(Float)
case forceRefresh
case forceTransition
case cacheMemoryOnly
case onlyFromCache
case backgroundDecode
case callbackDispatchQueue(DispatchQueue?)
case scaleFactor(CGFloat)
case preloadAllAnimationData
case requestModifier(ImageDownloadRequestModifier)
case processor(ImageProcessor)
case cacheSerializer(CacheSerializer)
case keepCurrentImageWhileLoading
case onlyLoadFirstFrame
case cacheOriginalImage
}
这个枚举的每个枚举项都有关联值,包含了很多信息
TargetCache指定一个缓存器(ImageCache的一个实例),Downloader指定一个下载器(ImageDownloader的一个实例),Transition指定显示图片的动画效果(提供淡入和从上下左右进入这5种效果,也可以传入自定义效果)。
第三个参数类型是DownloadProgressBlock,也是一个别名:
public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> ())
实际上是一个闭包类型,具体会在什么时候调用待会儿会看到。第四个参数类型CompletionHandler也一样是个闭包类型的别名:
public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> ())
这个看名字就知道会在操作结束之后调用。
返回类型是RetrieveImageTask,它是长这样的:
public class RetrieveImageTask {
public static let empty = RetrieveImageTask()
var cancelledBeforeDownloadStarting: Bool = false
public var downloadTask: RetrieveImageDownloadTask?
/**
Cancel current task. If this task is already done, do nothing.
*/
public func cancel() {
if let downloadTask = downloadTask {
downloadTask.cancel()
} else {
cancelledBeforeDownloadStarting = true
}
}
}
简单来说它就是一个接收图片的任务
/**
代码块A
/
let task = KingfisherManager.shared.retrieveImage(
with: resource,
options: options,
progressBlock: { receivedSize, totalSize in
guard resource.downloadURL == self.webURL else {
return
}
if let progressBlock = progressBlock {
progressBlock(receivedSize, totalSize)
}
},
completionHandler: {[weak base] image, error, cacheType, imageURL in
DispatchQueue.main.safeAsync {
maybeIndicator?.stopAnimatingView()
guard let strongBase = base, imageURL == self.webURL else {
completionHandler?(image, error, cacheType, imageURL)
return
}
self.setImageTask(nil)
guard let image = image else {
completionHandler?(nil, error, cacheType, imageURL)
return
}
guard let transitionItem = options.lastMatchIgnoringAssociatedValue(.transition(.none)),
case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
{
self.placeholder = nil
strongBase.image = image
completionHandler?(image, error, cacheType, imageURL)
return
}
#if !os(macOS)
UIView.transition(with: strongBase, duration: 0.0, options: [],
animations: { maybeIndicator?.stopAnimatingView() },
completion: { _ in
self.placeholder = nil
UIView.transition(with: strongBase, duration: transition.duration,
options: [transition.animationOptions, .allowUserInteraction],
animations: {
// Set image property in the animation.
transition.animations?(strongBase, image)
},
completion: { finished in
transition.completion?(finished)
completionHandler?(image, error, cacheType, imageURL)
})
})
#endif
}
})
setImageTask(task)
return task
KingfisherManager 是个单利, swift 创建单利十分简单
public static let shared = KingfisherManager()
KingfisherManager 的单利调用了 retrieveImage 它整合了下载和缓存两大功能,先看一下完整的方法签名, 认为是整个KingfisherManager的核心:
@discardableResult
public func retrieveImage(with resource: Resource,
options: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask
{
// 新建任务
let task = RetrieveImageTask()
let options = currentDefaultOptions + (options ?? KingfisherEmptyOptionsInfo)
//若强制刷新则联网下载并缓存
if options.forceRefresh {
_ = downloadAndCacheImage(
with: resource.downloadURL,
forKey: resource.cacheKey,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: completionHandler,
options: options)
} else {
//不强制刷新则从缓存中取
tryToRetrieveImageFromCache(
forKey: resource.cacheKey,
with: resource.downloadURL,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: completionHandler,
options: options)
}
return task
}
分析从缓存中获取 tryToRetrieveImageFromCache
//不强制刷新则从缓存中取
func tryToRetrieveImageFromCache(forKey key: String,
with url: URL,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo)
{
let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
completionHandler?(image, error, cacheType, imageURL)
}
func handleNoCache() {
if options.onlyFromCache {
let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
diskTaskCompletionHandler(nil, error, .none, url)
return
}
self.downloadAndCacheImage(
with: url,
forKey: key,
retrieveImageTask: retrieveImageTask,
progressBlock: progressBlock,
completionHandler: diskTaskCompletionHandler,
options: options)
}
let targetCache = options.targetCache
// First, try to get the exactly image from cache
targetCache.retrieveImage(forKey: key, options: options) { image, cacheType in
// If found, we could finish now.
if image != nil {
diskTaskCompletionHandler(image, nil, cacheType, url)
return
}
// If not found, and we are using a default processor, download it!
let processor = options.processor
guard processor != DefaultImageProcessor.default else {
handleNoCache()
return
}
// If processor is not the default one, we have a chance to check whether
// the original image is already in cache.
let optionsWithoutProcessor = options.removeAllMatchesIgnoringAssociatedValue(.processor(processor))
targetCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { image, cacheType in
// If we found the original image, there is no need to download it again.
// We could just apply processor to it now.
guard let image = image else {
handleNoCache()
return
}
guard let processedImage = processor.process(item: .image(image), options: options) else {
diskTaskCompletionHandler(nil, nil, .none, url)
return
}
targetCache.store(processedImage,
original: nil,
forKey: key,
processorIdentifier:options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: !options.cacheMemoryOnly,
completionHandler: nil)
diskTaskCompletionHandler(processedImage, nil, .none, url)
}
}
}
开始下载任务
上次说到了downloadAndCacheImage这个方法,看名字就知道既要下载图片又要缓存图片,它的方法体是这样的:
@discardableResult
func downloadAndCacheImage(with url: URL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
{
let downloader = options.downloader
return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
progressBlock: { receivedSize, totalSize in
progressBlock?(receivedSize, totalSize)
},
completionHandler: { image, error, imageURL, originalData in
downLoader 调用了downloadImage方法然后在completionHandler这个完成闭包中做缓存相关的操作,我们先不管缓存,先去downloadImage(downloader是它的一个实例)里看看downloadImage这个方法,它是长这样的:
@discardableResult
open func downloadImage(with url: URL,
retrieveImageTask: RetrieveImageTask? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: ImageDownloaderProgressBlock? = nil,
completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
{
if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
return nil
}
// 设置请求超时时间
let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
// We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
// 创建request 忽略本地和远程的缓存数据,直接从原始地址下
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
// 请求和响应是顺序的, 也就是说请求–>得到响应后,再请求
request.httpShouldUsePipelining = requestsUsePipelining
if let modifier = options?.modifier {
guard let r = modifier.modified(for: request) else {
completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
return nil
}
request = r
}
// There is a possiblility that request modifier changed the url to `nil` or empty.
// 请求被修改空的可能性
guard let url = request.url, !url.absoluteString.isEmpty else {
completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
return nil
}
var downloadTask: RetrieveImageDownloadTask?
setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
if fetchLoad.downloadTask == nil {
let dataTask = session.dataTask(with: request)
fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
dataTask.resume()
self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
// Hold self while the task is executing.
self.sessionHandler.downloadHolder = self
}
fetchLoad.downloadTaskCount += 1
downloadTask = fetchLoad.downloadTask
retrieveImageTask?.downloadTask = downloadTask
}
return downloadTask
}
里面有setup 方法 这个方法之前的部分都是发送网络请求之前的处理
func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
func prepareFetchLoad() {
barrierQueue.sync(flags: .barrier) {
let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
fetchLoads[url] = loadObjectForURL
if let session = session {
started(session, loadObjectForURL)
}
}
}
if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
if fetchLoad.cancelSemaphore == nil {
fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
}
cancelQueue.async {
_ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
fetchLoad.cancelSemaphore = nil
prepareFetchLoad()
}
} else {
prepareFetchLoad()
}
}
这个fetchLoads是一个以URL为键,ImageFetchLoad为值的Dictionary,ImageFetchLoad是ImageDownloader中的一个内部类,它的声明如下
class ImageFetchLoad {
var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
var responseData = NSMutableData()
var downloadTaskCount = 0
var downloadTask: RetrieveImageDownloadTask?
var cancelSemaphore: DispatchSemaphore?
}
//先是用图片的URL去self.fetchLoads里取对应的ImageFetchLoad, 如果没有的话就以当前URL为键创建一个,然后把传过来的progressBlock和completionHandler打包成一个元组,和options组成新元素, 添加到ImageFetchLoad里的contents数组中, 准备好之后,在闭包里面开始下载
if fetchLoad.downloadTask == nil {
let dataTask = session.dataTask(with: request)
fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
dataTask.resume()
self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
// Hold self while the task is executing.
self.sessionHandler.downloadHolder = self
}
fetchLoad.downloadTaskCount += 1
downloadTask = fetchLoad.downloadTask
retrieveImageTask?.downloadTask = downloadTask
这里使用了NSURLSession,是iOS7之后比较主流的用于网络请求的API(iOS7以前多使用NSURLConnection)
ImageDownloaderSessionHandler 实现URLSessionDataDelegate 代理
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let downloader = downloadHolder else {
return
}
if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
//向fetchLoads[URL].responseData添加一条响应数据
fetchLoad.responseData.append(data)
if let expectedLength = dataTask.response?.expectedContentLength {
for content in fetchLoad.contents {
//依次调用fetchLoads的contents中的所有过程回调
DispatchQueue.main.async {
content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
}
}
}
}
}
这个函数会在接收到数据的时候被调用
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let url = task.originalRequest?.url else {
return
}
guard error == nil else {
callCompletionHandlerFailure(error: error!, url: url)
return
}
// 处理加工
processImage(for: task, url: url)
}
这个方法是在请求完成之后调用
其中processImage 具体是
private func processImage(for task: URLSessionTask, url: URL) {
guard let downloader = downloadHolder else {
return
}
// We are on main queue when receiving this.
// 下载完成后处理
downloader.processQueue.async {
guard let fetchLoad = downloader.fetchLoad(for: url) else {
return
}
self.cleanFetchLoad(for: url) // 清除旧的url
let data: Data?
let fetchedData = fetchLoad.responseData as Data
if let delegate = downloader.delegate {
data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url)
} else {
data = fetchedData
}
// Cache the processed images. So we do not need to re-process the image if using the same processor.
// Key is the identifier of processor.
var imageCache: [String: Image] = [:]
for content in fetchLoad.contents {
let options = content.options
let completionHandler = content.callback.completionHandler
let callbackQueue = options.callbackDispatchQueue
let processor = options.processor
var image = imageCache[processor.identifier]
if let data = data, image == nil { // 将data 转成image
image = processor.process(item: .data(data), options: options)
// Add the processed image to cache.
// If `image` is nil, nothing will happen (since the key is not existing before).
imageCache[processor.identifier] = image
}
if let image = image {
//下载完成后可以进行的自定义操作,用户可以自行指定delegate
downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
if options.backgroundDecode {
let decodedImage = image.kf.decoded
callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
} else {
callbackQueue.safeAsync { completionHandler?(image, nil, url, data) }
}
} else {
//不能生成图片,返回304状态码,表示图片没有更新,可以直接使用缓存
if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil)
completionHandler?(nil, notModified, url, nil)
continue
}
//不能生成图片,报BadData错误
let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
}
}
}
}
主要的委托方法都看完了,最后还有一个跟身份认证有关的:
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let downloader = downloadHolder else {
return
}
downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler)
}
func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//一般用于SSL/TLS协议(https)
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
//在白名单中的域名做特殊处理,忽视警告
if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(.useCredential, credential)
return
}
}
//默认处理
completionHandler(.performDefaultHandling, nil)
}
trustedHosts是ImageDownloader里声明的一个字符串集合,应该就是类似于一个白名单,放到里面的域名是可以信任的。
缓存模块
我们是从KingfisherManager中的downloadAndCacheImage为入口进入到下载模块的,缓存模块也从这里进入。再贴一下downloadAndCacheImage吧:
@discardableResult
func downloadAndCacheImage(with url: URL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
{
let downloader = options.downloader
return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
progressBlock: { receivedSize, totalSize in
progressBlock?(receivedSize, totalSize)
},
completionHandler: { image, error, imageURL, originalData in
let targetCache = options.targetCache
// 在下载完图片之后的完成闭包中(会在下载请求结束后调用),如果服务器返回状态码notModified,说明服务器图片未更新,我们可以从缓存中取得图片数据,就是调用retrieveImage 我们进入到retrieveImage查看
if let error = error, error.code == KingfisherError.notModified.rawValue {
// Not modified. Try to find the image from cache.
// (The image should be in cache. It should be guaranteed by the framework users.)
targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
completionHandler?(cacheImage, nil, cacheType, url)
})
return
}
if let image = image, let originalData = originalData {
targetCache.store(image,
original: originalData,
forKey: key,
processorIdentifier:options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: !options.cacheMemoryOnly,
completionHandler: nil)
if options.cacheOriginalImage {
let defaultProcessor = DefaultImageProcessor.default
if let originaliImage = defaultProcessor.process(item: .data(originalData), options: options) {
targetCache.store(originaliImage,
original: originalData,
forKey: key,
processorIdentifier: defaultProcessor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: !options.cacheMemoryOnly,
completionHandler: nil)
}
}
}
completionHandler?(image, error, .none, url)
})
}
// 在下载完图片之后的完成闭包中(会在下载请求结束后调用),如果服务器返回状态码notModified,说明服务器图片未更新,我们可以从缓存中取得图片数据,就是调用retrieveImage 我们进入到retrieveImage查看
- 给完成闭包进行解包,若为空则提前返回:
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
return nil
}
- 如果内存中有缓存则直接从内存中取图片;再判断图片是否需要解码,若需要,则先解码再调用完成闭包,否则直接调用完成闭包:
@discardableResult
open func retrieveImage(forKey key: String,
options: KingfisherOptionsInfo?,
completionHandler: ((Image?, CacheType) -> ())?) -> RetrieveImageDiskTask?
{
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
return nil
}
var block: RetrieveImageDiskTask?
let options = options ?? KingfisherEmptyOptionsInfo
if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
options.callbackDispatchQueue.safeAsync {
completionHandler(image, .memory)
}
} else {
var sSelf: ImageCache! = self
block = DispatchWorkItem(block: {
// Begin to load image from disk
if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
if options.backgroundDecode {
sSelf.processQueue.async {
let result = image.kf.decoded
sSelf.store(result,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil)
options.callbackDispatchQueue.safeAsync {
completionHandler(result, .memory)
sSelf = nil
}
}
} else {
sSelf.store(image,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil
)
options.callbackDispatchQueue.safeAsync {
completionHandler(image, .disk)
sSelf = nil
}
}
} else {
// No image found from either memory or disk
options.callbackDispatchQueue.safeAsync {
completionHandler(nil, .none)
sSelf = nil
}
}
})
sSelf.ioQueue.async(execute: block!)
}
return block
}
- 如果内存中没有缓存,则从文件中取图片,并判断是否需要进行解码,若需要则先解码再将它缓存到内存中然后执行完成闭包,否则直接缓存到内存中然后执行完成闭包
获取图片就是这样了,这个方法里调用了store这个方法,显然是用来缓存图片的,来看一下它的具体逻辑:
memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
缓存到内存中
如果方法参数toDisk为true则先将其缓存到文件(如果图片数据存在并能被正确解析的话),然后调用完成闭包:
open func store(_ image: Image,
original: Data? = nil,
forKey key: String,
processorIdentifier identifier: String = "",
cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
toDisk: Bool = true,
completionHandler: (() -> Void)? = nil)
{
let computedKey = key.computedKey(with: identifier)
memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
func callHandlerInMainQueue() {
if let handler = completionHandler {
DispatchQueue.main.async {
handler()
}
}
}
if toDisk {
ioQueue.async {
if let data = serializer.data(with: image, original: original) {
if !self.fileManager.fileExists(atPath: self.diskCachePath) {
do {
try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {}
}
self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
}
callHandlerInMainQueue()
}
} else {
callHandlerInMainQueue()
}
}
- 整个缓存逻辑就是这样
ImageCache中还有一个删除过期缓存的方法
fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
// 一些准备工作,取缓存路径,过期时间等:
let diskCacheURL = URL(fileURLWithPath: diskCachePath)
let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
var cachedFiles = [URL: URLResourceValues]()
var urlsToDelete = [URL]()
var diskCacheSize: UInt = 0
// 遍历缓存图片(跳过隐藏文件和文件夹),如果图片过期,则加入待删除队列:
fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
// 一些准备工作,取缓存路径,过期时间等:
let diskCacheURL = URL(fileURLWithPath: diskCachePath)
let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
var cachedFiles = [URL: URLResourceValues]()
var urlsToDelete = [URL]()
var diskCacheSize: UInt = 0
for fileUrl in (try? fileManager.contentsOfDirectory(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)) ?? [] {
do {
let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
// If it is a Directory. Continue to next file URL.
//跳过目录
if resourceValues.isDirectory == true {
continue
}
// If this file is expired, add it to URLsToDelete
//若文件最新更新日期超过过期日期,则放入待删除队列
if !onlyForCacheSize,
let expiredDate = expiredDate,
let lastAccessData = resourceValues.contentAccessDate,
(lastAccessData as NSDate).laterDate(expiredDate) == expiredDate
{
urlsToDelete.append(fileUrl)
continue
}
if let fileSize = resourceValues.totalFileAllocatedSize {
diskCacheSize += UInt(fileSize)
if !onlyForCacheSize {
cachedFiles[fileUrl] = resourceValues
}
}
} catch _ { }
}
return (urlsToDelete, diskCacheSize, cachedFiles)
}
若剩余缓存内容超过预设的最大缓存尺寸,则删除存在时间较长的缓存,并将已删除图片的URL也加大删除队列中(为了一会儿的广播),直到缓存尺寸到达预设最大尺寸的一半:
open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {
// Do things in cocurrent io queue
ioQueue.async {
var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
for fileURL in URLsToDelete {
do {
try self.fileManager.removeItem(at: fileURL)
} catch _ { }
}
// 若当前缓存内容超过预设的最大缓存尺寸,则先将文件根据时间排序(旧的在前),然后开始循环删除,直到尺寸降到最大缓存尺寸的一半。
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?()
}
}
}
在主线程广播已删除的缓存图片,如果有传入完成闭包的话,就调用它:
缓存模块的主要内容就这些了,其他还有一些辅助方法像计算缓存尺寸啊、图片的排序啊、把图片URL进行MD5加密作为缓存文件名啊等等,具体写了,有兴趣的同学可以直接去看源码。在UIImage+Extension文件中还有一些处理图片的扩展方法,诸如标准化图片格式、GIF图片的存储、GIF图片的展示等等,这些都算是一些套路上的东西,正确调用苹果给的API就好了.
Kingfisher中还用到了很多小技巧,比如对关联对象(Associated Object)的使用,解决了extension不能扩展存储属性的问题:
public var webURL: URL? {
return objc_getAssociatedObject(base, &lastURLKey) as? URL
}
fileprivate func setWebURL(_ url: URL?) {
objc_setAssociatedObject(base, &lastURLKey, url, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
//全局变量,用来作为关联对象(估计是因为extension里面不能添加储存属性,只能通过关联对象配合计算属性和方法的方式来hack)
总结:
- 文件操作相关知识(遍历文件、跳过隐藏文件、按日期排序文件等等)
- 图片处理相关知识(判断图片格式、处理GIF等等)
- MD5摘要算法(这个我并没有仔细看)
- Associated Object的运用
- Swift中关于enum和模式匹配的优雅用法
由于时间长促,下次改善排版,尽可能详细阐述每个方法