iOS - 【Swift库】纯 Swift 图片缓存库Kingf
Swift 图片缓存库Kingfisher架构详解及使用
Kingfisher
是喵神的一个纯Swift语言的异步下载和缓存图片的Swift库,类似于OC 的SDWebImage
,github地址 最近一直在学习 Swift
,对一些优秀的Swift
第三方库也在逐步的了解应用。
来源:参考大神的文章(Kingfisher 3.x 学习(一))
一、Kingfisher的架构
阅读他人优秀代码是一个提高自身代码水平很好的方法。花了几天的时间,看了Kingfisher
的源代码,里面包含的很多知识点,让我受益匪浅。3.x版本相比与之前的版本,一个重要的改变就是protocol
的灵活运用,更加面向协议编程。当然还有其他很多知识,比如多线程,枚举,闭包,Extension 等等应用。 Kingfisher中
有Core
,Extension
,Helpers
三个目录结构 共20个文件
Core
image.swift
文件内部对 UIImage 以及 NSData 进行了拓展, 包含判定图片类型、图片解码以及Gif数据处理等操作
Indicator.swift
图片加载时loading指示
ImageCache.swift
主要负责将加载过的图片缓存至本地。
ImageDownloader.swift
负责下载网络图片。
ImagePrefetcher.swift
可用于提前指定一些图片下载
ImageProcessor.swift
可用于将下载的数据合成图片对象
CacheSerializer.swift
可用于图像对象序列化成图像数据存储到磁盘缓存和从磁盘缓存将图片数据反序列化成图像对象。
RequestModifier.swift
下载图像请求修改器。
ImageTransition.swift
过渡动画效果 使用UIViewAnimationOptions
动画效果
KingfisherManager.swift
Kingfisher 管理控制类,拥有图片下载及缓存功能
KingfisherOptionsInfo.swift
枚举KingfisherOptionsInfoItem 配置 Kingfisher 行为的参数,包括 是否自定义缓存对象 是否自定义下载器 是否过渡动画 是否设置下载低优先级 是否强制刷新 是否仅获取缓存图片 是否仅缓存至内存、是否允许图像后台解码等设置。
Filter.swift
图像过滤器
Resource.swift
记录了图片的下载地址和缓存Key。
Kingfisher.swift
添加KingfisherCompatible通用协议 kf新属性
Extension
ImageView+Kingfisher.swift
UIButton+Kingfisher.swift
NSButton+Kingfisher
对 UIImageView
UIButton
NSButton
进行了拓展 主要用于提供 Kingfisher 的外部接口。
Helpers
String+MD5.swift
负责图片缓存时对文件名进行MD5加密操作。
Box.swift
一个简单泛型类
ThreadHelper.swift
中的 dispatch_async_safely_main_queue
函数接受一个闭包 利用 NSThread.isMainThread 判断并将其放置在主线程中执行
二、Kingfisher.swift
主要文件ImageView+Kingfisher
,KingfisherManager
,ImageCache
,ImageDownloader
,废话不多说直接代码学习
运行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(.fade(1))],
progressBlock: { receivedSize, totalSize in
print("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
},
completionHandler: { image, error, cacheType, imageURL in
print("\(indexPath.row + 1): Finished")
})
首先调用的UIImageView
的kf
属性 之前是调用UIImageView
的Extension
中的kf_setImage
,现已弃用 那kf
属性是如何实现的?
下面是Kingfisher.swift
源码
- 自定义了不同平台下的一些类型别名 swift中的typealias 相当于OC中的typedef
#if os(macOS)
import AppKit
public typealias Image = NSImage
public typealias Color = NSColor
public typealias ImageView = NSImageView
typealias Button = NSButton
#else
import UIKit
public typealias Image = UIImage
public typealias Color = UIColor
#if !os(watchOS)
public typealias ImageView = UIImageView
typealias Button = UIButton
#endif
#endif
- 申明了泛型类Kingfisher 实现了一个简单构造器,其中上面的cellImageView就是base属性
public final class Kingfisher<Base> {
public let base: Base
public init(_ base: Base) {
self.base = base
}
}
- 申明KingfisherCompatible协议 有一个可读属性kf 其类型是关联类型
/**
A type that has Kingfisher extensions.
*/
public protocol KingfisherCompatible {
associatedtype CompatibleType
var kf: CompatibleType { get }
}
- KingfisherCompatible协议的实现 属性kf关联Kingfisher类型 返回一个Kingfisher实例 base 参数就是传入的self
public extension KingfisherCompatible {
public var kf: Kingfisher<Self> {
get { return Kingfisher(self) }
}
}
- Image ImageView Button 遵守 KingfisherCompatible 协议 所以上边的self参数就是遵守了协议的类型 因此base属性即cellImageView
extension Image: KingfisherCompatible { }
#if !os(watchOS)
extension ImageView: KingfisherCompatible {}
extension Button: KingfisherCompatible { }
#endif
三、ImageView+Kingfisher
现在来说说setImage
这个方法的实现 这个方法是在Kingfisher
的Extension 中实现 并且要求Base
属于UIImageView
类型 即where Base: ImageView
由于kf
属性关联了Kingfisher
所以可以调用(cell as! CollectionViewCell).cellImageView.kf.setImage
Extensions目录下的三个文件都是类似实现的 这里就以ImageView+Kingfisher.swift为例
下面方法是外部使用Kingfisher最频繁也是最重要的方法
第一个参数Resource
是一个URL遵守的Protocol,一般传入图片的URL,不可为空
第二个参数placeholder
是一个默认的占位图,可为空
第三个参数KingfisherOptionsInfo
是个枚举数组,配置Kingfisher下载图片的一些操作行为
第四个参数DownloadProgressBlock
是个下载进度闭包,可以用于更新下载UI
第五个参数completionHandler
是个下载完成闭包,闭包参数包含图片,错误,缓存类型,URL 信息
extension Kingfisher where Base: ImageView {
/**
Set an image with a resource, a placeholder image, options, progress handler and completion handler.
- parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`.
- parameter placeholder: A placeholder image when retrieving the image at URL.
- parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
- parameter progressBlock: Called when the image downloading progress gets updated.
- parameter completionHandler: Called when the image retrieved and set.
- returns: A task represents the retrieving process.
- note: Both the `progressBlock` and `completionHandler` will be invoked in main thread.
The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method.
*/
@discardableResult 忽略返回值警告
public func setImage(with resource: Resource?,
placeholder: Image? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: DownloadProgressBlock? = nil,
completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
{
当传入的resource为空时 使用guard语句提前退出 Resource是一个协议 URL遵守此协议 Resource有两个属性 cacheKey和downloadURL
guard let resource = resource else {
base.image = placeholder
completionHandler?(nil, nil, .none, nil)
return .empty
}
图片加载过程中是否显示placeholder
var options = options ?? KingfisherEmptyOptionsInfo
if !options.keepCurrentImageWhileLoading {
base.image = placeholder
}
如果indicator存在,开启转圈动画 indicator 通过属性关联存取
let maybeIndicator = indicator
maybeIndicator?.startAnimatingView()
关联属性绑定下载的URL
setWebURL(resource.downloadURL)
默认开启加载所有GIF图片数据,显示GIF 动态图片
if base.shouldPreloadAllGIF() {
options.append(.preloadAllGIFData)
}
调用KingfisherManager的方法来获取图片
let task = KingfisherManager.shared.retrieveImage(
with: resource,
options: options,
progressBlock: { receivedSize, totalSize in
下载进度回调
if let progressBlock = progressBlock {
progressBlock(receivedSize, totalSize)
}
},
completionHandler: {[weak base] image, error, cacheType, imageURL in
确保线程安全
DispatchQueue.main.safeAsync {
确保返回的图片与URL对应一致
guard let strongBase = base, imageURL == self.webURL else {
return
}
self.setImageTask(nil)
没有图片返回停止动画返回错误
guard let image = image else {
maybeIndicator?.stopAnimatingView()
completionHandler?(nil, error, cacheType, imageURL)
return
}
是否需要过渡动画 transitionItem 为 options中第一个.transition
需要过渡动画需要满足以下情况
1.transitionItem存在且不为.transition(.none)
2.options.forceTransition存在 或者 cacheType == .none
guard let transitionItem = options.firstMatchIgnoringAssociatedValue(.transition(.none)),
case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
{
maybeIndicator?.stopAnimatingView()
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
UIView.transition(with: strongBase, duration: transition.duration,
options: [transition.animationOptions, .allowUserInteraction],
animations: {
// Set image property in the animation.
设置图片,如果是自定义动画 在定义动画回调中设置图片,代码在ImageTransition.swift
transition.animations?(strongBase, image)
},
completion: { finished in
动画结束回调
transition.completion?(finished)
completionHandler?(image, error, cacheType, imageURL)
})
})
#endif
}
})
setImageTask(task)
return task
}
/**
Cancel the image download task bounded to the image view if it is running.
Nothing will happen if the downloading has already finished.
*/
取消下载
public func cancelDownloadTask() {
imageTask?.downloadTask?.cancel()
}
}
ImageView+Kingfisher
中的WebUR
indicatorType
indicator
imageTask
属性均使用属性关联技术实现数据的存取
四 、KingfisherManager
该类是Kingfisher
唯一的一个管理调度类。这个类有下载和缓存两大功能模块 主要包含了两个属性 两个方法
public var cache: ImageCache
图片缓存属性
public var downloader: ImageDownloader
图片下载属性
func downloadAndCacheImage
下载并且缓存图片方法
func tryToRetrieveImageFromCache
获取缓存图片
在ImageView+Kingfisher
中最后图片的获取就是由KingfisherManager
的单例实现的retrieveImage
- 外部调用获取图片方法
func retrieveImage(with resource: Resource,
options: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask{
let task = RetrieveImageTask()
if let options = options, 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
}
- 下载并且缓存图片的方法
func downloadAndCacheImage(with url: URL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo?) -> RetrieveImageDownloadTask?
{
获取下载器 并开启下载
let options = options ?? KingfisherEmptyOptionsInfo
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
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.)
如果有错误并且没有修改过URL 返回缓存图片
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)
}
completionHandler?(image, error, .none, url)
})
}
- 优先从缓存获取图片,如缓存中没有,在从网络获取图片
func tryToRetrieveImageFromCache(forKey key: String,
with url: URL,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: KingfisherOptionsInfo?)
{
打破下面diskTask内部闭包保持的循环引用,完成之后取消磁盘任务引用,避免循环引用,释放内存
let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
// Break retain cycle created inside diskTask closure below
retrieveImageTask.diskRetrieveTask = nil
completionHandler?(image, error, cacheType, imageURL)
}
let targetCache = options?.targetCache ?? cache
let diskTask = targetCache.retrieveImage(forKey: key, options: options,
completionHandler: { image, cacheType in
if image != nil {
成功返回图片
diskTaskCompletionHandler(image, nil, cacheType, url)
} else if let options = options, options.onlyFromCache {
返回失败 并且设置只从缓存获取图片 返回没有缓存错误
let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
diskTaskCompletionHandler(nil, error, .none, url)
} else {
返回失败 再从网络下载图片
self.downloadAndCacheImage(
with: url,
forKey: key,
retrieveImageTask: retrieveImageTask,
progressBlock: progressBlock,
completionHandler: diskTaskCompletionHandler,
options: options)
}
}
)
retrieveImageTask.diskRetrieveTask = diskTask
}
五、 KingfisherOptionsInfo
上面代码多次用到options
这个参数,它的参数类型是KingfisherOptionsInfo
是一个类型别名
public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]
KingfisherOptionsInfoItem
是一个枚举 配置 Kingfisher所有功能行为 下面是详细中文注释
public enum KingfisherOptionsInfoItem {
这个成员的关联值是一个ImageCache对象。 Kingfisher使用指定的缓存对象处理 相关业务,包括试图检索缓存图像和存储下载的图片。
case targetCache(ImageCache)
这个成员的关联值应该是一个ImageDownloader对象。Kingfisher将使用这个下载器下载的图片。
case downloader(ImageDownloader)
如果从网络下载的图片 Kingfisher将使用“ImageTransition这个枚举动画。从内存或磁盘缓存时默认过渡不会发生。如果需要,设置ForceTransition
case transition(ImageTransition)
有关“浮动”值将被设置为图像下载任务的优先级。值在0.0 ~ 1.0之间。如果没有设置这个选项,默认值(“NSURLSessionTaskPriorityDefault”)将被使用。
case downloadPriority(Float)
如果设置,将忽略缓存,开启一个下载任务的资源
case forceRefresh
如果设置 即使缓存的图片也将开启过渡动画
case forceTransition
如果设置,Kingfisher只会在内存中缓存值而不是磁盘
case cacheMemoryOnly
如果设置 Kingfisher只会从缓存中加载图片
case onlyFromCache
在使用之前在后台线程解码图像
case backgroundDecode
当从缓存检索图像时 这个成员的关联值将被用作目标队列的调度时回调。如果没 有设置, Kingfisher将使用主要quese回调
case callbackDispatchQueue(DispatchQueue?)
将检索到的图片数据转换成一个图时 这个成员变量将被用作图片缩放因子。图像分辨率,而不是屏幕尺寸。你可能处理时需要指定正确的缩放因子@2x或@3x Retina图像。
case scaleFactor(CGFloat)
是否所有的GIF应该加载数据。默认false,只显示GIF中第一张图片。如果true,所有的GIF数据将被加载到内存中进行解码。这个选项主要是用于内部的兼容性。你不应该把直接设置它。“AnimatedImageView”不会预加载所有数据,而一个正常的图像视图(“UIImageView”或“NSImageView”)将加载所有数据。选择使用相应的图像视图类型而不是设置这个选项。
case preloadAllGIFData
发送请求之前用于改变请求。这是最后的机会你可以修改请求。您可以修改请求一些定制的目的,如添加身份验证令牌头,进行基本的HTTP身份验证或类似的url映射。原始请求默认情况下将没有任何修改
case requestModifier(ImageDownloadRequestModifier)
下载完成时,处理器会将下载的数据转换为一个图像。如果缓存连接到下载器(当你正在使用KingfisherManager或图像扩展方法),转换后的图像也将被缓存
case processor(ImageProcessor)
提供一个CacheSerializer 可用于图像对象序列化成图像数据存储到磁盘缓存和从磁盘缓存将图片数据反序列化成图像对象
case cacheSerializer(CacheSerializer)
保持现有的图像同时设置另一个图像图像视图。通过设置这个选项,imageview的placeholder参数将被忽略和当前图像保持同时加载新图片
case keepCurrentImageWhileLoading
}
下面是自定义<== 运算符 比较两个KingfisherOptionsInfoItem 是否相等 相等返回true 否则返回false
precedencegroup ItemComparisonPrecedence {
associativity: none
higherThan: LogicalConjunctionPrecedence
}
infix operator <== : ItemComparisonPrecedence
// This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values.
func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool {
switch (lhs, rhs) {
case (.targetCache(_), .targetCache(_)): return true
case (.downloader(_), .downloader(_)): return true
case (.transition(_), .transition(_)): return true
case (.downloadPriority(_), .downloadPriority(_)): return true
case (.forceRefresh, .forceRefresh): return true
case (.forceTransition, .forceTransition): return true
case (.cacheMemoryOnly, .cacheMemoryOnly): return true
case (.onlyFromCache, .onlyFromCache): return true
case (.backgroundDecode, .backgroundDecode): return true
case (.callbackDispatchQueue(_), .callbackDispatchQueue(_)): return true
case (.scaleFactor(_), .scaleFactor(_)): return true
case (.preloadAllGIFData, .preloadAllGIFData): return true
case (.requestModifier(_), .requestModifier(_)): return true
case (.processor(_), .processor(_)): return true
case (.cacheSerializer(_), .cacheSerializer(_)): return true
case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true
default: return false
}
}
下面是对CollectionType
的一个扩展 返回匹配的第一个相同枚举值 上面过渡动画就有用到
public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
func firstMatchIgnoringAssociatedValue(_ target: Iterator.Element) -> Iterator.Element? {
return index { $0 <== target }.flatMap { self[$0] }
}
func removeAllMatchesIgnoringAssociatedValue(_ target: Iterator.Element) -> [Iterator.Element] {
return self.filter { !($0 <== target) }
}
}
在KingfisherOptionsInfo
中有很多的类似的属性get方法 如下是关于图片编码的,默认返回DefaultCacheSerializer.default
。如果要自定义图片编码,可以添加自定义CacheSerializer
到Options
数组
public var cacheSerializer: CacheSerializer {
if let item = firstMatchIgnoringAssociatedValue(.cacheSerializer(DefaultCacheSerializer.default)),
case .cacheSerializer(let cacheSerializer) = item
{
return cacheSerializer
}
return DefaultCacheSerializer.default
}
结束
至此 ,我们对Kingfisher对整体架构已经有比较清晰的认识了 如下图所示
image作者:乔克_叔叔
链接:https://www.jianshu.com/p/a47fefeed7f0
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。