Kingfisher基本入门介绍

Kingfisher基本入门介绍_第1张图片

写在开头:

  • 作为一个iOS开发你也许不知道图片的内存管理机制、也不不知道图片的解析机制、但是你肯定用过SDWebImage,也许用过Kingfisher。
  • 大多数人习惯了只要加载图片都用第三方, 但是你真正了解第三方为你做了什么吗?为什么我们不自己去下载图片,并且管理呢?
    也许你看完这篇文章心里就会有个答案

我们就从源码开始说起吧:

  • 首先,我们就一起分析一下该框架的组成。
    将KF导入工程后,下面是其结构:


    Kingfisher基本入门介绍_第2张图片
    项目结构

    除去support Files, 项目大致分为5个模块:

  • 图片存储模块(imageCache)
  • 图片下载模块(imageDownloader)
  • imageView分类模块(imageView+Kingfisher)
  • 图片加载模块(kingfisherManager)
  • 缓存key处理模块(String+MD5)
    其核心文件是KingfisherManager里面包含了图片赋值策略。
    我们从给图片赋值开始看:
 // kf是命名空间,swift中方法名不在是前缀加下划线显示而是采用了命名空间形式,跟原生库更接近
let imageV = UIImageView()
imageV.kf.setImage(with: URL(string: self.imageStr), placeholder: nil, options: nil, progressBlock: nil, completionHandler: nil)

我们创建了一个imageView,然后通过kf调用setImage给imageView赋值,我们点进去看看做了什么

