Kingfisher源码阅读(一)


 原文链接     by: Sheepy

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里面包含了两个属性,cacheKeydownloadURL,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,diskRetrieveTaskdownloadTask分别是“从磁盘获取缓存图片的任务”和“从网络下载图片的任务”,会分别在缓存模块和下载模块中用到,待会儿再细说。至于这个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这个方法。取得了optionstargetCachedownloader之后,就要判断用户是否指定强制刷新,如果是则直接联网下载,否则先从缓存中取数据,若没有缓存再联网下载。这一段我个人认为也稍微有点不符合直觉(我真不是处女座),喵神把“联网下载”那一段逻辑单独封装成一个方法,因为就算不需要强制刷新,但缓存中若没有数据的话,在“从缓存中取数据”这个任务的结束闭包中也还要进行下载操作,所以显然可以把“联网下载”的逻辑提取出来进行复用。这样子的话,“联网下载”被提取成一个方法,方法名清晰易懂,但“提取缓存”却还有那么一大段在那儿,显得不太对称。要是把提取缓存也封装成一个方法,然后在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源码阅读(二)

相关代码:

Kingfisher