首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kingfisher源码阅读(一)

Kingfisher源码阅读(一)

作者头像
Sheepy
发布2018-09-10 12:17:27
1.5K0
发布2018-09-10 12:17:27
举报

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源码阅读(二)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015.12.02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档