// discardableResult表示返回值可以忽略不会出现警告
    @discardableResult
    public func setImage(with resource: Resource?,
                         placeholder: Placeholder? = nil,
                         options: KingfisherOptionsInfo? = nil,
                         progressBlock: DownloadProgressBlock? = nil,
                         completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
    {
        guard let resource = resource else {
            self.placeholder = placeholder
            setWebURL(nil)
            completionHandler?(nil, nil, .none, nil)
            return .empty
        }
        // options是个数组,存储了关于图片加载的配置
        var options = KingfisherManager.shared.defaultOptions + (options ?? KingfisherEmptyOptionsInfo)
        let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
        
        if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet { // Always set placeholder while there is no image/placehoer yet.
            self.placeholder = placeholder
        }
       // 开启加载动画
        let maybeIndicator = indicator
        maybeIndicator?.startAnimatingView()
        
        setWebURL(resource.downloadURL)

        if base.shouldPreloadAllAnimation() {
            options.append(.preloadAllAnimationData)
        }
        // 真正加载图片的方法
        let task = KingfisherManager.shared.retrieveImage(
            with: resource,
            options: options,
            progressBlock: { receivedSize, totalSize in
                guard resource.downloadURL == self.webURL else {
                    return
                }
                if let progressBlock = progressBlock {
                    progressBlock(receivedSize, totalSize)
                }
            },
            completionHandler: {[weak base] image, error, cacheType, imageURL in
                DispatchQueue.main.safeAsync {
                    maybeIndicator?.stopAnimatingView()
                    guard let strongBase = base, imageURL == self.webURL else {
                        completionHandler?(image, error, cacheType, imageURL)
                        return
                    }
                    
                    self.setImageTask(nil)
                    guard let image = image else {
                        completionHandler?(nil, error, cacheType, imageURL)
                        return
                    }
                    
                    guard let transitionItem = options.lastMatchIgnoringAssociatedValue(.transition(.none)),
                        case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
                    {
                        self.placeholder = nil
                        strongBase.image = image
                        completionHandler?(image, error, cacheType, imageURL)
                        return
                    }
                    
                    #if !os(macOS)
                        UIView.transition(with: strongBase, duration: 0.0, options: [],
                                          animations: { maybeIndicator?.stopAnimatingView() },
                                          completion: { _ in

                                            self.placeholder = nil
                                            UIView.transition(with: strongBase, duration: transition.duration,
                                                              options: [transition.animationOptions, .allowUserInteraction],
                                                              animations: {
                                                                // Set image property in the animation.
                                                                transition.animations?(strongBase, image)
                                                              },
                                                              completion: { finished in
                                                                transition.completion?(finished)
                                                                completionHandler?(image, error, cacheType, imageURL)
                                                              })
                                          })
                    #endif
                }
            })
        
        setImageTask(task)
        
        return task
    }
  • 这个就是图片的设置方法,大概的流程注释已经写清楚了, 还有三点需要详细说明下:
    • options:是KingfisherOptionsInfoItem类型的枚举,存储了关于图片缓存策略相关的以及下载策略相关配置
    • retrieveImage是真实获取图片数据方法
    • completionHandler: 是查找或下载结果的回调。
@discardableResult
    public func retrieveImage(with resource: Resource,
        options: KingfisherOptionsInfo?,
        progressBlock: DownloadProgressBlock?,
        completionHandler: CompletionHandler?) -> RetrieveImageTask

我们可以看到这个方法内部根据option参数不同调用了不同的方法:

  • 第一个方法:
func downloadAndCacheImage(with url: URL,
                             forKey key: String,
                      retrieveImageTask: RetrieveImageTask,
                          progressBlock: DownloadProgressBlock?,
                      completionHandler: CompletionHandler?,
                                options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
  • 第二个方法:
func tryToRetrieveImageFromCache(forKey key: String,
                                       with url: URL,
                              retrieveImageTask: RetrieveImageTask,
                                  progressBlock: DownloadProgressBlock?,
                              completionHandler: CompletionHandler?,
                                        options: KingfisherOptionsInfo)

至此图片加载的简单流程结束了。

一 、接下来我们看第一个图片加载策略:

  @discardableResult
  func downloadAndCacheImage(with url: URL,
                           forKey key: String,
                    retrieveImageTask: RetrieveImageTask,
                        progressBlock: DownloadProgressBlock?,
                    completionHandler: CompletionHandler?,
                              options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
  {
   // 图片下载器, 内部定义了默认超时时间是15s,图片下载所需的URLSession, 以及其它一些配置
      let downloader = options.downloader ?? self.downloader
     // 单独的数据处理队列
      let processQueue = self.processQueue
    // 下载图片,并通过回调函数返回
      return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
          progressBlock: { receivedSize, totalSize in
           // 下载进度相关
              progressBlock?(receivedSize, totalSize)
          },
          // 下载结果回调
          completionHandler: { image, error, imageURL, originalData in

              let targetCache = options.targetCache ?? self.cache
            // 如果图片下载失败则从缓存查找,并返回
              if let error = error, error.code == KingfisherError.notModified.rawValue {
                  // Not modified. Try to find the image from cache.
                  // (The image should be in cache. It should be guaranteed by the framework users.)
                  targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> Void in
                      completionHandler?(cacheImage, nil, cacheType, url)
                  })
                  return
              }
              // 存储图片数据
              if let image = image, let originalData = originalData {
                  targetCache.store(image,
                                    original: originalData,
                                    forKey: key,
 // 根据option存储策略存储原始图片                                   processorIdentifier:options.processor.identifier,
                                    cacheSerializer: options.cacheSerializer,
                                    toDisk: !options.cacheMemoryOnly,
                                    completionHandler: {
                                      guard options.waitForCache else { return }
                                      
                                      let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
                                      completionHandler?(image, nil, cacheType, url)
                  })
                  
                  if options.cacheOriginalImage && options.processor != DefaultImageProcessor.default {
                      let originalCache = options.originalCache ?? targetCache
                      let defaultProcessor = DefaultImageProcessor.default
                      processQueue.async {
                          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)
                          }
                      }
                  }
              }

              if options.waitForCache == false || image == nil {
                  completionHandler?(image, error, .none, url)
              }
          })
  }

对这个方法做个简单的总结。这个方法总主要做了三件事:

  • 调用了downloadImage方法,然后通过completionHandler这个回调函数把image, error,imageURL, originalData回调给我们。
  • 如果下载失败则从缓存中中查找图片
  • 拿到的图片数据存储起来,并且回调给外部使用

