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里面包含了两个属性,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源码阅读(二)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 序言 Kingfisher是喵神的一个异步下载和缓存图片的Swift库,类似于OC 的SDWebImage中文简介...
    乔克_叔叔阅读 6,093评论 7 25
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • 对谁都不要说,好吗?清晨庭院的角落里花儿悄悄落泪的事。万一这件事说出去传到蜜蜂的耳朵里,它会像做了亏心事一样飞回去...
    没头脑和不高阅读 590评论 0 0
  • 伏,伏牛路小学的伏,这个伏字,左边一个单人旁,右边一个犬字。非常形象地写出来了一个蓄势待发的狗在随时听侯主人的命令...
    带眼镜阅读 414评论 0 1
  • 。。
    彩虹1981阅读 180评论 0 0