源码阅读-Kingfisher
最后更新:2018-01-16
使用教程:
1. 开始使用
桥接 KingFisher, 利用 KingfisherCompatible协议来处理,
此处与 SnapKit的处理方式还是有点不同, SnapKit 是使用 ConstraintViewDSL 对象来设置, 对View 来设置方法, 当然这两种方式都可以;设置Image 利用
扩展extension
来处理/**
A type that has Kingfisher extensions.
*/
public protocol KingfisherCompatible {
associatedtype CompatibleType
var kf: CompatibleType { get }
} public extension KingfisherCompatible {
public var kf: Kingfisher<Self> {
get { return Kingfisher(self) }
}
} extension Image: KingfisherCompatible { }
#if !os(watchOS)
extension ImageView: KingfisherCompatible { }
extension Button: KingfisherCompatible { }
#endif extension Kingfisher where Base: ImageView {}
extension Kingfisher where Base: Image {}
extension Kingfisher where Base: UIApplication {}
extension Kingfisher where Base: UIButton {}
此处有一个极佳的使用场景,就是可以利用这种方式将 String 、NSString、NSAttributeString转换为 NSAttributeString
协议 Resource
协议Resource
里面定义了缓存时使用的var cacheKey: String { get }
以及 下载时候使用的var downloadURL: URL { get }
; 对于cacheKey
没什么好说的,但是对于downloadURL
, 作者定义为URL
类型, 查看 Alamofire 源码,我们可以看到一个 protocol URLConvertible,直接将 String 转换为 URL, 不知道这样做对用户来说,是否更加方便?协议 Placeholder
在 SDWebImage - UIImageView+WebCache中,placeholderImage
为一张UIImage
对象. 此处作者进行了扩展, 协议Placeholder
定义了add
与remove
方式, 任何对象只要遵循协议即可, 作者默认实现了Image
, 如果你需要一个View来充当 PlaceHolder, 你只要让你的 View 遵循这个协议即可。KingfisherOptionsInfoItem &
defaultOptions
作者在源码中, 一直传递着defaultOptions
值,defaultOptions
是用于存储 枚举KingfisherOptionsInfoItem
值, 其里面可以定义一系列的操作,最基础的如 下载downloader(ImageDownloader)
与 缓存.targetCache(cache)
, 刚看时候,非常懵逼, 深入进去, 截取核心部分代码:precedencegroup ItemComparisonPrecedence {
associativity: none
higherThan: LogicalConjunctionPrecedence
} infix operator <== : ItemComparisonPrecedence // This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values.
func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool {
switch (lhs, rhs) {
case (.targetCache(_), .targetCache(_)): return true
case (.originalCache(_), .originalCache(_)): return true
case (.downloader(_), .downloader(_)): return true
case (.transition(_), .transition(_)): return true
case (.downloadPriority(_), .downloadPriority(_)): return true
case (.forceRefresh, .forceRefresh): return true
case (.fromMemoryCacheOrRefresh, .fromMemoryCacheOrRefresh): return true
case (.forceTransition, .forceTransition): return true
case (.cacheMemoryOnly, .cacheMemoryOnly): return true
case (.onlyFromCache, .onlyFromCache): return true
case (.backgroundDecode, .backgroundDecode): return true
case (.callbackDispatchQueue(_), .callbackDispatchQueue(_)): return true
case (.scaleFactor(_), .scaleFactor(_)): return true
case (.preloadAllAnimationData, .preloadAllAnimationData): return true
case (.requestModifier(_), .requestModifier(_)): return true
case (.processor(_), .processor(_)): return true
case (.cacheSerializer(_), .cacheSerializer(_)): return true
case (.imageModifier(_), .imageModifier(_)): return true
case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true
case (.onlyLoadFirstFrame, .onlyLoadFirstFrame): return true
case (.cacheOriginalImage, .cacheOriginalImage): return true
default: return false
}
} extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
func lastMatchIgnoringAssociatedValue(_ target: Iterator.Element) -> Iterator.Element? {
return reversed().first { $0 <== target }
} func removeAllMatchesIgnoringAssociatedValue(_ target: Iterator.Element) -> [Iterator.Element] {
return filter { !($0 <== target) }
}
} public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
/// The `ImageDownloader` which is specified.
public var downloader: ImageDownloader { if let item = lastMatchIgnoringAssociatedValue(.downloader(.default)),
case .downloader(let downloader) = item
{
return downloader
}
return ImageDownloader.default
} /// Or the placeholder will be used while downloading.
public var keepCurrentImageWhileLoading: Bool {
return contains { $0 <== .keepCurrentImageWhileLoading }
}
}
此处作者重载了操作符, 更多内容可以参考SwiftTips-操作符、 Apple官方-operator-precedence、Operator Declarations; 取值的时候,先
reversed()
, 然后在取·first()
, 是不是觉得很妙?
哦, 顺便提一句, 代码中的if case 语法
可以参考: 模式匹配第四弹:if case,guard case,for case考虑一下: 作者这个做法根 Optionset 实现很像,能否使用 Optionset 来处理 KingfisherOptionsInfoItem 呢?
2. 下载图片 DownloadImage
前面说了这么多, 还没提到真正下载的部分, 在extension Kingfisher where Base: ImageView{}
中,通过调用 KingfisherManager.shared.retrieveImage()
方法来下载, 下载的任务顺利转交到 KingfisherManager
去了. KingfisherManager
通过 options.forceRefresh
判断是直接去下载图片 (ImageDownloader)还是 去从缓存(ImageCache)中获取;
2.1 ImageDownloader 下载图片
ImageDownloader是KingFisher中的下载器。 简单查看一下文档, 定义了一个内部类: ImageFetchLoad
, URLSession
以及相关的配置, 三个DispatchQueue
。 值得一提的是, 喵神在设计NSURLSessionTaskDelegate
的时候, 单独设计出一个ImageDownloaderSessionHandler
, 原因可以查看issues-235。
现在, 让我们一起来看一下具体的实现吧, 里面的核心方法是:
open func downloadImage(with url: URL,
retrieveImageTask: RetrieveImageTask? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: ImageDownloaderProgressBlock? = nil,
completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask? { }
前面几个是构建 URLRequest
, 对 URLRequest
进行modifier
, 对 url
进行判断等一系列操作, 在这个方法里面调用了-setup:
方法:
func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
func prepareFetchLoad() {
barrierQueue.sync(flags: .barrier) {
let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
fetchLoads[url] = loadObjectForURL
if let session = session {
started(session, loadObjectForURL)
}
}
}
if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
if fetchLoad.cancelSemaphore == nil {
fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
}
cancelQueue.async {
_ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
fetchLoad.cancelSemaphore = nil
prepareFetchLoad()
}
} else {
prepareFetchLoad()
}
}
2.1.1 dispatch_barrier_sync
现在我们好好分析这部分的代码, 一开始, 调用if let fetchLoad = fetchLoad(for: url)
, 我们进入这个方法:
func fetchLoad(for url: URL) -> ImageFetchLoad? {
var fetchLoad: ImageFetchLoad?
barrierQueue.sync(flags: .barrier) { fetchLoad = fetchLoads[url] }
return fetchLoad
}
可以看到有一个 barrierQueue.sync(flags: .barrier) {}
, 这是一个栅栏, 如果你曾经看过 SDWebImage
的源码, 你可以在里面看到 dispatch_barrier_sync
; 这个保证了线程安全, 可以查看:SDWebImage-关于dispatch_barrier_sync的issue 以及 Kingfisher-关于线程安全问题;
看到这里,你也就明白为什么用 dispatch_barrier_sync
了吧。 在作者初始化的时候, 使用的是 barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\(name)", attributes: .concurrent)
, 一个并发的队列, 我一开始没想明白为什么会这样。后来在查看官方文档中看到这么一段话:
The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_sync function.
延伸阅读:通过GCD中的dispatch_barrier_(a)sync加强对sync中所谓等待的理解
2.1.2 DispatchSemaphore 信号量
接下来存在着一段这样的代码:
if fetchLoad.cancelSemaphore == nil {
fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
}
cancelQueue.async {
_ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
fetchLoad.cancelSemaphore = nil
prepareFetchLoad()
}
简答说一下信号量, 可以去看我的简易的测试文件
- 信号量 value 为0的时候,阻塞当前线程, value大于0的时候,当前线程执行;
- 当执行
semaphonre.wait()
的时候, value值减一; - 当执行
semaphonre.signal()
的时候, value值加一; - 初始化的时候, value值不能小于0,
wait()
与signal()
必须配对;
那我们来分析这段代码, 初始化DispatchSemaphore(value: 0)
阻塞了, 那么在接下来 _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
一直阻塞这里。 当下载失败的时候, 调用leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0
。 如果下载成功了, 会直接 self.cleanFetchLoad(for: url)
; 如果没有失败, 是不是感觉会一直阻塞着?
当然不会, 这是因为, 当你取消任务的时候func cancelDownloadingTask(_ task: RetrieveImageDownloadTask)
, task.internalTask.cancel()
会发通知给func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
, 但不是立即的,系统在后台做了一些不为人知的事情, 如果就在此时, 同样的url请求进来了,那么你就需要阻塞住, 等前面的取消任务完成再执行。
2.1.3 prepareFetchLoad()
func prepareFetchLoad() {
barrierQueue.sync(flags: .barrier) {
let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
fetchLoads[url] = loadObjectForURL
if let session = session {
started(session, loadObjectForURL)
}
}
}
首先查看 fetchLoads
里面是否有对应的 ImageFetchLoad
对象, 然后将回调数组保存: loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
; 这样就可以使得一个url, 下载一次, 但是可以多个回调的问题;
2.1.4 下载完成处理 processImage
代码太长, 就不贴了, 你可以去文件查看;
异步对下载好的图片数据进行处理, 同一个url 可能会有多个回调,因此遍历来处理, 处理完成之后回调回去;
至此, ImageDownloader
的任务已经结束了;
2.1 ImageCache 缓存图片
如果你仔细看过 KingfisherOptionsInfoItem
枚举, 你会发现存在两个Cache: targetCache(ImageCache)
& originalCache(ImageCache)
;
targetCache(ImageCache)
是用来缓存最终的图片的, 例如你下载好一张原图之后, 你利用 ImageProcessor
进行了处理, 例如缩小到原来的一半, 处理后的图片就是利用 targetCache(ImageCache)
来处理;
originalCache(ImageCache)
就是缓存原图;
虽然枚举值不一样, 但是都是用 ImageCache来处理;
ImageCache利用的是 NSCache
来缓存图片.
2.2.1 存储图片
首先将图片存储在缓存 NSCache
中, 如果需要存储在磁盘上,利用串行队列异步的进行存储原图;
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)
{
let computedKey = key.computedKey(with: identifier)
memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
func callHandlerInMainQueue() {
if let handler = completionHandler {
DispatchQueue.main.async {
handler()
}
}
}
if toDisk {
ioQueue.async {
if let data = serializer.data(with: image, original: original) {
if !self.fileManager.fileExists(atPath: self.diskCachePath) {
do {
try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {}
}
self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
}
callHandlerInMainQueue()
}
} else {
callHandlerInMainQueue()
}
}
延伸阅读: NSCache
2.2.2 获取图片
获取图片首先从内存中获取,如果没有,在根据条件判断是否从磁盘上获取,
@discardableResult
open func retrieveImage(forKey key: String,
options: KingfisherOptionsInfo?,
completionHandler: ((Image?, CacheType) -> Void)?) -> RetrieveImageDiskTask?
{
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
return nil
}
var block: RetrieveImageDiskTask?
let options = options ?? KingfisherEmptyOptionsInfo
let imageModifier = options.imageModifier
if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
options.callbackDispatchQueue.safeAsync {
completionHandler(imageModifier.modify(image), .memory)
}
} else if options.fromMemoryCacheOrRefresh { // Only allows to get images from memory cache.
options.callbackDispatchQueue.safeAsync {
completionHandler(nil, .none)
}
} else {
var sSelf: ImageCache! = self
block = DispatchWorkItem(block: {
// Begin to load image from disk
if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
if options.backgroundDecode {
sSelf.processQueue.async {
let result = image.kf.decoded
sSelf.store(result,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil)
options.callbackDispatchQueue.safeAsync {
completionHandler(imageModifier.modify(result), .memory)
sSelf = nil
}
}
} else {
sSelf.store(image,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil
)
options.callbackDispatchQueue.safeAsync {
completionHandler(imageModifier.modify(image), .disk)
sSelf = nil
}
}
} else {
// No image found from either memory or disk
options.callbackDispatchQueue.safeAsync {
completionHandler(nil, .none)
sSelf = nil
}
}
})
sSelf.ioQueue.async(execute: block!)
}
return block
}
关于 DispatchWorkItem
相关资料,你可以看这里
2.2.3 删除图片
作者给我们提供了如下方法来删除内存和缓存的图片
open func removeImage(forKey key: String,
processorIdentifier identifier: String = "",
fromMemory: Bool = true,
fromDisk: Bool = true,
completionHandler: (() -> Void)? = nil) {}
@objc public func clearMemoryCache() {}
open func clearDiskCache(completion handler: (()->())? = nil) {}
// 删除过期的缓存的文件
open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {}
在清除过期缓存文件的时候,作者将过期的文件全部删除, 如果超过了磁盘文件的大小,也会按照使用的顺序来进行删除.
open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {
// Do things in cocurrent io queue
ioQueue.async {
var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
for fileURL in URLsToDelete {
do {
try self.fileManager.removeItem(at: fileURL)
} catch _ { }
}
if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
let targetSize = self.maxDiskCacheSize / 2
// Sort files by last modify date. We want to clean from the oldest files.
let sortedFiles = cachedFiles.keysSortedByValue {
resourceValue1, resourceValue2 -> Bool in
if let date1 = resourceValue1.contentAccessDate,
let date2 = resourceValue2.contentAccessDate
{
return date1.compare(date2) == .orderedAscending
}
// Not valid date information. This should not happen. Just in case.
return true
}
for fileURL in sortedFiles {
do {
try self.fileManager.removeItem(at: fileURL)
} catch { }
URLsToDelete.append(fileURL)
if let fileSize = cachedFiles[fileURL]?.totalFileAllocatedSize {
diskCacheSize -= UInt(fileSize)
}
if diskCacheSize < targetSize {
break
}
}
}
DispatchQueue.main.async {
if URLsToDelete.count != 0 {
let cleanedHashes = URLsToDelete.map { $0.lastPathComponent }
NotificationCenter.default.post(name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
}
handler?()
}
}
}
fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
let diskCacheURL = URL(fileURLWithPath: diskCachePath)
let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
var cachedFiles = [URL: URLResourceValues]()
var urlsToDelete = [URL]()
var diskCacheSize: UInt = 0
for fileUrl in (try? fileManager.contentsOfDirectory(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)) ?? [] {
do {
let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
// If it is a Directory. Continue to next file URL.
if resourceValues.isDirectory == true {
continue
}
// If this file is expired, add it to URLsToDelete
if !onlyForCacheSize,
let expiredDate = expiredDate,
let lastAccessData = resourceValues.contentAccessDate,
(lastAccessData as NSDate).laterDate(expiredDate) == expiredDate
{
urlsToDelete.append(fileUrl)
continue
}
if let fileSize = resourceValues.totalFileAllocatedSize {
diskCacheSize += UInt(fileSize)
if !onlyForCacheSize {
cachedFiles[fileUrl] = resourceValues
}
}
} catch _ { }
}
return (urlsToDelete, diskCacheSize, cachedFiles)
}
源码阅读-Kingfisher的更多相关文章
- 【原】FMDB源码阅读(三)
[原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ...
- 【原】FMDB源码阅读(二)
[原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...
- 【原】FMDB源码阅读(一)
[原]FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于 ...
- 【原】AFNetworking源码阅读(六)
[原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ...
- 【原】AFNetworking源码阅读(五)
[原]AFNetworking源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中提及到了Multipart Request的构建方法- [AFHTTP ...
- 【原】AFNetworking源码阅读(四)
[原]AFNetworking源码阅读(四) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇还遗留了很多问题,包括AFURLSessionManagerTaskDe ...
- 【原】AFNetworking源码阅读(三)
[原]AFNetworking源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇的话,主要是讲了如何通过构建一个request来生成一个data tas ...
- 【原】AFNetworking源码阅读(二)
[原]AFNetworking源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中我们在iOS Example代码中提到了AFHTTPSessionMa ...
- 【原】AFNetworking源码阅读(一)
[原]AFNetworking源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 AFNetworking版本:3.0.4 由于我平常并没有经常使用AFNetw ...
随机推荐
- web框架 Spring+SpringMvc+Jpa 纯注解搭建
0.jar包依赖 maven pom.xml <properties> <spring_version>4.3.25.RELEASE</spring_version&g ...
- 在linux下和Mac下如何实现快捷方式连接SSH远程服务器
其实特别简单 在本地命令执行目录/usr/local/bin 下新建一个shell脚本 比如 #vim ssh1 写入要执行的内容连接SSH #!/usr/bin/expect -f set user ...
- 内核中likely和unlikely宏定义
在内核代码中经常会看到unlikely和likely的踪影.他们实际上是定义在 linux/compiler.h 中的两个宏. #define likely(x) __builtin_expec ...
- 3D max导出的设置选项
一3D max导出的设置选项
- Homebrew学习(六)之替换及重置homebrew、Homebred Core、Homebrew cask默认源
替换及重置homebrew默认源 中科大源 替换官方源: // 替换brew.git: cd "$(brew --repo)" git remote set-url origin ...
- qt嵌入式html和本地c++通信方式
前沿:我们在做qt项目的时候,通常会把某个html网页直接显示到应用程序中.比如绘图.直接把html形式的图标嵌入到应用程序中 但是我们需要把数据从后台c++端传到html端,实现显示.qt实现了相关 ...
- SpringBoot项目中遇到的BUG
1.启动项目的时候报错 1.Error starting ApplicationContext. To display the auto-configuration report re-run you ...
- KVM安装配置笔记
系统环境centos6.6 一.KVM安装前系统相关操作: (1)修改内核模式为兼容内核启动 # grep -v "#" /etc/grub.confdevice (hd0) HD ...
- ArrayList实现原理分析
ArrayList使用的存储的数据结构 ArrayList的初始化 ArrayList是如何动态增长 ArrayList如何实现元素的移除 ArrayList小结 ArrayList是我们经常使用的一 ...
- Java引用与C语言指针的区别
1.现象 指针在运行时可以改变其所指向的值(地址)即指向其它变量,而引用一旦和某个对象绑定后就不能再改变,总是指向最初的对象. 2.编译 程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变 ...