美文网首页iOS
iOS - 【Swift库】纯 Swift 图片缓存库Kingf

iOS - 【Swift库】纯 Swift 图片缓存库Kingf

作者: 俺不是大佬儿 | 来源:发表于2022-04-26 10:31 被阅读0次

    Swift 图片缓存库Kingfisher架构详解及使用

    Kingfisher是喵神的一个纯Swift语言的异步下载和缓存图片的Swift库,类似于OC 的SDWebImagegithub地址 最近一直在学习 Swift,对一些优秀的Swift第三方库也在逐步的了解应用。

    【Kingfisher】 github地址

    来源:参考大神的文章(Kingfisher 3.x 学习(一))

    一、Kingfisher的架构

    阅读他人优秀代码是一个提高自身代码水平很好的方法。花了几天的时间,看了Kingfisher的源代码,里面包含的很多知识点,让我受益匪浅。3.x版本相比与之前的版本,一个重要的改变就是protocol的灵活运用,更加面向协议编程。当然还有其他很多知识,比如多线程,枚举,闭包,Extension 等等应用。 Kingfisher中CoreExtensionHelpers 三个目录结构 共20个文件

    Core

    image.swift 文件内部对 UIImage 以及 NSData 进行了拓展, 包含判定图片类型、图片解码以及Gif数据处理等操作
    Indicator.swift 图片加载时loading指示
    ImageCache.swift 主要负责将加载过的图片缓存至本地。
    ImageDownloader.swift 负责下载网络图片。
    ImagePrefetcher.swift 可用于提前指定一些图片下载
    ImageProcessor.swift 可用于将下载的数据合成图片对象
    CacheSerializer.swift 可用于图像对象序列化成图像数据存储到磁盘缓存和从磁盘缓存将图片数据反序列化成图像对象。
    RequestModifier.swift 下载图像请求修改器。
    ImageTransition.swift 过渡动画效果 使用UIViewAnimationOptions动画效果
    KingfisherManager.swift Kingfisher 管理控制类,拥有图片下载及缓存功能
    KingfisherOptionsInfo.swift 枚举KingfisherOptionsInfoItem 配置 Kingfisher 行为的参数,包括 是否自定义缓存对象 是否自定义下载器 是否过渡动画 是否设置下载低优先级 是否强制刷新 是否仅获取缓存图片 是否仅缓存至内存、是否允许图像后台解码等设置。
    Filter.swift 图像过滤器
    Resource.swift 记录了图片的下载地址和缓存Key。
    Kingfisher.swift 添加KingfisherCompatible通用协议 kf新属性

    Extension

    ImageView+Kingfisher.swift UIButton+Kingfisher.swift NSButton+KingfisherUIImageView UIButton NSButton 进行了拓展 主要用于提供 Kingfisher 的外部接口。

    Helpers

    String+MD5.swift 负责图片缓存时对文件名进行MD5加密操作。
    Box.swift 一个简单泛型类
    ThreadHelper.swift中的 dispatch_async_safely_main_queue 函数接受一个闭包 利用 NSThread.isMainThread 判断并将其放置在主线程中执行

    二、Kingfisher.swift

    主要文件ImageView+Kingfisher,KingfisherManager,ImageCache,ImageDownloader,废话不多说直接代码学习

    运行demo 下面有这么一段代码:

        let url = URL(string:"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.row + 1).jpg")!
        (cell as! CollectionViewCell).cellImageView.kf.setImage(with: url,
                                               placeholder: nil,
                                               options: [.transition(.fade(1))],
                                               progressBlock: { receivedSize, totalSize in
                                                print("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
                },
                                               completionHandler: { image, error, cacheType, imageURL in
                                                print("\(indexPath.row + 1): Finished")
            })
    
    

    首先调用的UIImageViewkf属性 之前是调用UIImageViewExtension中的kf_setImage ,现已弃用 那kf 属性是如何实现的?
    下面是Kingfisher.swift源码

    • 自定义了不同平台下的一些类型别名 swift中的typealias 相当于OC中的typedef
    #if os(macOS)
        import AppKit
        public typealias Image = NSImage
        public typealias Color = NSColor
        public typealias ImageView = NSImageView
        typealias Button = NSButton
    #else
        import UIKit
        public typealias Image = UIImage
        public typealias Color = UIColor
        #if !os(watchOS)
        public typealias ImageView = UIImageView
        typealias Button = UIButton
        #endif
    #endif
    
    
    • 申明了泛型类Kingfisher 实现了一个简单构造器,其中上面的cellImageView就是base属性
    public final class Kingfisher<Base> {
        public let base: Base
        public init(_ base: Base) {
            self.base = base
        }
    }
    
    
    • 申明KingfisherCompatible协议 有一个可读属性kf 其类型是关联类型
    /**
     A type that has Kingfisher extensions.
     */
    
    public protocol KingfisherCompatible {
        associatedtype CompatibleType
        var kf: CompatibleType { get }
    }
    
    
    • KingfisherCompatible协议的实现 属性kf关联Kingfisher类型 返回一个Kingfisher实例 base 参数就是传入的self
    public extension KingfisherCompatible {
        public var kf: Kingfisher<Self> {
            get { return Kingfisher(self) }
        }
    }
    
    
    • Image ImageView Button 遵守 KingfisherCompatible 协议 所以上边的self参数就是遵守了协议的类型 因此base属性即cellImageView
    extension Image: KingfisherCompatible { }
    #if !os(watchOS)
    extension ImageView: KingfisherCompatible {}
    extension Button: KingfisherCompatible { }
    #endif
    
    

    三、ImageView+Kingfisher

    现在来说说setImage这个方法的实现 这个方法是在Kingfisher的Extension 中实现 并且要求Base属于UIImageView类型 即where Base: ImageView 由于kf 属性关联了Kingfisher
    所以可以调用(cell as! CollectionViewCell).cellImageView.kf.setImage
    Extensions目录下的三个文件都是类似实现的 这里就以ImageView+Kingfisher.swift为例
    下面方法是外部使用Kingfisher最频繁也是最重要的方法
    第一个参数Resource是一个URL遵守的Protocol,一般传入图片的URL,不可为空
    第二个参数placeholder是一个默认的占位图,可为空
    第三个参数KingfisherOptionsInfo 是个枚举数组,配置Kingfisher下载图片的一些操作行为
    第四个参数DownloadProgressBlock是个下载进度闭包,可以用于更新下载UI
    第五个参数completionHandler是个下载完成闭包,闭包参数包含图片,错误,缓存类型,URL 信息

    extension Kingfisher where Base: ImageView {
        /**
         Set an image with a resource, a placeholder image, options, progress handler and completion handler.
    
         - parameter resource:          Resource object contains information such as `cacheKey` and `downloadURL`.
         - parameter placeholder:       A placeholder image when retrieving the image at URL.
         - parameter options:           A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
         - parameter progressBlock:     Called when the image downloading progress gets updated.
         - parameter completionHandler: Called when the image retrieved and set.
    
         - returns: A task represents the retrieving process.
    
         - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread.
         The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method.
         */
        @discardableResult 忽略返回值警告
        public func setImage(with resource: Resource?,
                             placeholder: Image? = nil,
                             options: KingfisherOptionsInfo? = nil,
                             progressBlock: DownloadProgressBlock? = nil,
                             completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
        {
            当传入的resource为空时 使用guard语句提前退出 Resource是一个协议 URL遵守此协议 Resource有两个属性 cacheKey和downloadURL
            guard let resource = resource else {
                base.image = placeholder
                completionHandler?(nil, nil, .none, nil)
                return .empty
            }
            图片加载过程中是否显示placeholder
            var options = options ?? KingfisherEmptyOptionsInfo
            if !options.keepCurrentImageWhileLoading {
                base.image = placeholder
            }
            如果indicator存在,开启转圈动画 indicator 通过属性关联存取
            let maybeIndicator = indicator
            maybeIndicator?.startAnimatingView()
            关联属性绑定下载的URL
            setWebURL(resource.downloadURL)
    
            默认开启加载所有GIF图片数据,显示GIF 动态图片
            if base.shouldPreloadAllGIF() {
                options.append(.preloadAllGIFData)
            }
            调用KingfisherManager的方法来获取图片
            let task = KingfisherManager.shared.retrieveImage(
                with: resource,
                options: options,
                progressBlock: { receivedSize, totalSize in
                    下载进度回调
                    if let progressBlock = progressBlock {
                        progressBlock(receivedSize, totalSize)
                    }
                },
                completionHandler: {[weak base] image, error, cacheType, imageURL in
                    确保线程安全
                    DispatchQueue.main.safeAsync {
                        确保返回的图片与URL对应一致
                        guard let strongBase = base, imageURL == self.webURL else {
                            return
                        }
                        self.setImageTask(nil)
                        没有图片返回停止动画返回错误
                        guard let image = image else {
                            maybeIndicator?.stopAnimatingView()
                            completionHandler?(nil, error, cacheType, imageURL)
                            return
                        }
                        是否需要过渡动画 transitionItem 为 options中第一个.transition
                        需要过渡动画需要满足以下情况
                        1.transitionItem存在且不为.transition(.none)
                        2.options.forceTransition存在 或者 cacheType == .none 
                        guard let transitionItem = options.firstMatchIgnoringAssociatedValue(.transition(.none)),
                            case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
                        {
                            maybeIndicator?.stopAnimatingView()
                            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
                                                UIView.transition(with: strongBase, duration: transition.duration,
                                                                  options: [transition.animationOptions, .allowUserInteraction],
                                                                  animations: {
                                                                    // Set image property in the animation.
                                                                    设置图片,如果是自定义动画 在定义动画回调中设置图片,代码在ImageTransition.swift
                                                                    transition.animations?(strongBase, image)
                                                                  },
                                                                  completion: { finished in
                                                                    动画结束回调
                                                                    transition.completion?(finished)
                                                                    completionHandler?(image, error, cacheType, imageURL)
                                                                  })
                                              })
                        #endif
                    }
                })
            setImageTask(task)
       return task
        }
        /**
         Cancel the image download task bounded to the image view if it is running.
         Nothing will happen if the downloading has already finished.
         */
        取消下载
        public func cancelDownloadTask() {
            imageTask?.downloadTask?.cancel()
        }
    }
    
    

    ImageView+Kingfisher 中的WebUR indicatorType indicator imageTask 属性均使用属性关联技术实现数据的存取

    四 、KingfisherManager

    该类是Kingfisher唯一的一个管理调度类。这个类有下载和缓存两大功能模块 主要包含了两个属性 两个方法
    public var cache: ImageCache 图片缓存属性
    public var downloader: ImageDownloader 图片下载属性
    func downloadAndCacheImage 下载并且缓存图片方法
    func tryToRetrieveImageFromCache 获取缓存图片

    ImageView+Kingfisher中最后图片的获取就是由KingfisherManager的单例实现的retrieveImage

    • 外部调用获取图片方法
    func retrieveImage(with resource: Resource,
            options: KingfisherOptionsInfo?,
            progressBlock: DownloadProgressBlock?,
            completionHandler: CompletionHandler?) -> RetrieveImageTask{
            let task = RetrieveImageTask()
            if let options = options, 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
        }
    
    
    • 下载并且缓存图片的方法
       func downloadAndCacheImage(with url: URL,
                                 forKey key: String,
                          retrieveImageTask: RetrieveImageTask,
                              progressBlock: DownloadProgressBlock?,
                          completionHandler: CompletionHandler?,
                                    options: KingfisherOptionsInfo?) -> RetrieveImageDownloadTask?
        {
            获取下载器 并开启下载 
            let options = options ?? KingfisherEmptyOptionsInfo
            let downloader = options.downloader
            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
                    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.)
                       如果有错误并且没有修改过URL 返回缓存图片
                        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)
                    }
    
                    completionHandler?(image, error, .none, url)
    
                })
        }
    
    
    • 优先从缓存获取图片,如缓存中没有,在从网络获取图片
        func tryToRetrieveImageFromCache(forKey key: String,
                                           with url: URL,
                                  retrieveImageTask: RetrieveImageTask,
                                      progressBlock: DownloadProgressBlock?,
                                  completionHandler: CompletionHandler?,
                                            options: KingfisherOptionsInfo?)
        {
            打破下面diskTask内部闭包保持的循环引用,完成之后取消磁盘任务引用,避免循环引用,释放内存
            let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
                // Break retain cycle created inside diskTask closure below
                retrieveImageTask.diskRetrieveTask = nil
                completionHandler?(image, error, cacheType, imageURL)
            }
    
            let targetCache = options?.targetCache ?? cache
            let diskTask = targetCache.retrieveImage(forKey: key, options: options,
                completionHandler: { image, cacheType in
                    if image != nil {
                         成功返回图片
                        diskTaskCompletionHandler(image, nil, cacheType, url)
                    } else if let options = options, options.onlyFromCache {
                        返回失败 并且设置只从缓存获取图片 返回没有缓存错误
                        let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
                        diskTaskCompletionHandler(nil, error, .none, url)
                    } else {
                        返回失败 再从网络下载图片
                        self.downloadAndCacheImage(
                            with: url,
                            forKey: key,
                            retrieveImageTask: retrieveImageTask,
                            progressBlock: progressBlock,
                            completionHandler: diskTaskCompletionHandler,
                            options: options)
                    }
                }
            )
            retrieveImageTask.diskRetrieveTask = diskTask
        }
    
    

    五、 KingfisherOptionsInfo

    上面代码多次用到options这个参数,它的参数类型是KingfisherOptionsInfo是一个类型别名
    public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]
    KingfisherOptionsInfoItem 是一个枚举 配置 Kingfisher所有功能行为 下面是详细中文注释

    public enum KingfisherOptionsInfoItem {
    
        这个成员的关联值是一个ImageCache对象。 Kingfisher使用指定的缓存对象处理 相关业务,包括试图检索缓存图像和存储下载的图片。
        case targetCache(ImageCache)
    
        这个成员的关联值应该是一个ImageDownloader对象。Kingfisher将使用这个下载器下载的图片。
        case downloader(ImageDownloader)
    
        如果从网络下载的图片 Kingfisher将使用“ImageTransition这个枚举动画。从内存或磁盘缓存时默认过渡不会发生。如果需要,设置ForceTransition
        case transition(ImageTransition)
    
        有关“浮动”值将被设置为图像下载任务的优先级。值在0.0 ~ 1.0之间。如果没有设置这个选项,默认值(“NSURLSessionTaskPriorityDefault”)将被使用。
        case downloadPriority(Float)
    
        如果设置,将忽略缓存,开启一个下载任务的资源
        case forceRefresh
    
        如果设置 即使缓存的图片也将开启过渡动画
        case forceTransition
    
        如果设置,Kingfisher只会在内存中缓存值而不是磁盘
        case cacheMemoryOnly
    
        如果设置 Kingfisher只会从缓存中加载图片
        case onlyFromCache
    
        在使用之前在后台线程解码图像
        case backgroundDecode
    
        当从缓存检索图像时 这个成员的关联值将被用作目标队列的调度时回调。如果没 有设置, Kingfisher将使用主要quese回调
        case callbackDispatchQueue(DispatchQueue?)
    
        将检索到的图片数据转换成一个图时 这个成员变量将被用作图片缩放因子。图像分辨率,而不是屏幕尺寸。你可能处理时需要指定正确的缩放因子@2x或@3x Retina图像。
        case scaleFactor(CGFloat)
    
        是否所有的GIF应该加载数据。默认false,只显示GIF中第一张图片。如果true,所有的GIF数据将被加载到内存中进行解码。这个选项主要是用于内部的兼容性。你不应该把直接设置它。“AnimatedImageView”不会预加载所有数据,而一个正常的图像视图(“UIImageView”或“NSImageView”)将加载所有数据。选择使用相应的图像视图类型而不是设置这个选项。
        case preloadAllGIFData
    
        发送请求之前用于改变请求。这是最后的机会你可以修改请求。您可以修改请求一些定制的目的,如添加身份验证令牌头,进行基本的HTTP身份验证或类似的url映射。原始请求默认情况下将没有任何修改
        case requestModifier(ImageDownloadRequestModifier)
    
        下载完成时,处理器会将下载的数据转换为一个图像。如果缓存连接到下载器(当你正在使用KingfisherManager或图像扩展方法),转换后的图像也将被缓存
        case processor(ImageProcessor)
    
        提供一个CacheSerializer 可用于图像对象序列化成图像数据存储到磁盘缓存和从磁盘缓存将图片数据反序列化成图像对象
        case cacheSerializer(CacheSerializer)
    
        保持现有的图像同时设置另一个图像图像视图。通过设置这个选项,imageview的placeholder参数将被忽略和当前图像保持同时加载新图片
        case keepCurrentImageWhileLoading
    }
    
    

    下面是自定义<== 运算符 比较两个KingfisherOptionsInfoItem 是否相等 相等返回true 否则返回false

    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 (.downloader(_), .downloader(_)): return true
        case (.transition(_), .transition(_)): return true
        case (.downloadPriority(_), .downloadPriority(_)): return true
        case (.forceRefresh, .forceRefresh): 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 (.preloadAllGIFData, .preloadAllGIFData): return true
        case (.requestModifier(_), .requestModifier(_)): return true
        case (.processor(_), .processor(_)): return true
        case (.cacheSerializer(_), .cacheSerializer(_)): return true
        case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true
        default: return false
        }
    }
    
    

    下面是对CollectionType的一个扩展 返回匹配的第一个相同枚举值 上面过渡动画就有用到

    public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
        func firstMatchIgnoringAssociatedValue(_ target: Iterator.Element) -> Iterator.Element? {
            return index { $0 <== target }.flatMap { self[$0] }
        }
    
        func removeAllMatchesIgnoringAssociatedValue(_ target: Iterator.Element) -> [Iterator.Element] {
            return self.filter { !($0 <== target) }
        }
    }
    
    

    KingfisherOptionsInfo中有很多的类似的属性get方法 如下是关于图片编码的,默认返回DefaultCacheSerializer.default。如果要自定义图片编码,可以添加自定义CacheSerializerOptions数组

       public var cacheSerializer: CacheSerializer {
            if let item = firstMatchIgnoringAssociatedValue(.cacheSerializer(DefaultCacheSerializer.default)),
                case .cacheSerializer(let cacheSerializer) = item
            {
                return cacheSerializer
            }
            return DefaultCacheSerializer.default
        }
    
    

    结束

    至此 ,我们对Kingfisher对整体架构已经有比较清晰的认识了 如下图所示

    image

    作者:乔克_叔叔
    链接:https://www.jianshu.com/p/a47fefeed7f0
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

        本文标题:iOS - 【Swift库】纯 Swift 图片缓存库Kingf

        本文链接:https://www.haomeiwen.com/subject/cfycyrtx.html