下面我们根据源码来分析作者如何实现这个三个功能的
图片下载:

    @discardableResult
    open func downloadImage(with url: URL,
                       retrieveImageTask: RetrieveImageTask? = nil,
                       options: KingfisherOptionsInfo? = nil,
                       progressBlock: ImageDownloaderProgressBlock? = nil,
                       completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
    {
        // 如果这个任务在开始之前就已经被取消则返回nil
        if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
            completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
            return nil
        }
        // 外界如果给了超时时间没给就15s
        let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
        
        // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
        request.httpShouldUsePipelining = requestsUsePipelining

        if let modifier = options?.modifier {
            guard let r = modifier.modified(for: request) else {
                completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
                return nil
            }
            request = r
        }
        
        // There is a possibility that request modifier changed the url to `nil` or empty.
        guard let url = request.url, !url.absoluteString.isEmpty else {
            completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
            return nil
        }
        
        var downloadTask: RetrieveImageDownloadTask?
// setup是一个任务管理队列,里面用了semphore作为锁去保证线程安全,spinlock会造成死锁。
        setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
            if fetchLoad.downloadTask == nil {
                let dataTask = session.dataTask(with: request)
                
                fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
                
                dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
                self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
                dataTask.resume()
                
                // Hold self while the task is executing.
                self.sessionHandler.downloadHolder = self
            }
            
            fetchLoad.downloadTaskCount += 1
            downloadTask = fetchLoad.downloadTask
            
            retrieveImageTask?.downloadTask = downloadTask
        }
        return downloadTask
    }

返回的RetrieveImageDownloadTask暴露给外部可以让下载中的任务取消

从缓存查找图片

    // MARK: - Get data from cache

    /**
    Get an image for a key from memory or disk.
    
    - parameter key:               Key for the image. - 查找图片的URL
    - parameter options:           Options of retrieving image. If you need to retrieve an image which was 
                                   stored with a specified `ImageProcessor`, pass the processor in the option too.
    - parameter completionHandler: Called when getting operation completes with image result and cached type of 
                                   this image. If there is no such key cached, the image will be `nil`.
    
    - returns: The retrieving task.
    */
    @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

// 从内存中查找图片, key为图片的url,未处理
        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
     // 从磁盘查找图片,key在这里为图片的URL经过md5处理。
                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), .disk)
                              // 释放资源
                                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
    }

存储图片

    /**
    Store an image to cache. It will be saved to both memory and disk. It is an async operation.
    
    - parameter image:             将要存储的图片
    - parameter original:          图片的data,将会跟图片转成的data对比,用来获取图片类型.如果为空则会存储一个png类型的图片.
    - parameter key:               存储图片的key.
    - parameter identifier:       用来处理图片的一个标识符.
    - parameter toDisk:            是否存储到磁盘,如果是false则仅仅存储在缓存内.
    - parameter completionHandler: 结果回调.
    */
    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)
    {
        // 如果ID为空, 则直接返回key,否则拼接上ID
        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 保证数据操作安全  
            ioQueue.async {// 存储到磁盘
                // 根据image获取图片的data。如果未传入data,则所有图片按照png类型,转为data。data主要用来判断图片格式, 通过image生成一个data
                if let data = serializer.data(with: image, original: original) {
                    //  文件存储在cache中(iOS文件结构: Documents, Library:(Cache , Preference) Tmp)
                    if !self.fileManager.fileExists(atPath: self.diskCachePath) {
                        do {
                            try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
                        } catch _ {}
                    }
                    //  cachePath(), 存储路径,对文件名称进行md5处理。可以保证唯一性和长度固定等
                    self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
                }
// 存储结束通知外面
                callHandlerInMainQueue()
            }
        } else {
            callHandlerInMainQueue()
        }
    }

二 、接下来我们看第二个图片加载策略:

tryToRetrieveImageFromCache

看完第一个第二个基本不用看,第二个和第一个主要区别是第一个直接下载。第二个会先从缓存中查找,找不到再进行第一步的操作

至此图片的的下载和缓存已经大概介绍完了

总结:

  • 根据URL去加载图片
  • 先从缓存查找 key为图片URL
  • 磁盘查找, 找到后在存入缓存中key为图片URL经过md5处理
  • 未找到下载图片,下载完成后存入缓存和磁盘。磁盘操作有一个队列串形处理

未完待续

你可能感兴趣的:(Kingfisher基本入门介绍)