写在开头:
- 作为一个iOS开发你也许不知道图片的内存管理机制、也不不知道图片的解析机制、但是你肯定用过SDWebImage,也许用过Kingfisher。
- 大多数人习惯了只要加载图片都用第三方, 但是你真正了解第三方为你做了什么吗?为什么我们不自己去下载图片,并且管理呢?
也许你看完这篇文章心里就会有个答案
我们就从源码开始说起吧:
-
首先,我们就一起分析一下该框架的组成。
将KF导入工程后,下面是其结构:
除去support Files, 项目大致分为5个模块:
- 图片存储模块(imageCache)
- 图片下载模块(imageDownloader)
- imageView分类模块(imageView+Kingfisher)
- 图片加载模块(kingfisherManager)
- 缓存key处理模块(String+MD5)
其核心文件是KingfisherManager里面包含了图片赋值策略。
我们从给图片赋值开始看:
// kf是命名空间,swift中方法名不在是前缀加下划线显示而是采用了命名空间形式,跟原生库更接近
let imageV = UIImageView()
imageV.kf.setImage(with: URL(string: self.imageStr), placeholder: nil, options: nil, progressBlock: nil, completionHandler: nil)
我们创建了一个imageView,然后通过kf调用setImage给imageView赋值,我们点进去看看做了什么
// discardableResult表示返回值可以忽略不会出现警告
@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
}
// options是个数组,存储了关于图片加载的配置
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)
}
// 真正加载图片的方法
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
}
- 这个就是图片的设置方法,大概的流程注释已经写清楚了, 还有三点需要详细说明下:
- options:是KingfisherOptionsInfoItem类型的枚举,存储了关于图片缓存策略相关的以及下载策略相关配置
- retrieveImage是真实获取图片数据方法
- completionHandler: 是查找或下载结果的回调。
@discardableResult
public func retrieveImage(with resource: Resource,
options: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask
我们可以看到这个方法内部根据option参数不同调用了不同的方法:
- 第一个方法:
func downloadAndCacheImage(with url: URL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
- 第二个方法:
func tryToRetrieveImageFromCache(forKey key: String,
with url: URL,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo)
至此图片加载的简单流程结束了。
一 、接下来我们看第一个图片加载策略:
@discardableResult
func downloadAndCacheImage(with url: URL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
{
// 图片下载器, 内部定义了默认超时时间是15s,图片下载所需的URLSession, 以及其它一些配置
let downloader = options.downloader ?? self.downloader
// 单独的数据处理队列
let processQueue = self.processQueue
// 下载图片,并通过回调函数返回
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 ?? self.cache
// 如果图片下载失败则从缓存查找,并返回
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) -> Void in
completionHandler?(cacheImage, nil, cacheType, url)
})
return
}
// 存储图片数据
if let image = image, let originalData = originalData {
targetCache.store(image,
original: originalData,
forKey: key,
// 根据option存储策略存储原始图片 processorIdentifier:options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: !options.cacheMemoryOnly,
completionHandler: {
guard options.waitForCache else { return }
let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
completionHandler?(image, nil, cacheType, url)
})
if options.cacheOriginalImage && options.processor != DefaultImageProcessor.default {
let originalCache = options.originalCache ?? targetCache
let defaultProcessor = DefaultImageProcessor.default
processQueue.async {
if let originalImage = defaultProcessor.process(item: .data(originalData), options: options) {
originalCache.store(originalImage,
original: originalData,
forKey: key,
processorIdentifier: defaultProcessor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: !options.cacheMemoryOnly,
completionHandler: nil)
}
}
}
}
if options.waitForCache == false || image == nil {
completionHandler?(image, error, .none, url)
}
})
}
对这个方法做个简单的总结。这个方法总主要做了三件事:
- 调用了downloadImage方法,然后通过completionHandler这个回调函数把image, error,imageURL, originalData回调给我们。
- 如果下载失败则从缓存中中查找图片
- 拿到的图片数据存储起来,并且回调给外部使用
下面我们根据源码来分析作者如何实现这个三个功能的
图片下载:
@discardableResult
open func downloadImage(with url: URL,
retrieveImageTask: RetrieveImageTask? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: ImageDownloaderProgressBlock? = nil,
completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
{
// 如果这个任务在开始之前就已经被取消则返回nil
if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
return nil
}
// 外界如果给了超时时间没给就15s
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 = 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 possibility 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是一个任务管理队列,里面用了semphore作为锁去保证线程安全,spinlock会造成死锁。
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
self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
dataTask.resume()
// Hold self while the task is executing.
self.sessionHandler.downloadHolder = self
}
fetchLoad.downloadTaskCount += 1
downloadTask = fetchLoad.downloadTask
retrieveImageTask?.downloadTask = downloadTask
}
return downloadTask
}
返回的RetrieveImageDownloadTask暴露给外部可以让下载中的任务取消
从缓存查找图片
// MARK: - Get data from cache
/**
Get an image for a key from memory or disk.
- parameter key: Key for the image. - 查找图片的URL
- parameter options: Options of retrieving image. If you need to retrieve an image which was
stored with a specified `ImageProcessor`, pass the processor in the option too.
- parameter completionHandler: Called when getting operation completes with image result and cached type of
this image. If there is no such key cached, the image will be `nil`.
- returns: The retrieving task.
*/
@discardableResult
open func retrieveImage(forKey key: String,
options: KingfisherOptionsInfo?,
completionHandler: ((Image?, CacheType) -> Void)?) -> RetrieveImageDiskTask?
{
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
return nil
}
var block: RetrieveImageDiskTask?
let options = options ?? KingfisherEmptyOptionsInfo
let imageModifier = options.imageModifier
// 从内存中查找图片, key为图片的url,未处理
if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
options.callbackDispatchQueue.safeAsync {
// 把查找结果回调出去-
completionHandler(imageModifier.modify(image), .memory)
}
} else if options.fromMemoryCacheOrRefresh { // Only allows to get images from memory cache.
options.callbackDispatchQueue.safeAsync {
completionHandler(nil, .none)
}
} else {
var sSelf: ImageCache! = self
block = DispatchWorkItem(block: {
// Begin to load image from disk
// 从磁盘查找图片,key在这里为图片的URL经过md5处理。
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(imageModifier.modify(result), .disk)
// 释放资源
sSelf = nil
}
}
} else {
// 存储图片
sSelf.store(image,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil
)
options.callbackDispatchQueue.safeAsync {
completionHandler(imageModifier.modify(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 an image to cache. It will be saved to both memory and disk. It is an async operation.
- parameter image: 将要存储的图片
- parameter original: 图片的data,将会跟图片转成的data对比,用来获取图片类型.如果为空则会存储一个png类型的图片.
- parameter key: 存储图片的key.
- parameter identifier: 用来处理图片的一个标识符.
- parameter toDisk: 是否存储到磁盘,如果是false则仅仅存储在缓存内.
- parameter completionHandler: 结果回调.
*/
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)
{
// 如果ID为空, 则直接返回key,否则拼接上ID
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 保证数据操作安全
ioQueue.async {// 存储到磁盘
// 根据image获取图片的data。如果未传入data,则所有图片按照png类型,转为data。data主要用来判断图片格式, 通过image生成一个data
if let data = serializer.data(with: image, original: original) {
// 文件存储在cache中(iOS文件结构: Documents, Library:(Cache , Preference) Tmp)
if !self.fileManager.fileExists(atPath: self.diskCachePath) {
do {
try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {}
}
// cachePath(), 存储路径,对文件名称进行md5处理。可以保证唯一性和长度固定等
self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
}
// 存储结束通知外面
callHandlerInMainQueue()
}
} else {
callHandlerInMainQueue()
}
}
二 、接下来我们看第二个图片加载策略:
tryToRetrieveImageFromCache
看完第一个第二个基本不用看,第二个和第一个主要区别是第一个直接下载。第二个会先从缓存中查找,找不到再进行第一步的操作
至此图片的的下载和缓存已经大概介绍完了
总结:
- 根据URL去加载图片
- 先从缓存查找 key为图片URL
- 磁盘查找, 找到后在存入缓存中key为图片URL经过md5处理
- 未找到下载图片,下载完成后存入缓存和磁盘。磁盘操作有一个队列串形处理
未完待续