源码地址: https://github.com/onevcat/Kingfisher
Kingfisher是iOS圈内有名的王威写的三方库,参考SDWebImage用Swift语言实现。 三方图片库最重要的功能是图片并发下载、内存管理、图片格式转换、异步管理、文件管理等。 看看Kingfisher是怎么做的:
1、 图片下载, Kingfisher对URLSession、URLSessionDelegate进行了封装, 参考URLSession学习笔记;
2、 图片缓存, Kingfisher使用二级缓存:内存和文件; 内存是用ios NSCache实现(参考NSCache内存优化), 文件默认超过7天会删除; **内存和文件存储的图片只可能是PNG、JPEG或GIF这三种数据类型。 **
3、图片格式转换,包括Data和Image互转、图片转向、缩放、裁剪、圆形显示等都是iOS系统类实现, 具体在Image.swift实现。
4、文件管理, 使用FileManager遍历文件夹, 比对图片文件创建时间和当前时间之差并删除过期文件。
5、异步管理, 使用DispatchQueue实现异步操作,避免阻塞主线程。 参考GCD多线程详解
6、下载图片任务的优先级, 通过设置URLSessionTask的priority属性实现。
7、在app退出到后台运行时, Kingfisher会自动检测缓存文件总大小是否超过阈值并删除逾期或最远修改的文件;
一张图说明Kingfisher的代码结构:
其中最重要的就是ImageCache.swift和ImageDownloader.swift, 分别实现下载和缓存功能。
从Kingfisher用法开始debug跟踪:
let url = URL(string: "url_of_your_image")
imageView.kf.setImage(with: url)
1、 以UIImageView对象为例, 其setImage方法是在UIImageView+Kingfisher.swift里定义的。 **拿到Image对象后最核心的语句是strongBase.image = image, 即将图片显示在UIImageView控件里。
public func setImage(with resource: Resource?,
placeholder: Placeholder? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: DownloadProgressBlock? = nil,
completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
{
//参数合法性判断、设置读取策略和启动动画
….
//从内存、文件或网络获取url对应图片数据
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.placeholder = nil
strongBase.image = image //真正的将图片显示出来,strongBase即是UIImageView
completionHandler?(image, error, cacheType, imageURL)
……
#if !os(macOS)
… #endif
}
})
setImageTask(task)
return task
}
PS: setImageTask(task)使用iOS运行时关联对象语法缓存task实例。 下面是每个下载任务包含的关联对象key:
// MARK: - Associated Object
private var lastURLKey: Void?
private var indicatorKey: Void?
private var indicatorTypeKey: Void?
private var placeholderKey: Void?
private var imageTaskKey: Void?
2、 下面分析KingfisherManager.shared.retrieveImage函数, 看Kingfisher是如何拿到Image对象的。
注意downloadAndCacheImage函数参数的缩进方式, 强烈推荐!!! 排版看着比较舒服。
open func downloadImage(with url: URL,
retrieveImageTask: RetrieveImageTask? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: ImageDownloaderProgressBlock? = nil,
completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
{
….
if let modifier = options?.modifier { //拦截器,修改URLRequest。例如添加验签参数
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
}
….
var downloadTask: RetrieveImageDownloadTask?
setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
….
dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority //优先级
dataTask.resume() //URLSessionTask的resume方法,即开始执行
self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
…..
return downloadTask
}
在ImageDownloader.swift里由URLSessionDelegate的回调函数, 可以处理下载进度和下数据等。
processImage函数处理数据并执行回调, 缓存图片数据并刷新界面。
当app退出到后台运行时, Kingfisher会自动删除逾期的文件。 删除逻辑是在cleanExpiredDiskCache函数实现的。
/**
Clean expired disk cache when app in background. This is an async operation.
In most cases, you should not call this method explicitly.
It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received.
*/
@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!)
}
}
删除逾期文件; 当文件总大小超过阈值时删除最远修改日期的文件,直到总大小低于阈值。
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 //判断总大小在小于阈值时退出
}
}
}
。。。。
}
}
上面代码中URLsToDelete数组是在travelCachedFiles函数里赋值的:
PS:Kingfisher使用default作为单例模式的参数名称, default是Swift的关键字,需要转义后才能作为参数使用, 其它关键字也可以这样转义。
public static let `default` = DefaultCacheSerializer()
网友评论