模块介绍
通用模块
ImageSource
Kingfisher中对图片资源的表示有两种,定义在Source枚举中。
1 | public enum Source { |
network
类型的需要提供一个url和cacheKey,然后kingfisher会自行下载。provider类型的,是自己提供image数据。下面看下Resource
1 | public protocol Resource { |
Resource的协议需要两个属性,下载地址和缓存key。正常情况下我们无需实现这个Resource,因为Kingfisher中的URL通过extension实现了这个协议,我们平常直接用URL就可以了。
1 | extension URL: Resource { |
但是,如果你想定制key的格式,可以使用Resource的另一个实现。
1 | public struct ImageResource: Resource { |
ImageDataProvider 提供了三个实现,分别是LocalFileImageDataProvider
,Base64ImageDataProvider
,RawImageDataProvider
。三种方式提供image的data,不过这个没什么使用场景,源码中,也只在测试的地方有用到。
KingfisherError
1 | public enum KingfisherError: Error { |
Kingfisher 将 error 按照过程划分,分别是请求,返回,缓存,处理,设置,每个过程一种 error 类型。
###下载
当缓存没有命中的时候,就会启动一个下载。下载主要用到两个类,ImageDownloader
启动下载,并处理回调,SessionDelegate处理URLSessionDataTask的下载代理,并提供任务管理功能。图片下载过程中,会通过ImageDownloaderDelegate
通知外部。
此外可以通过ImageDownloadRequestModifier拦截修改图片的url。
图片处理
Kingfisher提供丰富的图片处理功能。这些功能通过ImageProcessor
来提供,如果有需要其他的处理,可以自行实现这个协议来提供,比如webp格式的图片处理。
Kingfisher在demo中列出了目前已经提供了的。
1 | (DefaultImageProcessor.default, "Default"), |
这些功能主要通过 ImageDrawing
中的方法来实现。
####圆角
1 | public func image(withRoundRadius radius: CGFloat, |
draw
方法负责开启上下文和关闭上下文,block中是绘制的主要代码。
模糊
1 | public func blurred(withRadius radius: CGFloat) -> Image { |
模糊是通过Accelerate.framework中的vImage实现,使用vImage来做比较自由,但是比较麻烦,需要了解算法细节。
其他的诸如Size,blend都比较简单。
缓存
缓存策略
Kingfisher有两个关于缓存策略的枚举,StorageExpiration
用来表示文件创建成功后的缓存策略。
1 | public enum StorageExpiration { |
ExpirationExtending
用来表示再次访问后,如何更新该文件的缓存策略。
1 | public enum ExpirationExtending { |
内存缓存
kingfisher内存缓存MemoryStorage
定义了一个命名空间,里面有三个对象,Config
,StorageObject
和实际用来做处理的Backend
。
StorageObject
包括数据value,过期策略,和缓存的key值。
1 | class StorageObject<T> { |
缓存配置对象
1 | extension MemoryStorage { |
实际做内存缓存处理的类Backend
,以NSCache
为存储源,将需要存储的数据构造成StorageObject
来存储。提供存储,移除,查询三个功能,在查到缓存的时候,会根据延长缓存策略,更新这个对象的缓存策略。
1 | func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? { |
此外,会创建一个Timer,每隔一段时间(config中配置的)清理一次NSCache中过期的对象。
对NSCache的操作,都需要加锁,这里使用的NSLock。
磁盘缓存
磁盘缓存的结构跟内存缓存类似,使用DiskStorage枚举构造一个命名空间。内部定义了三个结构,配置信息Config
,代表磁盘文件的FileMeta
,以及处理逻辑的Backend
。
1 | public struct Config { |
1 | struct FileMeta { |
磁盘缓存的数据源是磁盘,在查询,删除,存储上,比内存缓存要麻烦一点。
Backend<T: DataTransformable>
这里T是内部使用的数据类型。DataTransformable
协议定义了T与Data的变换。需要存储时,要从T类型中拿到Data。下面看下store方法。
1 | func store( |
存储文件带的属性,将创建时间和过期时间放进去。方便后续的处理。
1 | func value(forKey key: String, referenceDate: Date, actuallyLoad: Bool) throws -> T? { |
磁盘缓存没有定时清理。App将要进入后台,被杀死和被挂起的时候,会做一次清理。
文件操作
文件写入
1 | let now = Date() |
缓存的文件写入是通过FileMananger
的open func createFile(atPath path: String, contents data: Data?, attributes attr: [FileAttributeKey : Any]? = nil) -> Bool
方法写入,在创建时间和修改时间属性中填入当前时间和过期时间。后续可以根据过期时间来清理磁盘。
文件属性的操作
读取文件属性可以通过url
的resourceValues
方法,如下
1 | let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey] |
文件的属性很多,需要了解更多的,可以查看URLResourceKey
这个结构体。库中主要用到了
1 | //修改时间,实际存储的是过期时间 |
文件属性的修改则是通过FileManager
来处理
1 | let attributes: [FileAttributeKey : Any] = [ |
文件数据的读入,比较简单
1 | let data = try Data(contentsOf: fileURL) |
ImageCache
ImageCache
包装了磁盘缓存和内存缓存,对外提供简单的查询,存储,清除等功能。
1 | store(_ image: Image, |
流程介绍
Kingfisher最常用的方式
1 | let url = URL(string: "https://example.com/image.png") |
入口即setImage
,从这个方法进去看看
1 |
|
流程大概是:
- UIImageView调用setImage设置图片
- 通过KingfisherManager.shared.retrieveImage获取图片
- KingfisherManager.shared.retrieveImage去查内存缓存,有就返回,没有继续
- KingfisherManager.shared.retrieveImage去查磁盘缓存,有就返回,没有继续
- KingfisherManager.shared.loadAndCacheImage下载并缓存图片
- 查看图片中当前的任务和刚执行完的任务是不是同一个,同一个就设置图片,结束
- 不是同一个,就不处理,结束
UIButton的setImage是类似的逻辑,就不多说了
这里需要特别说明的是在tableView中的处理,因为tableView中,快速上下滑动,cell重用。会导致一个UIImageView多次setImage。上面流程中的回设图片中会对比当前task和完成的task,如果不一致,说明这个是之前的某次setImage开启的下载完成了。这是一个对重用的处理细节。
另外当重新滑到之前的位置,setImage的url是开启的,并且没有下载完成,缓存中找不到。这个时候也不会立刻开始下载,而是去看下载任务队列中是否有该url的任务,找到之后,将这个任务绑定到这个UIImageView上。这也是重用的一个场景。
多线程处理
库中会频繁用到网络请求和磁盘读写,为了不阻塞主线程,很显然需要用到多线程处理。使用URLSession,数据的返回默认会从异步执行。而磁盘的读写则需要我们自己处理,库中创建了一个处理IO
的串行队列来处理。
因为这些异步处理,需要一些手段保证数据的一致性。下面列举一下临界区。
KingfisherManager
是一个单例,所有UIImageView
,UIButton
的setImage
操作都是通过它来完成。KingfisherManager
中有一个ImageCache
和一个downloader
。cache
中的内存缓存是一个NSCache
对象,全局的存取到会用到。
downloader
中下载会返回一个task,这些task会在sessionDelegate中管理。private var tasks: [URL: SessionDataTask] = [:]
SessionDataTask
中保存了该任务的回调信息
多线程会导致一些同步问题,从网络下载完图片,会在未知线程缓存,接下来缓存时,有可能遇到其他任务正在读取缓存的情况。所以内存缓存的写入时加锁处理的。
库中多用队列来处理任务。流程中可能出现的多线程问题:
- 同个文件的读写
请求的起点一般是从UIImageView.setImage
开始的,这个方法的调用一般是在主线程中发起。这个方法拿到数据后,completionHandler
是通过CallbackQueue.mainCurrentOrAsync
在主线程中执行。
在获取数据,会先查内存缓存,在查磁盘缓存。内存缓存的查询比较快,会在当前线程知节执行。磁盘缓存的查询,会使用一个单独的串行队列完成。
补充
Delegate<Input, Output>
Kingfisher 中使用Delegate类来包装block,添加[weak self]
来避免 retain cycle。
1 | class Delegate<Input, Output> { |
delegate方法,将传入的block包装成自己的block,中间加入weak处理。call方法则是调用这个block。
defer的使用
1 | func addCallback(_ callback: TaskCallback) -> CancelToken { |
第二个defer
调用之后,返回的currentToken,值是+1之前的。所以deffer的调用时机,其实是返回之后。
从语言设计上来说,
defer
的目的就是进行资源清理和避免重复的返回前需要执行的代码,而不是用来以取巧地实现某些功能。这样做只会让代码可读性降低。
上边这段话是作者在关于 Swift defer 的正确使用中说的话,然后还是在kingfisher中用了。一个小趣点。
另外这篇文章中还提到了作用域的问题,defer这个词,并不是在方法返回后执行的。而是所在的作用域结束后返回的。比如你在if语句中写defer,那这个defer就是在if之后执行。不是在方法返回之后。文中有具体例子,可以细看。
这里使用了多个defer,之前没见过,查了下,多个defer的调用会以反序执行,类似入栈,出栈,网上有个很不错的例子,这里看一下。
1 | guard let database = openDatabase(...) else { return } |
打开一个数据库,打开一个连接,结束之后,defer反序执行,先关掉连接,在关数据库。很恰当。
GCD的使用
库中对GCD的使用,是包装了一个CallbackQueue
。
1 | public enum CallbackQueue { |
从execute的实现可以了解这几个枚举的含义。有一个是没出现过的,就是safeAsync。
1 | extension DispatchQueue { |
这个safeAsync里会先判断当前是不是主队列和主线程。如果是主线程,主队列,就直接执行block,不然就async执行该block。这个方法只在上面的mainCurrentOrAsync
由主队列调用,感觉这样写不是特别好,或者名字不是特别恰当。因为这个方法是加载DispatchQueue上的,所以其他队列也是可以调用的,但是非主队列调用,都是有问题的。这个方法的声明的范围太广了。