源码阅读 Kingfisher

Kingfisher是一个用于图片下载和缓存的轻量级、纯swift库。通过喵神的介绍,可以得知Kingfisher有以下特点:

  • 实现了图片的异步下载和缓存
  • 基于URLSession的网络,提供基本图像处理器和过滤器。
  • 内存和磁盘的多层缓存。
  • 可取消下载和处理任务以提高性能。
  • 独立的组件,根据需要单独使用下载器或缓存系统。
  • 预览图像并在以后需要时从缓存中显示它们。
  • UIImageView,NSImageUIButton的扩展,可以直接从URL设置图像。
  • 设置图像时内置过渡动画。
  • 加载图像时可自定义占位符。
  • 可扩展的图像处理和图像格式支持。

目录结构

在项目中,我们使用CocoaPods下载安装Kingfisher
我们查看Kingfisher的目录结构,如下

Kingfisher
  AnimatedImageView.swift    //动画控件   
  Box.swift    //工具类
  CacheSerializer.swift    //序列化类,读写文件时Data和Image互转
  Filter.swift    //仅对CIImage有效
  FormatIndicatedCacheSerializer.swift    //PNG/JPEG/GIF和Data互转
  Image.swift    //图片格式转换
  ImageCache.swift    //图片缓存
  ImageDownloader.swift    //图片下载
  ImageModifier.swift    //图片修改
  ImagePrefetcher.swift    //图片下载管理类,对并发多个下载任务的处理
  ImageProcessor.swift    //数据处理类,将Data转为Image
  ImageTransition.swift    //动画效果
  ImageView+Kingfisher.swift    //扩展ImageView添加下载图片的方法
  Indicator.swift    //动画相关
  Kingfisher.h    //版本号
  Kingfisher.swift    //类,扩展ImageView添加属性kf
  KingfisherManager.swift    //管理类,封装图片下载和缓存的逻辑
  KingfisherOptionsInfo.swift    //枚举类
  Placeholder.swift    //默认图片管理类
  RequestModifier.swift    //协议,修改原始URLRequest参数
  Resource.swift    //协议,声明下载链接和缓存key
  String+MD5.swift    //MD5加密
  ThreadHelper.swift    //工具类
  UIButton+Kingfisher.swift    //扩展UIButton添加下载图片的方法

调用方法

很简单的一句话:

self.imageV.kf.setImage(with: imgUrl)

如果想在图片加载的过程中添加默认图片,可以添加placeholder方法,监听加载的过程progressBlock,图片加载完成后的回调completionHandler

查看方法

查看kf.setImage方法,我们会跳到ImageView+Kingfisher.swift文件里。这里我们对方法做一个简单的介绍

@discardableResult
public func setImage(with resource: Resource?,
                     placeholder: Placeholder? = nil,
                     options: KingfisherOptionsInfo? = nil,
                     progressBlock: DownloadProgressBlock? = nil,
                     completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
{

}

我们可以看到,在这个方法里面,包括了ImageView的资源、默认图片、作者封装的枚举类、加载进度的回调以及完成结果。
@discardableResult方法是为了取消不使用返回值的警告。
在这个方法里面(方法太长就不列举出来,捡主要的说),

  • 首先判断参数合法性,当resourcenil时,展示默认图片。
  • 设置读取策略和启动动画。比如常见的一个问题:当用户头像改变但图片URL没有改变时,怎么去处理用户头像。一般有两种方法,一种是在缓存用户头像时保存当前时间。另一种就是设置读取策略,KingfisherOptionsInfo是一个枚举,设置它为forceRefresh时,可以强制刷新。
  • 从内存、文件或网络URL获取对应图片数据。
  • 获取图片完成后,在主线程刷新界面。

我们查看一下这里面的参数:

Resource

Resource是一个协议,我们查看源码可以看到:

public protocol Resource {
    var cacheKey: String { get }
    var downloadURL: URL { get }
}

cacheKey是图片保存的key值,当cacheKeynil时,取downloadURL.absoluteString(有兴趣的可以去了解一下absoluteStringpath的区别)。downloadURL不言而喻,就是图片的URL

Placeholder

public protocol Placeholder {
    func add(to imageView: ImageView)
    func remove(from imageView: ImageView)
}

Placeholder是一个协议,作者为它定义了addremove方法,任何。默认实现了Image,如果想用View充当Placeholder,只要让view遵守协议即可

extension Placeholder where Self: View{}

KingfisherOptionsInfo

KingfisherOptionsInfo是一个类型别名,点击查看

public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]

