Kingfisher是一个在iOS开发中被广泛使用的用于图片下载与缓存的Swift框架,只需要使用极少的代码,就能实现图片的下载与缓存,同时也提供了许多的配置,满足不同的需求。
整体架构
以下是Kingfisher的一个大概的架构,只涵盖了主要组件,还有一些组件如ImagePrefetcher、RetryStrategy等没有画进去。
主要由以下几个部分组成
- View Extension:各种View的扩展。其中还会设置image、placeholder、动画等。
- KingfisherManager:负责获取图片的逻辑,使用ImageDownloader和ImageCache分别进行下载和缓存,也会在下载失败的时候进行重试。
- ImageDownloader:负责下载相关功能。其中的SessionDelegate不仅仅是delegate,它更像是一个manager。
- ImageDataProcessor:下载之后,负责将图片从Data转为图片,并可配合不同的imageProcessor来处理图片。
- ImageCache:负责缓存,使用MemoryStorage和DiskStorage分别在内存和磁盘进行缓存。
整体流程
以下是Kingfisher的最简单的使用,仅仅使用这两行代码,就能实现自动下载url的图片并缓存。
KF.url(url)
.set(to: imageView)
我们可以先从下面这个简单的图来了解一下整个流程。
- 调用上述代码后,最终会调用imageView的
setImage扩展方法 - 方法内部调用
KingfisherManager去获取图片 KingfisherManager先调用ImageCache获取图片ImageCache内部优先从内存缓存中取图片,没有再从磁盘缓存中获取- 如果缓存中没有,
KingfisherManager才会让ImageDownloader开启下载任务 ImageDownloader下载完之后会初始化一个ImageDataProcessor将data转换成图片并通过回调把图片传给KingfisherManager。KingfisherManager获取到图片后会使用ImageCache先进行缓存,分别缓存到内存和磁盘- 缓存完成后回调
KingfisherManager,KingfisherManager再把图片回调给imageView的扩展的setImage方法,此方法里会把图片设置好
异步下载与缓存的实现原理
接下来我们会看一看上述提到的代码实现
Source
Source是一个重要类型,我们需要先了解一下它是什么,才会有利于阅读之后的代码。
我们先看下它的定义
public enum Source {
// 省略...
/// The target image should be got from network remotely. The associated `Resource`
/// value defines detail information like image URL and cache key.
case network(Resource)
/// The target image should be provided in a data format. Normally, it can be an image
/// from local storage or in any other encoding format (like Base64).
case provider(ImageDataProvider)
// 省略...
}
Source表示来源,在Kingfisher中,图片的来源分为这两个
- network:从网络中下载
- provider:本地提供
我们之所以能通过简单的一个url来下载图片,是因为Kingfisher提供了一个方法,将url转这个Source。
extension URL: Resource { //... }
extension Resource {
/// Converts `self` to a valid `Source` based on its `downloadURL` scheme. A `.provider` with
/// `LocalFileImageDataProvider` associated will be returned if the URL points to a local file. Otherwise,
/// `.network` is returned.
public func convertToSource(overrideCacheKey: String? = nil) -> Source {
let key = overrideCacheKey ?? cacheKey
return downloadURL.isFileURL ?
.provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) :
.network(ImageResource(downloadURL: downloadURL, cacheKey: key))
}
}
从这里也能看到,如果使用文件url,会转成provider,否则就会转成network。
设置图片
设置图片的方法位于ImageView的扩展方法,代码如下:
func setImage(
with source: Source?,
placeholder: Placeholder? = nil,
parsedOptions: KingfisherParsedOptionsInfo,
progressBlock: DownloadProgressBlock? = nil,
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
{
// ...
let task = KingfisherManager.shared.retrieveImage(
with: source,
options: options,
downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
completionHandler: { result in
CallbackQueue.mainCurrentOrAsync.execute {
// ...
switch result {
case .success(let value):
guard self.needsTransition(options: options, cacheType: value.cacheType) else {
mutatingSelf.placeholder = nil
// 设置图片
self.base.image = value.image
completionHandler?(result)
return
}
// 动画后设置图片
self.makeTransition(image: value.image, transition: options.transition) {
completionHandler?(result)
}
case .failure:
if let image = options.onFailureImage {
// 设置图片
self.base.image = image
}
completionHandler?(result)
}
}
}
)
mutatingSelf.imageTask = task
return task
}
从代码可知,它有两个时机会设置图片
- 获取图片成功后,使用动画或非动画的形式设置图片
- 获取图片失败后,可设置自定义的失败图片
KingfisherManager获取图片
从上面的代码可以看出,获取图片的功能是依靠KingfisherManager来实现的。
KingfisherManager是一个重要的类,它有以下几个职责
- 决定是从缓存中取图片还是下载图片
- 下载之后缓存图片
- 在下载失败的时候根据重试策略重新开始下载
- 低数据模式的处理
以下是它获取图片的代码
private func retrieveImage(
with source: Source,
context: RetrievingContext,
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
{
let options = context.options
// 强制下载
if options.forceRefresh {
return loadAndCacheImage(
source: source,
context: context,
completionHandler: completionHandler)?.value
} else {
// 优先从缓存中读取
let loadedFromCache = retrieveImageFromCache(
source: source,
context: context,
completionHandler: completionHandler)
if loadedFromCache {
return nil
}
// 只从缓存中取,否则报错
if options.onlyFromCache {
let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey))
completionHandler?(.failure(error))
return nil
}
// 下载并缓存
return loadAndCacheImage(
source: source,
context: context,
completionHandler: completionHandler)?.value
}
}
@discardableResult
func loadAndCacheImage(
source: Source,
context: RetrievingContext,
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
{
let options = context.options
func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
cacheImage(
source: source,
options: options,
context: context,
result: result,
completionHandler: completionHandler
)
}
switch source {
case .network(let resource):
let downloader = options.downloader ?? self.downloader
// 下载完成后缓存图片
let task = downloader.downloadImage(
with: resource.downloadURL, options: options, completionHandler: _cacheImage
)
if let task = task {
return .download(task)
} else {
return nil
}
case .provider(let provider):
// 从provider获取图片并缓存
provideImage(provider: provider, options: options, completionHandler: _cacheImage)
return .dataProviding
}
}
从代码可知
- 获取图片的时候总是优先从缓存中获取,没有再下载并缓存。
- 会根据
Source的不同选择不同的获取方式。
下载图片
当需要下载图片的时候,KingfisherManager会让ImageDownloader开始下载任务,以下是ImageDownloader开始下载任务的代码。
private func startDownloadTask(
context: DownloadingContext,
callback: SessionDataTask.TaskCallback
) -> DownloadTask
{
// 添加下载任务
let downloadTask = addDownloadTask(context: context, callback: callback)
let sessionTask = downloadTask.sessionTask
guard !sessionTask.started else {
return downloadTask
}
sessionTask.onTaskDone.delegate(on: self) { (self, done) in
// ...
switch result {
case .success(let (data, response)):
// 创建ImageDataProcessor将data转为图片
let processor = ImageDataProcessor(
data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
)
// 转换成功后会调用
processor.onImageProcessed.delegate(on: self) { (self, done) in
// ...
// 将image包装好
let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
let queue = callback.options.callbackQueue
// 回调包装好的结果
queue.execute { callback.onCompleted?.call(imageResult) }
}
// 开始转换
processor.process()
case .failure(let error):
callbacks.forEach { callback in
let queue = callback.options.callbackQueue
queue.execute { callback.onCompleted?.call(.failure(error)) }
}
}
}
// ..
// 立即开始
sessionTask.resume()
return downloadTask
}
从代码可知,ImageDownloader会先创建一个下载任务并立即开始,当下载完成后会将data转为图片,并包装成一个ImageLoadingResult类并回调出去,届时KingfisherManager就会收到结果,并可取出图片。
添加下载任务
在介绍如何添加下载任务之前,我们先了解一下下载任务的种类,它分为三种:
- DownloadTask
- SessionDataTask
- URLSessionDataTask
URLSessionDataTask是iOS的URLSession的下载任务。
他们的关系如下
知道了下载任务的种类,就能更好得理解添加下载任务的实现了。
ImageDownloader添加下载任务是添加到SessionDelegate里的,以下是SessionDelegate添加下载任务的代码。
private var session: URLSession
// ...
private func addDownloadTask(
context: DownloadingContext,
callback: SessionDataTask.TaskCallback
) -> DownloadTask
{
let downloadTask: DownloadTask
if let existingTask = sessionDelegate.task(for: context.url) {
// 对已存在的SessionDataTask,只增加一个callback
downloadTask = sessionDelegate.append(existingTask, url: context.url, callback: callback)
} else {
// 创建新的SessionDataTask
let sessionDataTask = session.dataTask(with: context.request)
sessionDataTask.priority = context.options.downloadPriority
downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)
}
return downloadTask
}
func append(
_ task: SessionDataTask,
url: URL,
callback: SessionDataTask.TaskCallback) -> DownloadTask
{
// 对已存在的SessionDataTask,只增加一个callback
let token = task.addCallback(callback)
return DownloadTask(sessionTask: task, cancelToken: token)
}
func add(
_ dataTask: URLSessionDataTask,
url: URL,
callback: SessionDataTask.TaskCallback) -> DownloadTask
{
lock.lock()
defer { lock.unlock() }
// 创建新的SessionDataTask
let task = SessionDataTask(task: dataTask)
task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in
// ... 取消任务实现
}
let token = task.addCallback(callback)
tasks[url] = task
return DownloadTask(sessionTask: task, cancelToken: token)
}
我们可以从中发现
- 创建
SessionDataTask需要一个URLSessionDataTask - 如果已存在对某个url的下载任务
SessionDataTask,则只会添加一个callback到其中,并创建一个新的DownloadTask。 - 如果不存在
SessionDataTask,则会创建SessionDataTask还有DownloadTask。
添加任务的流程大概是这样
下载完成
从上面的代码我们知道,下载是依靠URLSession实现的,
再来看看SessionDelegate的部分代码
open class SessionDelegate: NSObject {
//...
// 这是存储下载任务的地方
private var tasks: [URL: SessionDataTask] = [:]
}
extension SessionDelegate: URLSessionDataDelegate {
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let task = self.task(for: dataTask) else {
return
}
// 其中会拼接task的data
// mutableData.append(data)
task.didReceiveData(data)
//... 副作用
}
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let sessionTask = self.task(for: task) else { return }
// ...
onCompleted(task: task, result: result)
}
}
主要就是URLSessionDataDelegate的在相关代理方法中,拼接SessionDataTask的data并在complete的代理调用后完成后将结果回调出去。
值得注意的是,SessionDelegate的task属性存放着下载任务,是一个字典,所以一个URL只会对应一个下载任务SessionDataTask。
缓存实现原理
图片的缓存是要在下载完成之后由KingfisherManager调用ImageCache是做缓存的。
以下是KingfisherManager的下载并缓存的方法的实现:
@discardableResult
func loadAndCacheImage(
source: Source,
context: RetrievingContext,
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
{
let options = context.options
func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
cacheImage(
source: source,
options: options,
context: context,
result: result,
completionHandler: completionHandler
)
}
switch source {
case .network(let resource):
let downloader = options.downloader ?? self.downloader
// completionHandler直接调用_cacheImage
let task = downloader.
downloadImage(
with: resource.downloadURL, options: options, completionHandler: _cacheImage
)
// ...
}
}
可以看到,在下载完成后,就直接调用_cacheImage做缓存了。
cacheImage方法判断下载成功与否来做对应的操作,我们暂时只关注成功的操作。在成功的情况下,会调用ImageCache来进行缓存。
// Add image to cache.
let targetCache = options.targetCache ?? self.cache
targetCache.store(
value.image,
original: value.originalData,
forKey: source.cacheKey,
options: options,
toDisk: !options.cacheMemoryOnly)
{
_ in
coordinator.apply(.cachingImage) {
completionHandler?(.success(result))
}
}
以下是store方法的实现
open func store(_ image: KFCrossPlatformImage,
original: Data? = nil,
forKey key: String,
options: KingfisherParsedOptionsInfo,
toDisk: Bool = true,
completionHandler: ((CacheStoreResult) -> Void)? = nil)
{
// ...
// 计算缓存key
let computedKey = key.computedKey(with: identifier)
// 缓存到内存
memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
// ...
ioQueue.async {
let serializer = options.cacheSerializer
// 图片转data
if let data = serializer.data(with: image, original: original) {
// 保存到磁盘
self.syncStoreToDisk(
data,
forKey: key,
processorIdentifier: identifier,
callbackQueue: callbackQueue,
expiration: options.diskCacheExpiration,
writeOptions: options.diskStoreWriteOptions,
completionHandler: completionHandler)
} else {
// ...
}
}
}
以上能看出缓存的步骤大概是这样:
- 计算缓存key
- 缓存到内存
- 缓存到磁盘
上述代码使用了两个成员,memoryStorage和syncStoreToDisk中会调用的diskStorage。
public let memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>
public let diskStorage: DiskStorage.Backend<Data>
这里的Backend可以暂且理解为各自缓存的实现。
内存缓存原理
内存缓存的代码如下:
func storeNoThrow(
value: T,
forKey key: String,
expiration: StorageExpiration? = nil)
{
lock.lock()
defer { lock.unlock() }
// 过期时间
let expiration = expiration ?? config.expiration
guard !expiration.isExpired else { return }
let object: StorageObject<T>
// 这里决定在app进入后台的时候,是否要清除缓存
if config.keepWhenEnteringBackground {
object = BackgroundKeepingStorageObject(value, key: key, expiration: expiration)
} else {
object = StorageObject(value, key: key, expiration: expiration)
}
// 设置缓存
storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
keys.insert(key)
}
设置缓存是调用storage的方法,storage的定义如下
let storage = NSCache<NSString, StorageObject<T>>()
所以内存缓存的原理已经很明显了,就是利用NSCache来实现。
磁盘缓存原理
磁盘缓存的代码如下
public func store(
value: T,
forKey key: String,
expiration: StorageExpiration? = nil,
writeOptions: Data.WritingOptions = []) throws
{
// ...
// 过期时间
let expiration = expiration ?? config.expiration
// The expiration indicates that already expired, no need to store.
guard !expiration.isExpired else { return }
let data: Data
do {
data = try value.toData()
} catch {
throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))
}
let fileURL = cacheFileURL(forKey: key)
do {
try data.write(to: fileURL, options: writeOptions)
} catch {
throw KingfisherError.cacheError(
reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)
)
}
// ... 过期时间设置
}
磁盘的实现原理也很明显了,就是将图片转为data并使用write写入文件。
文件的路径是Cache文件夹加上一些标识和缓存key。
缓存key的计算
这里说的缓存key是针对默认的情况,缓存key是可以自定义的。
对于内存缓存来说,key是url
extension URL: Resource {
public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString }
public var downloadURL: URL { return self }
}
对于磁盘缓存来说,key是url的md5值。使用url作为key在内存找不到缓存的时候,就会把url转为md5去磁盘中找。
func cacheFileName(forKey key: String) -> String {
// ...
let hashedKey = key.kf.md5
// ...
return hashedKey
/// ...
}
缓存控制
不管是内存缓存还是磁盘缓存,它们都有一定的过期时间,使用它们的时候会延长过期时间,当某些事件发生的时候会清除掉过期的缓存,当然也可以选择自己手动清除。
过期时间与延长
在创建缓存的时候,会给缓存定义一个过期时间。对于内存,默认是300秒,而磁盘则是7天。每次使用它们的时候,会延长过期时间。
内存:
// 过期时间
public var expiration: StorageExpiration = .seconds(300)
// 使用延长
public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
guard let object = storage.object(forKey: key as NSString) else {
return nil
}
if object.expired {
return nil
}
// 延长过期时间
object.extendExpiration(extendingExpiration)
return object.value
}
对于磁盘,它的过期时间是写在file的attibutes上的
public func store(
value: T,
forKey key: String,
expiration: StorageExpiration? = nil,
writeOptions: Data.WritingOptions = []) throws
{
// ...
let now = Date()
let attributes: [FileAttributeKey : Any] = [
// The last access date.
.creationDate: now.fileAttributeDate,
// The estimated expiration date.
.modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
]
do {
try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path)
} catch {
// ...
}
// ...
}
// 过期时间
public var expiration: StorageExpiration = .days(7)
// 使用延长
func value(
forKey key: String,
referenceDate: Date,
actuallyLoad: Bool,
extendingExpiration: ExpirationExtending) throws -> T?
{
// ...
do {
let data = try Data(contentsOf: fileURL)
let obj = try T.fromData(data)
metaChangingQueue.async {
meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration)
}
return obj
} catch {
throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error))
}
}
缓存清除
对于内存缓存
- 借助NSCache本身的
totalCostLimit和countLimit。从createMemoryStorage可知,totalCostLimit默认是1/4的物理内存或者Int最大值,哪个小用哪个。countLimit:默认是Int的最大值。 - 内存缓存初始化后,会设置一个定时器,定时清除缓存,默认是120秒。
- 收到内存警告清除全部,通过监听
didReceiveMemoryWarningNotification通知。 - 进入后台会清除,但设置
BackgroundKeepingStorageObject实现NSDiscardableContent来防止这一行为。
对于磁盘缓存,未过期的缓存不会被自动清除,清除的都是过期的
- 程序即将推出清除过期的,监听
didReceiveMemoryWarningNotification。 - 即将进入后台是,监听
didEnterBackgroundNotification。
总结
以上说明了Kingfisher的主要是关于下载与缓存的实现原理,简单介绍了其中的各个组件的职责和实现,可能还会一些遗漏或错误的地方,还希望大家能多多包涵,不吝赐教。
本文首发于我的博客,十分感谢大家的阅读。