Kingfisher源码阅读(一)
Kingfisher是喵神写的一个异步下载和缓存图片的Swift库,github上将近3k的Star,相信不需要我再安利了。它的中文简介在这里,github地址在这里。
我始终觉得编程的精髓是抽象和模块化。阅读别人的代码也应该先从大处着眼,从抽象层面最高的地方开始,自顶向下地逐模块阅读。我花了一个白天加两个晚上认真地读了一遍Kingfisher,加了一些中文注释,本系列比较详细地记录了阅读过程,所以可能会显得有点啰嗦。
Kingfisher的文档非常完备,我先大致看了一下,然后下载源码,跑了一下demo。demo中有这么一段:
cell.cellImageView.kf_setImageWithURL(URL, placeholderImage: nil,
optionsInfo: [.Transition(ImageTransition.Fade(1))],
progressBlock: { receivedSize, totalSize in
print("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
},
completionHandler: { image, error, cacheType, imageURL in
print("\(indexPath.row + 1): Finished")
}
)
这个kf_setImageWithURL
显然是UIImage
的一个extension
方法,既然是暴露出来供库的使用者调用的,应该就是抽象层面最高的。于是我command+click进去看了一下,它长这个样子:
public func kf_setImageWithURL(URL: NSURL,
placeholderImage: UIImage?,
optionsInfo: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask
{
return kf_setImageWithResource(Resource(downloadURL: URL),
placeholderImage: placeholderImage,
optionsInfo: optionsInfo,
progressBlock: progressBlock,
completionHandler: completionHandler)
}
主要就是把传过来的URL
包装成了一个Resource
,然后调用kf_setImageWithResource
方法。Resource
里面包含了两个属性,cacheKey
和downloadURL
,cacheKey
就是原URL
的完整字符串,之后会作为缓存的键使用(内存缓存直接使用cacheKey
作为NSCache
的键,文件缓存把cacheKey
进行MD5加密后的字符串作为缓存文件名)。下面再看看这个kf_setImageWithResource
方法,它是这个UIImageView+Kingfisher.swift
里的核心方法,其他还有一些提供给用户使用的kf_setImageWithXXX
的方法到最后都会调用它。kf_setImageWithResource
里有这一句:
let task = KingfisherManager.sharedManager.retrieveImageWithResource(...)
它使用了KingfisherManager
这个类,而这个类看名字就知道是整个库的一个管理调度类。KingfisherManager.sharedManager
,显然是取KingfisherManaget
的一个单例,Swift中的单例模式非常简单,因为有let
可以声明imutable的属性,不用担心线程安全问题,只要在 KingfisherManager.swift
里像这样写就行:
private let instance = KingfisherManager()
public class KingfisherManager {
public class var sharedManager: KingfisherManager {
return instance
}
...
}
KingfisherManager
的单例调用了retrieveImageWithResource
,它整合了下载和缓存两大功能,先看一下完整的方法签名:
public func retrieveImageWithResource(resource: Resource,
optionsInfo: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask
第一个参数类型Resource
之前已经说过了,第二个参数类型KingfisherOptionsInfo?
是什么呢?它是一个类型别名:public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]
,而KingfisherOptionsInfoItem
是一个enum
:
public enum KingfisherOptionsInfoItem {
case Options(KingfisherOptions)
case TargetCache(ImageCache)
case Downloader(ImageDownloader)
case Transition(ImageTransition)
}
这个枚举的每个枚举项都有关联值,包含了很多信息。KingfisherOptions
是一个自定义的Options
,就是一个遵守OptionSetType
协议的struct
,里面有一些选项,可以对下载和缓存时的一些行为进行配置。TargetCache
指定一个缓存器(ImageCache
的一个实例),Downloader
指定一个下载器(ImageDownloader
的一个实例),Transition
指定显示图片的动画效果(提供淡入和从上下左右进入这5种效果,也可以传入自定义效果)。
第三个参数类型是DownloadProgressBlock
,也是一个别名:
//下载进度(参数:接收尺寸, 总尺寸)
public typealias DownloadProgressBlock = ((receivedSize: Int64, totalSize: Int64) -> ())`
实际上是一个闭包类型,具体会在什么时候调用待会儿会看到。第四个参数类型CompletionHandler
也一样是个闭包类型的别名:
public typealias CompletionHandler = ((image: UIImage?, error: NSError?, cacheType: CacheType, imageURL: NSURL?) -> ())
这个看名字就知道会在操作结束之后调用。
返回类型是RetrieveImageTask
,它是长这样的:
public class RetrieveImageTask {
// If task is canceled before the download task started (which means the `downloadTask` is nil),
// the download task should not begin.
var cancelled: Bool = false
var diskRetrieveTask: RetrieveImageDiskTask?
var downloadTask: RetrieveImageDownloadTask?
/**
Cancel current task. If this task does not begin or already done, do nothing.
*/
public func cancel() {
// From Xcode 7 beta 6, the `dispatch_block_cancel` will crash at runtime.
// It fixed in Xcode 7.1.
// See https://github.com/onevcat/Kingfisher/issues/99 for more.
if let diskRetrieveTask = diskRetrieveTask {
dispatch_block_cancel(diskRetrieveTask)
}
if let downloadTask = downloadTask {
downloadTask.cancel()
}
cancelled = true
}
}
简单来说它就是一个接收图片的任务,它的内部有三个属性,cancelled
是个表明任务是否被取消的flag,diskRetrieveTask
和downloadTask
分别是“从磁盘获取缓存图片的任务”和“从网络下载图片的任务”,会分别在缓存模块和下载模块中用到,待会儿再细说。至于这个cancel()
方法么就是把上面说的两个任务都取消,然后把取消flag设置为true
。
看完了retrieveImageWithResource
的方法签名,现在来看一下完整的方法,这个方法我认为是整个KingfisherManager
的核心:
public func retrieveImageWithResource(resource: Resource,
optionsInfo: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask
{
//新建任务
let task = RetrieveImageTask()
// There is a bug in Swift compiler which prevents to write `let (options, targetCache) = parseOptionsInfo(optionsInfo)`
// It will cause a compiler error.
//解析optionsInfo
let parsedOptions = parseOptionsInfo(optionsInfo)
let (options, targetCache, downloader) = (parsedOptions.0, parsedOptions.1, parsedOptions.2)
//若强制刷新则联网下载并缓存
if options.forceRefresh {
downloadAndCacheImageWithURL(resource.downloadURL,
forKey: resource.cacheKey,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: completionHandler,
options: options,
targetCache: targetCache,
downloader: downloader)
} else {
//不强制刷新则从缓存中取
let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
// Break retain cycle created inside diskTask closure below
//完成之后取消任务引用,避免循环引用,释放内存
task.diskRetrieveTask = nil
completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
}
let diskTask = targetCache.retrieveImageForKey(resource.cacheKey, options: options,
completionHandler: { image, cacheType in
if image != nil {
diskTaskCompletionHandler(image: image, error: nil, cacheType:cacheType, imageURL: resource.downloadURL)
} else {
//没有缓存则联网下载并缓存
self.downloadAndCacheImageWithURL(resource.downloadURL,
forKey: resource.cacheKey,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: diskTaskCompletionHandler,
options: options,
targetCache: targetCache,
downloader: downloader)
}
}
)
task.diskRetrieveTask = diskTask
}
return task
}
几个重要的点我加了中文注释,应该很好理解。现在先来看一下parseOptionsInfo
这个方法,它是用来解析optionsInfo
的:
func parseOptionsInfo(optionsInfo: KingfisherOptionsInfo?) -> (Options, ImageCache, ImageDownloader) {
//3个默认值
var options = KingfisherManager.DefaultOptions
var targetCache = self.cache
var targetDownloader = self.downloader
//用户没有指定的话则使用默认下载器、默认缓存器和默认配置。
guard let optionsInfo = optionsInfo else {
return (options, targetCache, targetDownloader)
}
//匹配各个枚举类型,进行分别处理。扩展方法kf-findFirstMatch和重载运算符“==”配合,写得很优雅(把"=="换成自定义其他操作符就更好了,"=="有点不符合直觉)。
if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)), case .Options(let optionsInOptionsInfo) = optionsItem {
//如果选项包含后台回调,则使用一个新线程,否则使用默认queue(主线程)
let queue = optionsInOptionsInfo.contains(KingfisherOptions.BackgroundCallback) ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) : KingfisherManager.DefaultOptions.queue
//默认比例是1
let scale = optionsInOptionsInfo.contains(KingfisherOptions.ScreenScale) ? UIScreen.mainScreen().scale : KingfisherManager.DefaultOptions.scale
//打包options
options = (forceRefresh: optionsInOptionsInfo.contains(KingfisherOptions.ForceRefresh),
lowPriority: optionsInOptionsInfo.contains(KingfisherOptions.LowPriority),
cacheMemoryOnly: optionsInOptionsInfo.contains(KingfisherOptions.CacheMemoryOnly),
shouldDecode: optionsInOptionsInfo.contains(KingfisherOptions.BackgroundDecode),
queue: queue, scale: scale)
}
if let optionsItem = optionsInfo.kf_findFirstMatch(.TargetCache(self.cache)), case .TargetCache(let cache) = optionsItem {
targetCache = cache
}
if let optionsItem = optionsInfo.kf_findFirstMatch(.Downloader(self.downloader)), case .Downloader(let downloader) = optionsItem {
targetDownloader = downloader
}
return (options, targetCache, targetDownloader)
}
其中:
if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)), case .Options(let optionsInOptionsInfo) = optionsItem
这个写法让我一时没反应过来,愣了好一会儿,后来想起来在WWDC视频上看到过Swfit2关于模式匹配的一些新内容,喵神的写法应该是跟下面这个写法等效的,只是喵神的更加简洁优雅:
if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)) {
switch optionsItem {
case .Options(let optionsInOptionsInfo):
let queue = ...
...
}
}
我把源代码注释掉,改成上面这种形式跑了一下,发现没有问题。
然后kf_findFirstMatch(.Options(.None)
这个方法又让我纠结了一阵,它是对CollectionType
的一个扩展(给协议加扩展方法也是Swift2新特性),长这样的:
extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {
func kf_findFirstMatch(target: Generator.Element) -> Generator.Element? {
//取得target的索引
let index = indexOf {
e in
//这个"==",上面已经重载过了,只要类型相等就返回true,所以如果target是.Options(.None),e只要是.Options(_)都可以匹配,返回.Options(_)的索引
return e == target
}
return (index != nil) ? self[index!] : nil
}
}
现在我加了注释大家应该看得明白了,这个函数会返回跟target
同类型的元素的索引。之前我想当然地认为这个函数应该返回跟target
相等元素的索引,比如kf_findFirstMatch(.Options(.None)
,应该要返回匹配到的.Options(.None)
的索引,然而实际上,只要匹配到任意一个.Options(_)
,就可以返回它的索引了。因为==
被这样重载了:
func == (a: KingfisherOptionsInfoItem, b: KingfisherOptionsInfoItem) -> Bool {
switch (a, b) {
case (.Options(_), .Options(_)): return true
case (.TargetCache(_), .TargetCache(_)): return true
case (.Downloader(_), .Downloader(_)): return true
case (.Transition(_), .Transition(_)): return true
default: return false
}
}
怎么说呢,总觉得不太符合直觉,索性自定义一个新的运算符可能更合适些,不容易造成误解。
好了,接着往下看retrieveImageWithResource
这个方法。取得了options
、targetCache
和downloader
之后,就要判断用户是否指定强制刷新,如果是则直接联网下载,否则先从缓存中取数据,若没有缓存再联网下载。这一段我个人认为也稍微有点不符合直觉(我真不是处女座),喵神把“联网下载”那一段逻辑单独封装成一个方法,因为就算不需要强制刷新,但缓存中若没有数据的话,在“从缓存中取数据”这个任务的结束闭包中也还要进行下载操作,所以显然可以把“联网下载”的逻辑提取出来进行复用。这样子的话,“联网下载”被提取成一个方法,方法名清晰易懂,但“提取缓存”却还有那么一大段在那儿,显得不太对称。要是把提取缓存
也封装成一个方法,然后在retrieveImageWithResource
里调用,可能可读性更好一些:
if options.forceRefresh {
//若用户指定强制刷新则直接联网下载并缓存
downloadAndCacheImageWithURL(resource.downloadURL,
forKey: resource.cacheKey,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: completionHandler,
options: options,
targetCache: targetCache,
downloader: downloader)
} else {
//不强制刷新则尝试从缓存中取,若无缓存则联网下载并缓存
tryToRetrieveImageFromCacheForKey(resource.cacheKey,
withURL: resource.downloadURL,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: completionHandler,
options: options,
targetCache: targetCache,
downloader: downloader)
}
相应地,tryToRetrieveImageFromCacheForKey
长这样:
func tryToRetrieveImageFromCacheForKey(key: String,
withURL URL: NSURL,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: Options,
targetCache: ImageCache,
downloader: ImageDownloader)
{
let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
// Break retain cycle created inside diskTask closure below
//完成之后取消任务引用,避免循环引用,释放内存
retrieveImageTask.diskRetrieveTask = nil
completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
}
let diskTask = targetCache.retrieveImageForKey(key, options: options,
completionHandler: { image, cacheType in
if image != nil {
diskTaskCompletionHandler(image: image, error: nil, cacheType:cacheType, imageURL: URL)
} else {
//没有缓存则联网下载并缓存
self.downloadAndCacheImageWithURL(URL,
forKey: key,
retrieveImageTask: retrieveImageTask,
progressBlock: progressBlock,
completionHandler: diskTaskCompletionHandler,
options: options,
targetCache: targetCache,
downloader: downloader)
}
}
)
retrieveImageTask.diskRetrieveTask = diskTask
}
到这里为止,我们对Kingfisher对整体架构已经有比较清晰的认识了,大概是这个样子:
Kingfisher.png喵神是我第一个知道的iOS领域的大牛,我是从后端转iOS的嘛,之前看完苹果官方的《The Swift Programming Language》之后,就入手了喵神的《Swifter》,看完受益匪浅。最近想找点优秀的源码读一读,第一时间就想到了Kingfisher。其实之前我并没有用过这个库(因为要兼容iOS7),在项目中只是自己简单封装了一下异步下载和缓存的过程,而且我只做了内存缓存,虽然勉强够用了,但看了Kingfisher之后实在是觉得自己写得非常简陋。读完了之后忍不住想记录下来,先小结一下读了上面这部分的收获吧:
- 在系统设计方面有了一点心得
- 对软件项目的规范也有了直接的体会(我身边没有人给我这方面的指点,一直都是看书跟自己摸索)
- Swift中关于
enum
和模式匹配的优雅用法让我印象深刻
接下来我会继续写一下阅读下载模块和缓存模块的过程,下载模块中用到了很多GCD的新特性,缓存模块主要是文件操作和对不同格式图片的解码操作等等,都非常值得学习。
下一篇地址:Kingfisher源码阅读(二)