所以我们关注的应该是KingfisherOptionsInfoItem是什么东西?那么它是什么呢?

public enum KingfisherOptionsInfoItem {
    case targetCache(ImageCache)    //系统缓存位置。可以设置的属性
    case originalCache(ImageCache)    //系统缓存原始图像位置(只用于自己设置placeholder) 
    case downloader(ImageDownloader)    //获取更改session属性,设置请求
    case transition(ImageTransition)    //自定义动画
    case downloadPriority(Float)    //下载优先级(0-1)
    case forceRefresh    //每次请求忽略缓存,直接下载
    case fromMemoryCacheOrRefresh    //先取缓存再去文件,再去下载
    case forceTransition    //强制移动
    case cacheMemoryOnly     //只从缓存读取,不读取本机沙盒图片
    case onlyFromCache    //从缓存、沙盒读取,没有也不下载网络,显示placeholder
    case backgroundDecode    //设置后,显示前在后台线程解码
    case callbackDispatchQueue(DispatchQueue?)    //自定义回调队列,默认主线程
    case scaleFactor(CGFloat)    //自定义图片data -> Image缩放比例,不指定按屏幕2x\3x缩放
    case preloadAllAnimationData    //预先加载data成图片缓存
    case requestModifier(ImageDownloadRequestModifier)    //改变请求
    case processor(ImageProcessor)    //自定义Data转图片样式
    case cacheSerializer(CacheSerializer)    //自定义缓存Data 转图像样式
    case imageModifier(ImageModifier)    //修改图像
    case keepCurrentImageWhileLoading     //包含这个意味着placeHolder设置无效,没有直接用默认
    case onlyLoadFirstFrame    //如果返回结果是.gif图,只取第一帧显示
    case cacheOriginalImage    //同时缓存原始图片和下载后的图片
}

DownloadProgressBlock

public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> ())

DownloadProgressBlock里面有receivedSizetotalSize,可以根据这两个参数得知图片下载了多少和图片多大,也可以计算图片的下载进度。

CompletionHandler

public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> ())

CompletionHandler的回调里会有imageerrorcacheTypeimageURL四个参数。imageerrorimageURL不做介绍,直接就能看出什么内容。
主要看一下cacheType:

public enum CacheType {
    case none, memory, disk
    public var cached: Bool {
        switch self {
        case .memory, .disk: return true
        case .none: return false
        }
    }
}
  • none检索图片时,图片尚未缓存
  • memory图片缓存在内存中
  • disk图片缓存在磁盘中

检索图片

OK,让我们继续看这些代码。在setImage方法中,从内存、文件或网络URL获取对应图片数据是怎么实现的呢?这里,我们可以查看KingfisherManager.shared.retrieveImage方法。

@discardableResult
public func retrieveImage(with resource: Resource,
    options: KingfisherOptionsInfo?,
    progressBlock: DownloadProgressBlock?,
    completionHandler: CompletionHandler?) -> RetrieveImageTask
{
    let task = RetrieveImageTask()
    let options = currentDefaultOptions + (options ?? KingfisherEmptyOptionsInfo)
    if options.forceRefresh {
        _ = downloadAndCacheImage(
            with: resource.downloadURL,
            forKey: resource.cacheKey,
            retrieveImageTask: task,
            progressBlock: progressBlock,
            completionHandler: completionHandler,
            options: options)
    } else {
        tryToRetrieveImageFromCache(
            forKey: resource.cacheKey,
            with: resource.downloadURL,
            retrieveImageTask: task,
            progressBlock: progressBlock,
            completionHandler: completionHandler,
            options: options)
    }
    
    return task
}

可以看到,代码会通过KingfisherOptionsInfo进行判断是强制刷新,网络下载并执行缓存策略还是从内存或文件中获取对应的Image。

图片下载

