Kingfisher基本入门介绍


写在开头:

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

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

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


    项目结构

    除去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处理
  • 未找到下载图片,下载完成后存入缓存和磁盘。磁盘操作有一个队列串形处理

未完待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353