• 源码阅读-Kingfisher


    最后更新:2018-01-16

    使用教程:

    1. 官方的链接
    2. 使用 Kingfisher 处理网络图片的读取与缓存

    1. 开始使用

    1. 桥接 KingFisher, 利用 KingfisherCompatible协议来处理,
      此处与 SnapKit的处理方式还是有点不同, SnapKit 是使用 ConstraintViewDSL 对象来设置, 对View 来设置方法, 当然这两种方式都可以;

    2. 设置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

    1. 协议 Resource
      协议 Resource 里面定义了缓存时使用的 var cacheKey: String { get } 以及 下载时候使用的var downloadURL: URL { get }; 对于 cacheKey没什么好说的,但是对于 downloadURL, 作者定义为 URL类型, 查看 Alamofire 源码,我们可以看到一个 protocol URLConvertible ,直接将 String 转换为 URL, 不知道这样做对用户来说,是否更加方便?

    2. 协议 Placeholder
      SDWebImage - UIImageView+WebCache中, placeholderImage 为一张 UIImage 对象. 此处作者进行了扩展, 协议 Placeholder 定义了 addremove 方式, 任何对象只要遵循协议即可, 作者默认实现了 Image, 如果你需要一个View来充当 PlaceHolder, 你只要让你的 View 遵循这个协议即可。

    3. 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-precedenceOperator 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)
        }
    
    
    
    
    

  • 相关阅读:
    [转] 传统 Ajax 已死,Fetch 永生
    React组件属性部类(propTypes)校验
    [转]webpack进阶构建项目(一)
    package.json 字段全解析
    [转]Nodejs基础中间件Connect
    [转]passport.js学习笔记
    [转]Travis Ci的最接底气的中文使用教程
    建站笔记1:centos6.5下安装mysql
    [软件人生]关于认知,能力的思考——中国城市里的无知现象片段
    一步一步学Spring.NET——1、Spring.NET环境准备
  • 原文地址:https://www.cnblogs.com/gaox97329498/p/12070516.html
Copyright © 2020-2023  润新知