@discardableResult
    func downloadAndCacheImage(with url: URL,
                             forKey key: String,
                      retrieveImageTask: RetrieveImageTask,
                          progressBlock: DownloadProgressBlock?,
                      completionHandler: CompletionHandler?,
                                options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
    {...}

downloadAndCacheImage方法中,会return downloader.downloadImage方法,下载的主要逻辑在这里实现,对应的文件是ImageDownloader.swift

@discardableResult
    open func downloadImage(with url: URL,
                       retrieveImageTask: RetrieveImageTask? = nil,
                       options: KingfisherOptionsInfo? = nil,
                       progressBlock: ImageDownloaderProgressBlock? = nil,
                       completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
    {...}

ImageDownloader.swift文件中,主要参数:

  • downloadTimeout 超时时间,默认15秒
  • trustedHosts 信任的请求地址,和自己实现请求代理设置冲突,二选一
  • sessionConfiguration session配置设置
  • requestsUsePipelining 请求是否管道类型,是否按顺序下载,默认false
  • sessionHandler单独设计出的一个ImageDownloaderSessionHandler,是为了解决之前出现的内存泄漏
  • delegate 下载代理
  • authenticationChallengeResponder 信任请求代理,和trustedHosts冲突二选一
  • fetchLoads 下载完成每个URL可能有多个处理方式,优先取这里的
  • 此外还有三个DispatchQueuebarrierQueueprocessQueuecancelQueue

下载完成后,在completionHandler回调中处理图片,如果下载失败:

if let error = error, error.code == KingfisherError.notModified.rawValue {
    //从缓存中读取,不需保存,直接返回
    targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
        completionHandler?(cacheImage, nil, cacheType, url)
    })
    return
}

下载成功:

if let image = image, let originalData = originalData {
    //存储图片
    targetCache.store(image,
                      original: originalData,
                      forKey: key,
                      processorIdentifier:options.processor.identifier,
                      cacheSerializer: options.cacheSerializer,
                      toDisk: !options.cacheMemoryOnly,
                      completionHandler: nil)
    if options.cacheOriginalImage && options.processor != DefaultImageProcessor.default {
        let originalCache = options.originalCache
        let defaultProcessor = DefaultImageProcessor.default
        if let originalImage = defaultProcessor.process(item: .data(originalData), options: options) {
            originalCache.store(originalImage,
                              original: originalData,
                              forKey: key,
                              processorIdentifier: defaultProcessor.identifier,
                              cacheSerializer: options.cacheSerializer,
                              toDisk: !options.cacheMemoryOnly,
                              completionHandler: nil)
        }
    }
}
存储图片

我们先来看一下实现的代码:

open func store(_ image: Image,
                  original: Data? = nil,
                  forKey key: String,
                  processorIdentifier identifier: String = "",
                  cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
                  toDisk: Bool = true,
                  completionHandler: (() -> Void)? = nil)
{...}

代码中memoryCache是:

fileprivate let memoryCache = NSCache<NSString, AnyObject>()

可见图片存储首先是缓存在NSCache中,如果想存储在磁盘中(if toDisk),利用串行队列异步的进行存储原图。

获取图片
@discardableResult
open func retrieveImage(forKey key: String,
                           options: KingfisherOptionsInfo?,
                 completionHandler: ((Image?, CacheType) -> ())?) -> RetrieveImageDiskTask?
{...}
  • 首先从内存中获取图片if let image = self.retrieveImageInMemoryCache(forKey: key, options: options)
  • 如果没有,在根据条件判断是否从磁盘上获取if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options)
删除图片
open func removeImage(forKey key: String,
                      processorIdentifier identifier: String = "",
                      fromDisk: Bool = true,
                      completionHandler: (() -> Void)? = nil)
{...}
@objc public func clearMemoryCache() {...}
open func clearDiskCache(completion handler: (()->())? = nil) {...}
open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {...}
@objc public func backgroundCleanExpiredDiskCache() {...}

其中,一些方法是通过通知的方法来实现:

// 系统内存警告
NotificationCenter.default.addObserver(
    self, selector: #selector(clearMemoryCache), name: .UIApplicationDidReceiveMemoryWarning, object: nil)    
// 程序终止
NotificationCenter.default.addObserver(
    self, selector: #selector(cleanExpiredDiskCache), name: .UIApplicationWillTerminate, object: nil)
// 程序进入后台
NotificationCenter.default.addObserver(
    self, selector: #selector(backgroundCleanExpiredDiskCache), name: .UIApplicationDidEnterBackground, object: nil)

此外,还有一些属性要注意:

  • maxMemoryCost最大缓存量,在收到内存警告时会被清空。
  • pathExtension沙盒后续拼接文件夹名称
  • maxCachePeriodInSecond默认清除一周前的图片
  • maxDiskCacheSize沙盒最大存储量,为0,默认无限制

以上就是对Kingfisher的简单描述,它有很多方法值得我们去借鉴,比如@discardableResultwheretypealiasif case let、善于利用guard、扩展协议等等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容