彩世界开奖app官网-彩世界平台官方网址(彩票平台)
做最好的网站
来自 计算机编程 2019-09-13 03:13 的文章
当前位置: 彩世界开奖app官网 > 计算机编程 > 正文

深入理解Kingfisher(上)彩世界开奖app官网

缓存功能的架构以及主要属性介绍

缓存功能分为两部分:一是内存缓存,二是硬盘缓存。我们需要实现的主要功能有:

  • 缓存路径管理
  • 缓存的添加与删除
  • 缓存的读取
  • 缓存的清理
  • 缓存状态监控

缓存管理类所包含的主要属性如下所示:

public class ImageCache { //Memory private let memoryCache = NSCache() /// The largest cache cost of memory cache. The total cost is pixel count of all cached images in memory. public var maxMemoryCost: UInt = 0 { didSet { self.memoryCache.totalCostLimit = Int(maxMemoryCost) } } //Disk private let ioQueue: dispatch_queue_t private let diskCachePath: String private var fileManager: NSFileManager! /// The longest time duration of the cache being stored in disk. Default is 1 week. public var maxCachePeriodInSecond = defaultMaxCachePeriodInSecond /// The largest disk size can be taken for the cache. It is the total allocated size of cached files in bytes. Default is 0, which means no limit. public var maxDiskCacheSize: UInt = 0 private let processQueue: dispatch_queue_t /// The default cache. public class var defaultCache: ImageCache { return defaultCacheInstance } public init(name: String) { if name.isEmpty { fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") } let cacheName = cacheReverseDNS   name memoryCache.name = cacheName let paths = NSSearchPathForDirectoriesInDomains(.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true) diskCachePath = (paths.first! as NSString).stringByAppendingPathComponent(cacheName) ioQueue = dispatch_queue_create(ioQueueName   name, DISPATCH_QUEUE_SERIAL) processQueue = dispatch_queue_create(processQueueName   name, DISPATCH_QUEUE_CONCURRENT) dispatch_sync(ioQueue, { () -> Void in self.fileManager = NSFileManager NSNotificationCenter.defaultCenter().addObserver(self, selector: "clearMemoryCache", name: UIApplicationDidReceiveMemoryWarningNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "cleanExpiredDiskCache", name: UIApplicationWillTerminateNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "backgroundCleanExpiredDiskCache", name: UIApplicationDidEnterBackgroundNotification, object: nil) } deinit { NSNotificationCenter.defaultCenter().removeObserver }}

其中,memoryCache: NSCache 用于管理内存缓存。ioQueue: dispatch_queue_t 为单独的硬盘操作队列,由于硬盘存取操作极为耗时,使其与主线程并行执行以免造成阻塞。diskCachePath: String 用于设置文件的存储路径。fileManager: NSFileManager 用于文件管理。processQueue: dispatch_queue_t 用于执行图片的 decode 操作,不过并不会被调用。defaultCache 为 ImageCache 类的单例,Swift 中,采用 static let 即可直接创建一个单例,系统会自动调用 dispatch_once。

这个看名字就知道会在操作结束之后调用。

对了,最后的最后,Swift已经开源啦!

彩世界开奖app官网 1Kingfisher.png

 @discardableResult
    public func retrieveImage(with resource: Resource,
        options: KingfisherOptionsInfo?,
        progressBlock: DownloadProgressBlock?,
        completionHandler: CompletionHandler?) -> RetrieveImageTask
    {
       // 新建任务
        let task = RetrieveImageTask()
        let options = currentDefaultOptions   (options ?? KingfisherEmptyOptionsInfo)
    //若强制刷新则联网下载并缓存
        if 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
    }

缓存模块的主要内容就这些了,其他还有一些辅助方法像计算缓存尺寸啊、图片的排序啊、把图片URL进行MD5加密作为缓存文件名啊等等,我就不具体写了,有兴趣的同学可以直接去看源码。在UIImage Extension文件中还有一些处理图片的扩展方法,诸如标准化图片格式、GIF图片的存储、GIF图片的展示等等我也不细讲了,这些都算是一些套路上的东西,正确调用苹果给的API就好了。

字典按值排序

在缓存的管理当中,有时候我们需要依照缓存的修改时间进行排序,以确定缓存是否过期,而缓存时间往往位于字典键值对中值的位置,通常情况下对其排序并不是太容易,这里提供一个工具函数,代码如下:

extension Dictionary { func keysSortedByValue(isOrderedBefore:(Value, Value) -> Bool) -> [Key] { var array = Array array.sortInPlace { let  = $0 let  = $1 return isOrderedBefore } return array.map { let  = $0 return k } }}

接受排序规则闭包,若返回值为 true,则第一个参数在第二个的前面,函数返回排序过后的Key值数组。函数体的第一句是亮点,这里直接用 Array 的初始化方法将 Dictionary 转成了一个元组数组,每个元组包含两个值,第一个为原字典Key,第二个为原字典Value。sortInPlace 为在当前数组内存位置上进行排序,闭包里先用两个 let 取到字典Value,将其送入排序规则中比对并返回比对结果,该函数执行过后,我们就能得到一个按字典Value排好序的元组数组。接着,我们调用 map 函数,将每个元组的第一个值,取出并覆盖原元组,最后得到有序的字典Key值数组。

    public static let shared = KingfisherManager()
  • 如果内存中没有缓存,则从文件中取图片,并判断是否需要进行解码,若需要则先解码再将它缓存到内存中然后执行完成闭包,否则直接缓存到内存中然后执行完成闭包,这里有一些关于GCD和避免retain cycle的技术细节,我写在注释中了:

本文将详尽的对所涉及到的知识点进行讲解,但由于笔者水平有限,失误和遗漏之处在所难免,恳请前辈们批评指正。

缓存模块

我们是从KingfisherManager中的downloadAndCacheImage为入口进入到下载模块的,缓存模块也从这里进入。再贴一下downloadAndCacheImage吧:

 @discardableResult
    func downloadAndCacheImage(with url: URL,
                             forKey key: String,
                      retrieveImageTask: RetrieveImageTask,
                          progressBlock: DownloadProgressBlock?,
                      completionHandler: CompletionHandler?,
                                options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
    {
        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
                // 在下载完图片之后的完成闭包中(会在下载请求结束后调用),如果服务器返回状态码notModified,说明服务器图片未更新,我们可以从缓存中取得图片数据,就是调用retrieveImage 我们进入到retrieveImage查看
                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) -> () 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)
                    if options.cacheOriginalImage {
                        let defaultProcessor = DefaultImageProcessor.default
                        if let originaliImage = defaultProcessor.process(item: .data(originalData), options: options) {
                            targetCache.store(originaliImage,
                                              original: originalData,
                                              forKey: key,
                                              processorIdentifier: defaultProcessor.identifier,
                                              cacheSerializer: options.cacheSerializer,
                                              toDisk: !options.cacheMemoryOnly,
                                              completionHandler: nil)
                        }

                    }
                }

                completionHandler?(image, error, .none, url)

            })
    }

// 在下载完图片之后的完成闭包中(会在下载请求结束后调用),如果服务器返回状态码notModified,说明服务器图片未更新,我们可以从缓存中取得图片数据,就是调用retrieveImage 我们进入到retrieveImage查看

  1. 给完成闭包进行解包,若为空则提前返回:
 // No completion handler. Not start working and early return.
        guard let completionHandler = completionHandler else {
            return nil
        }
  1. 如果内存中有缓存则直接从内存中取图片;再判断图片是否需要解码,若需要,则先解码再调用完成闭包,否则直接调用完成闭包:
 @discardableResult
    open func retrieveImage(forKey key: String,
                               options: KingfisherOptionsInfo?,
                     completionHandler: ((Image?, CacheType) -> ())?) -> RetrieveImageDiskTask?
    {
        // No completion handler. Not start working and early return.
        guard let completionHandler = completionHandler else {
            return nil
        }

        var block: RetrieveImageDiskTask?
        let options = options ?? KingfisherEmptyOptionsInfo

        if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
            options.callbackDispatchQueue.safeAsync {
                completionHandler(image, .memory)
            }
        } 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(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(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
    }
  1. 如果内存中没有缓存,则从文件中取图片,并判断是否需要进行解码,若需要则先解码再将它缓存到内存中然后执行完成闭包,否则直接缓存到内存中然后执行完成闭包
    获取图片就是这样了,这个方法里调用了store这个方法,显然是用来缓存图片的,来看一下它的具体逻辑:
        memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)

缓存到内存中
如果方法参数toDisk为true则先将其缓存到文件(如果图片数据存在并能被正确解析的话),然后调用完成闭包:

 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()
        }
    }
  1. 整个缓存逻辑就是这样
    ImageCache中还有一个删除过期缓存的方法
 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
      // 遍历缓存图片(跳过隐藏文件和文件夹),如果图片过期,则加入待删除队列:
        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)
    }

若剩余缓存内容超过预设的最大缓存尺寸,则删除存在时间较长的缓存,并将已删除图片的URL也加大删除队列中(为了一会儿的广播),直到缓存尺寸到达预设最大尺寸的一半:

 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?()
            }
        }
    }

在主线程广播已删除的缓存图片,如果有传入完成闭包的话,就调用它:
缓存模块的主要内容就这些了,其他还有一些辅助方法像计算缓存尺寸啊、图片的排序啊、把图片URL进行MD5加密作为缓存文件名啊等等,具体写了,有兴趣的同学可以直接去看源码。在UIImage Extension文件中还有一些处理图片的扩展方法,诸如标准化图片格式、GIF图片的存储、GIF图片的展示等等,这些都算是一些套路上的东西,正确调用苹果给的API就好了.
Kingfisher中还用到了很多小技巧,比如对关联对象(Associated Object)的使用,解决了extension不能扩展存储属性的问题:

 public var webURL: URL? {
        return objc_getAssociatedObject(base, &lastURLKey) as? URL
    }

    fileprivate func setWebURL(_ url: URL?) {
        objc_setAssociatedObject(base, &lastURLKey, url, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }

//全局变量,用来作为关联对象(估计是因为extension里面不能添加储存属性,只能通过关联对象配合计算属性和方法的方式来hack)
总结:

  1. 文件操作相关知识(遍历文件、跳过隐藏文件、按日期排序文件等等)
  2. 图片处理相关知识(判断图片格式、处理GIF等等)
  3. MD5摘要算法(这个我并没有仔细看)
  4. Associated Object的运用
  5. Swift中关于enum和模式匹配的优雅用法

由于时间长促,下次改善排版,尽可能详细阐述每个方法

  • GCD的调度对象块(dispatch_block_t),可以在执行前取消(dispatch_block_cancel)
  • 文件操作相关知识(遍历文件、跳过隐藏文件、按日期排序文件等等)
  • 图片处理相关知识(判断图片格式、处理GIF等等)
  • MD5摘要算法(这个我并没有仔细看)
  • Associated Object的运用

缓存清理

我们在缓存清理方面的需求一般有两个:清理所有硬盘内存缓存、后台自动删除过期超量硬盘缓存。

这部分操作相较于下部分简单一些,代码如下:

 @objc public func clearMemoryCache() { memoryCache.removeAllObjects() } /** Clear disk cache. This is an async operation. */ public func clearDiskCache() { clearDiskCacheWithCompletionHandler } /** Clear disk cache. This is an async operation. - parameter completionHander: Called after the operation completes. */ public func clearDiskCacheWithCompletionHandler(completionHander: )?) { dispatch_async(ioQueue, { () -> Void in do { try self.fileManager.removeItemAtPath(self.diskCachePath) } catch _ { } do { try self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil) } catch _ { } if let completionHander = completionHander { dispatch_async(dispatch_get_main_queue -> Void in completionHander }

这里需要注意的是,我们使用 self.fileManager.removeItemAtPath 删除所有硬盘缓存之后,需要使用 self.fileManager.createDirectoryAtPath 重建缓存目录。

这部分的重点有如下几个:

  • 遍历所有缓存文件
  • 判断缓存文件是否过期
  • 将缓存文件按日期排序,逐步清理直到所占空间小于预定大小
  • 后台自动清理缓存

解决前三个问题,代码如下:

 public func cleanExpiredDiskCacheWithCompletionHander(completionHandler: )?) { // Do things in cocurrent io queue dispatch_async(ioQueue, { () -> Void in let diskCacheURL = NSURL(fileURLWithPath: self.diskCachePath) let resourceKeys = [NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey] let expiredDate = NSDate(timeIntervalSinceNow: -self.maxCachePeriodInSecond) var cachedFiles = [NSURL: [NSObject: AnyObject]]() var URLsToDelete = [NSURL]() var diskCacheSize: UInt = 0 if let fileEnumerator = self.fileManager.enumeratorAtURL(diskCacheURL, includingPropertiesForKeys: resourceKeys, options: NSDirectoryEnumerationOptions.SkipsHiddenFiles, errorHandler: nil) { for fileURL in fileEnumerator.allObjects as! [NSURL] { do { let resourceValues = try fileURL.resourceValuesForKeys(resourceKeys) // If it is a Directory. Continue to next file URL. if let isDirectory = resourceValues[NSURLIsDirectoryKey] as? NSNumber { if isDirectory.boolValue { continue } } // If this file is expired, add it to URLsToDelete if let modificationDate = resourceValues[NSURLContentModificationDateKey] as? NSDate { if modificationDate.laterDate(expiredDate) == expiredDate { URLsToDelete.append continue } } if let fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey] as? NSNumber { diskCacheSize  = fileSize.unsignedLongValue cachedFiles[fileURL] = resourceValues } } catch _ { } } } for fileURL in URLsToDelete { do { try self.fileManager.removeItemAtURL } 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[NSURLContentModificationDateKey] as? NSDate { if let date2 = resourceValue2[NSURLContentModificationDateKey] as? NSDate { return date1.compare == .OrderedAscending } } // Not valid date information. This should not happen. Just in case. return true }) for fileURL in sortedFiles { do { try self.fileManager.removeItemAtURL } catch { } URLsToDelete.append if let fileSize = cachedFiles[fileURL]?[NSURLTotalFileAllocatedSizeKey] as? NSNumber { diskCacheSize -= fileSize.unsignedLongValue } if diskCacheSize < targetSize { break } } } dispatch_async(dispatch_get_main_queue -> Void in if URLsToDelete.count != 0 { let cleanedHashes = URLsToDelete.map -> String in return url.lastPathComponent! }) NSNotificationCenter.defaultCenter().postNotificationName(KingfisherDidCleanDiskCacheNotification, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes]) } if let completionHandler = completionHandler { completionHandler }

其中,关于第一个问题:我们利用 NSFileManager 的实例方法 enumeratorAtURL,来获得 NSDirectoryEnumerator 的实例 fileEnumerator,再利用 for in 遍历 fileEnumerator.allObjects 来获取每个缓存文件的 fileURL: NSURL。第二个问题:我们通过 fileURL.resourceValuesForKeys[NSURLContentModificationDateKey] 来得到对应文件的最近修改日期属性,将其与过期时间比较,即可确定其是否过期。第三个问题:我们通过之前的字典按值排序拓展方法来对缓存文件按最近修改日期进行排序,随即对其遍历,按顺序删除,直到小于预定大小。

除以上三个问题之外,我们还希望,当应用程序在进入后台的时候,可以自动检测过期超量缓存,并在后台完成清理操作,实现代码如下:

 /** Clean expired disk cache when app in background. This is an async operation. In most cases, you should not call this method explicitly. It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received. */ @objc public func backgroundCleanExpiredDiskCache() { func endBackgroundTask(inout task: UIBackgroundTaskIdentifier) { UIApplication.sharedApplication().endBackgroundTask task = UIBackgroundTaskInvalid } var backgroundTask: UIBackgroundTaskIdentifier! backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in endBackgroundTask(&backgroundTask!) } cleanExpiredDiskCacheWithCompletionHander { () -> () in endBackgroundTask(&backgroundTask!) } }

该函数会在应用进入运行时自动调用,实现方法是利用 NSNotificationCenter.defaultCenter 来监听系统的 UIApplicationDidEnterBackgroundNotification 广播;beginBackgroundTaskWithExpirationHandler 以及 endBackgroundTask 之间的操作会在后台执行,不止可以做自动缓存清理,你也可以将一些比较耗时的下载操作放在后台进行;backgroundTask 作为任务开始和结束的标识,endBackgroundTask 函数参数列表中的 inout 是为了使 backgroundTask 在函数体内部的修改有效化,类似于传入指针。

Kingfisher是喵神写的一个异步下载和缓存图片的Swift库,github上将近3k的Star,相信不需要我再安利了。它的中文简介在这里,github地址在这里。
本次我们研究的是最新的基于Swift 4
Kingfisher的文档非常完备,我先大致看了一下,然后下载源码,跑了一下demo。demo中有这么一段:

  • 若剩余缓存内容超过预设的最大缓存尺寸,则删除存在时间较长的缓存,并将已删除图片的URL也加大删除队列中(为了一会儿的广播),直到缓存尺寸到达预设最大尺寸的一半:

图片解码

我们知道 PNG 以及 JPEG 等格式的图片对原图进行了压缩,必须要将其图片数据解码成位图之后才能使用,这是原因,Kingfisher 里提供了用于解码的函数,代码如下:

// MARK: - Decodeextension UIImage { func kf_decodedImage() -> UIImage? { return self.kf_decodedImage(scale: self.scale) } func kf_decodedImage(scale scale: CGFloat) -> UIImage? { let imageRef = self.CGImage let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue).rawValue let contextHolder = UnsafeMutablePointer<Void>() let context = CGBitmapContextCreate(contextHolder, CGImageGetWidth, CGImageGetHeight, 8, 0, colorSpace, bitmapInfo) if let context = context { let rect = CGRectMake(0, 0, CGFloat(CGImageGetWidth), CGFloat(CGImageGetHeight)) CGContextDrawImage(context, rect, imageRef) let decompressedImageRef = CGBitmapContextCreateImage return UIImage(CGImage: decompressedImageRef!, scale: scale, orientation: self.imageOrientation) } else { return nil } }}

这段代码的主要含义是通过 CGBitmapContextCreate 以及 CGContextDrawImage 函数,将被压缩的图片画在 context 上,再通过调用 CGBitmapContextCreateImage 函数,即可完成对被压缩图片的解码。但通过测试后续代码发现,包含 decode 函数的分支从来没被调用过,据本人推测,UIImage 在接收 NSData 数据进行初始化的时候,其本身很可能包含有通过 Magic Number 获知图片格式后,解码并展示的功能,并不需要外部解码。

简单来说它就是一个接收图片的任务

图片格式识别

Magic Number 是用于区分不同文件格式,被放置于文件首的标记数据。Kingfisher 中用它来区分不同的图片格式,如PNG、JPG、GIF。代码如下:

private let pngHeader: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]private let jpgHeaderSOI: [UInt8] = [0xFF, 0xD8]private let jpgHeaderIF: [UInt8] = [0xFF]private let gifHeader: [UInt8] = [0x47, 0x49, 0x46]// MARK: - Image formatenum ImageFormat { case Unknown, PNG, JPEG, GIF}extension NSData { var kf_imageFormat: ImageFormat { var buffer = [UInt8](count: 8, repeatedValue: 0) self.getBytes(&buffer, length: 8) if buffer == pngHeader { return .PNG } else if buffer[0] == jpgHeaderSOI[0] && buffer[1] == jpgHeaderSOI[1] && buffer[2] == jpgHeaderIF[0] { return .JPEG }else if buffer[0] == gifHeader[0] && buffer[1] == gifHeader[1] && buffer[2] == gifHeader[2] { return .GIF } return .Unknown }}

代码上部定义的 imageHeader,就是不同格式图片放置在文件首的对应 Magic Number 数据,我们通过 NSData 的 getBytes: 方法得到图片数据的 Magic Number,通过比对确定图片格式。

 @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
        }

        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)
        }
        /**
        代码块A (方便解说, 将代码拆开, 后面同样标记,标识同一位置代码)
        /
}

获取图片就是这样了,这个方法里调用了storeImage这个方法,显然是用来缓存图片的,来看一下它的具体逻辑:

GIF数据的展示

我们并不能像其他格式的图片一样直接传入 NSData 给 UIImage 来创建一个GIF动图,而是需要使用 UIImage 的 animatedImageWithImages 方法,但此函数所需的参数是 [UIImage],所以我们需要首先将 NSData 格式的图片数据拆分为每一帧的静态图片,再将其传入上述函数之中,代码如下:

extension UIImage { static func kf_animatedImageWithGIFData(gifData data: NSData) -> UIImage? { return kf_animatedImageWithGIFData(gifData: data, scale: UIScreen.mainScreen().scale, duration: 0.0) } static func kf_animatedImageWithGIFData(gifData data: NSData, scale: CGFloat, duration: NSTimeInterval) -> UIImage? { let options: NSDictionary = [kCGImageSourceShouldCache as String: NSNumber(bool: true), kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF] guard let imageSource = CGImageSourceCreateWithData(data, options) else { return nil } let frameCount = CGImageSourceGetCount(imageSource) var images = [UIImage]() var gifDuration = 0.0 for i in 0 ..< frameCount { guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, options) else { return nil } guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil), gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary, frameDuration = (gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber) else { return nil } gifDuration  = frameDuration.doubleValue images.append(UIImage(CGImage: imageRef, scale: scale, orientation: .Up)) } if (frameCount == 1) { return images.first } else { return UIImage.animatedImageWithImages(images, duration: duration <= 0.0 ? gifDuration : duration) } }}

与 CGImageDestination 相对应的,CGImageSource 对象是对数据读出操作的抽象,Kingfisher 用其实现GIF数据的读出。与 CGImageDestination 的写入操作相类似,这里我们通过 CGImageSourceCreateWithData、CGImageSourceGetCount 以及循环执行相应次数的 CGImageSourceCreateImageAtIndex 来得到 [UIImage],并通过 CGImageSourceCopyPropertiesAtIndex 等相关操作取得每张图片的原持续时间,将其求和,最后将两个对应参数传入 UIImage.animatedImageWithImages 中,即可得到所需的GIF动图。

 /**
        代码块A 
        /
   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
extension NSData {
    //图片格式解析
    var kf_imageFormat: ImageFormat {
        var buffer = [UInt8](count: 8, repeatedValue: 0)
        //获取前8个Byte
        self.getBytes(&buffer, length: 8)
        if buffer == pngHeader {
            return .PNG
        } else if buffer[0] == jpgHeaderSOI[0] &&
            buffer[1] == jpgHeaderSOI[1] &&
            buffer[2] == jpgHeaderIF[0]
        {
            return .JPEG
        } else if buffer[0] == gifHeader[0] &&
            buffer[1] == gifHeader[1] &&
            buffer[2] == gifHeader[2]
        {
            return .GIF
        }

        return .Unknown
    }
}

缓存路径管理

为了方便对硬盘的存取操作,我们需要这样几个工具函数,来帮我们实现通过缓存Key获得某特定缓存的:

  • 对应 UIImage 图片
  • 对应 NSData 数据
  • 硬盘存储路径
  • 加密后的文件名

代码如下:

extension ImageCache { func diskImageForKey(key: String, scale: CGFloat) -> UIImage? { if let data = diskImageDataForKey { return UIImage.kf_imageWithData(data, scale: scale) } else { return nil } } func diskImageDataForKey(key: String) -> NSData? { let filePath = cachePathForKey return NSData(contentsOfFile: filePath) } func cachePathForKey(key: String) -> String { let fileName = cacheFileNameForKey return (diskCachePath as NSString).stringByAppendingPathComponent } func cacheFileNameForKey(key: String) -> String { return key.kf_MD5() }}

由下及上,由深入浅,每个函数都用到了上一个函数的结果并进行了进一步加工。

返回类型是RetrieveImageTask,它是长这样的:

let diskCacheURL = NSURL(fileURLWithPath: self.diskCachePath)
let resourceKeys = [NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]
//过期日期:当期日期减去缓存时限,缓存时限默认为一周
let expiredDate = NSDate(timeIntervalSinceNow: -self.maxCachePeriodInSecond)
var cachedFiles = [NSURL: [NSObject: AnyObject]]()
var URLsToDelete = [NSURL]()
var diskCacheSize: UInt = 0

图片正立

使用 Core Graphics 绘制图片时,图片会倒立显示,Kingfisher 中使用了这个特性创建了一个工具函数来确保图片的正立,虽然该函数并未在后续的文件中使用到,代码如下:

// MARK: - Normalizationextension UIImage { public func kf_normalizedImage() -> UIImage { if imageOrientation == .Up { return self } UIGraphicsBeginImageContextWithOptions(size, false, scale) drawInRect(CGRect(origin: CGPointZero, size: size)) let normalizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return normalizedImage; }}

如果该图片方向为正立,返回自身;否则将其用 Core Graphics 绘制,返回正立后的图片。

KingfisherManager 的单利调用了 retrieveImage 它整合了下载和缓存两大功能,先看一下完整的方法签名, 认为是整个KingfisherManager的核心:

GIF数据的保存

我们知道,UIImage 并不能直接保存,需要先将其转化为 NSData 才能写入硬盘以及内存中缓存起来,UIKit 提供了两个 C 语言函数:UIImageJPEGRepresentation 和 UIImagePNGRepresentation,以便于将 JPG 及 PNG 格式的图片转化为 NSData 数据,但却并没有提供相应的 UIImageGIFRepresentation,所以我们需要自己编写这个函数以完成对Gif数据的保存,代码如下:

import ImageIOimport MobileCoreServices// MARK: - GIFfunc UIImageGIFRepresentation(image: UIImage) -> NSData? { return UIImageGIFRepresentation(image, duration: 0.0, repeatCount: 0)}func UIImageGIFRepresentation(image: UIImage, duration: NSTimeInterval, repeatCount: Int) -> NSData? { guard let images = image.images else { return nil } let frameCount = images.count let gifDuration = duration <= 0.0 ? image.duration / Double(frameCount) : duration / Double(frameCount) let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: gifDuration]] let imageProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: repeatCount]] let data = NSMutableData() guard let destination = CGImageDestinationCreateWithData(data, kUTTypeGIF, frameCount, nil) else { return nil } CGImageDestinationSetProperties(destination, imageProperties) for image in images { CGImageDestinationAddImage(destination, image.CGImage!, frameProperties) } return CGImageDestinationFinalize(destination) ? NSData(data: data) : nil}

为实现这个功能,我们首先需要在文件头部添加 ImageIO 和 MobileCoreServices 这两个系统库。第一部分 guard 语句用于确保GIF数据存在,images 即GIF动图中每一帧的静态图片,类型为 [UIImage]? ;第二部分取得图片总张数以及每一帧的持续时间。CGImageDestination 对象是对数据写入操作的抽象,Kingfisher 用其实现对GIF数据的保存。CGImageDestinationCreateWithData 指定了图片数据的保存位置、数据类型以及图片的总张数,最后一个参数现需传入 nil。CGImageDestinationSetProperties 用于传入包含静态图的通用配置参数,此处传入了Gif动图的重复播放次数。CGImageDestinationAddImage 用于添加每一张静态图片的数据以及对应的属性,此处添加了每张图片的持续时间。CGImageDestinationFinalize 需要在所有数据被写入后调用,成功返回 true,失败则返回 false。

开始下载任务

上次说到了downloadAndCacheImage这个方法,看名字就知道既要下载图片又要缓存图片,它的方法体是这样的:

 @discardableResult
    func downloadAndCacheImage(with url: URL,
                             forKey key: String,
                      retrieveImageTask: RetrieveImageTask,
                          progressBlock: DownloadProgressBlock?,
                      completionHandler: CompletionHandler?,
                                options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
    {
        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

downLoader 调用了downloadImage方法然后在completionHandler这个完成闭包中做缓存相关的操作,我们先不管缓存,先去downloadImage(downloader是它的一个实例)里看看downloadImage这个方法,它是长这样的:

 @discardableResult
    open func downloadImage(with url: URL,
                       retrieveImageTask: RetrieveImageTask? = nil,
                       options: KingfisherOptionsInfo? = nil,
                       progressBlock: ImageDownloaderProgressBlock? = nil,
                       completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
    {
        if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
            completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
            return nil
        }

        // 设置请求超时时间
        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.
        // 创建request 忽略本地和远程的缓存数据,直接从原始地址下
        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 possiblility 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(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
                dataTask.resume()
                self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)

                // Hold self while the task is executing.
                self.sessionHandler.downloadHolder = self
            }

            fetchLoad.downloadTaskCount  = 1
            downloadTask = fetchLoad.downloadTask

            retrieveImageTask?.downloadTask = downloadTask
        }
        return downloadTask
    }

里面有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()
        }
    }

这个fetchLoads是一个以URL为键,ImageFetchLoad为值的Dictionary,ImageFetchLoad是ImageDownloader中的一个内部类,它的声明如下

 class ImageFetchLoad {
        var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
        var responseData = NSMutableData()

        var downloadTaskCount = 0
        var downloadTask: RetrieveImageDownloadTask?
        var cancelSemaphore: DispatchSemaphore?
    }

//先是用图片的URL去self.fetchLoads里取对应的ImageFetchLoad, 如果没有的话就以当前URL为键创建一个,然后把传过来的progressBlock和completionHandler打包成一个元组,和options组成新元素, 添加到ImageFetchLoad里的contents数组中, 准备好之后,在闭包里面开始下载

 if fetchLoad.downloadTask == nil {
                let dataTask = session.dataTask(with: request)

                fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)

                dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
                dataTask.resume()
                self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)

                // Hold self while the task is executing.
                self.sessionHandler.downloadHolder = self
            }

            fetchLoad.downloadTaskCount  = 1
            downloadTask = fetchLoad.downloadTask

            retrieveImageTask?.downloadTask = downloadTask

这里使用了NSURLSession,是iOS7之后比较主流的用于网络请求的API(iOS7以前多使用NSURLConnection)
ImageDownloaderSessionHandler 实现URLSessionDataDelegate 代理

 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

        guard let downloader = downloadHolder else {
            return
        }


        if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
            //向fetchLoads[URL].responseData添加一条响应数据
            fetchLoad.responseData.append(data)

            if let expectedLength = dataTask.response?.expectedContentLength {
                for content in fetchLoad.contents {
                    //依次调用fetchLoads的contents中的所有过程回调
                    DispatchQueue.main.async {
                        content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
                    }
                }
            }
        }
    }

这个函数会在接收到数据的时候被调用

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {

        guard let url = task.originalRequest?.url else {
            return
        }

        guard error == nil else {
            callCompletionHandlerFailure(error: error!, url: url)
            return
        }
        // 处理加工
        processImage(for: task, url: url)
    }

这个方法是在请求完成之后调用
其中processImage 具体是

private func processImage(for task: URLSessionTask, url: URL) {

        guard let downloader = downloadHolder else {
            return
        }

        // We are on main queue when receiving this.
        // 下载完成后处理
        downloader.processQueue.async {

            guard let fetchLoad = downloader.fetchLoad(for: url) else {
                return
            }

            self.cleanFetchLoad(for: url) // 清除旧的url

            let data: Data?
            let fetchedData = fetchLoad.responseData as Data

            if let delegate = downloader.delegate {
                data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url)
            } else {
                data = fetchedData
            }

            // Cache the processed images. So we do not need to re-process the image if using the same processor.
            // Key is the identifier of processor.
            var imageCache: [String: Image] = [:]
            for content in fetchLoad.contents {

                let options = content.options
                let completionHandler = content.callback.completionHandler
                let callbackQueue = options.callbackDispatchQueue

                let processor = options.processor

                var image = imageCache[processor.identifier]
                if let data = data, image == nil { // 将data 转成image
                    image = processor.process(item: .data(data), options: options)
                    // Add the processed image to cache. 
                    // If `image` is nil, nothing will happen (since the key is not existing before).
                    imageCache[processor.identifier] = image
                }

                if let image = image {
                    //下载完成后可以进行的自定义操作,用户可以自行指定delegate
                    downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)

                    if options.backgroundDecode {
                        let decodedImage = image.kf.decoded
                        callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
                    } else {
                        callbackQueue.safeAsync { completionHandler?(image, nil, url, data) }
                    }

                } else {
                    //不能生成图片,返回304状态码,表示图片没有更新,可以直接使用缓存
                    if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
                        let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil)
                        completionHandler?(nil, notModified, url, nil)
                        continue
                    }
                    //不能生成图片,报BadData错误
                    let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
                    callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
                }
            }
        }
    }

主要的委托方法都看完了,最后还有一个跟身份认证有关的:

 func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard let downloader = downloadHolder else {
            return
        }

        downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler)
    }

  func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        //一般用于SSL/TLS协议(https)
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            //在白名单中的域名做特殊处理,忽视警告
            if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
                let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
                completionHandler(.useCredential, credential)
                return
            }
        }
        //默认处理
        completionHandler(.performDefaultHandling, nil)
    }

trustedHosts是ImageDownloader里声明的一个字符串集合,应该就是类似于一个白名单,放到里面的域名是可以信任的。

缓存状态监控

缓存状态监控是缓存管理当中很有用的工具,主要包含:

  • 查询某图片是否存在于缓存中
  • 查询某图片的缓存文件名
  • 查询当前缓存所占硬盘空间大小

代码如下:

// MARK: - Check cache statuspublic extension ImageCache { /** * Cache result for checking whether an image is cached for a key. */ public struct CacheCheckResult { public let cached: Bool public let cacheType: CacheType? } /** Check whether an image is cached for a key. - parameter key: Key for the image. - returns: The check result. */ public func isImageCachedForKey(key: String) -> CacheCheckResult { if memoryCache.objectForKey != nil { return CacheCheckResult(cached: true, cacheType: .Memory) } let filePath = cachePathForKey if fileManager.fileExistsAtPath { return CacheCheckResult(cached: true, cacheType: .Disk) } return CacheCheckResult(cached: false, cacheType: nil) } /** Get the hash for the key. This could be used for matching files. - parameter key: The key which is used for caching. - returns: Corresponding hash. */ public func hashForKey(key: String) -> String { return cacheFileNameForKey } /** Calculate the disk size taken by cache. It is the total allocated size of the cached files in bytes. - parameter completionHandler: Called with the calculated size when finishes. */ public func calculateDiskCacheSizeWithCompletionHandler(completionHandler: ((size: UInt) ->  { dispatch_async(ioQueue, { () -> Void in let diskCacheURL = NSURL(fileURLWithPath: self.diskCachePath) let resourceKeys = [NSURLIsDirectoryKey, NSURLTotalFileAllocatedSizeKey] var diskCacheSize: UInt = 0 if let fileEnumerator = self.fileManager.enumeratorAtURL(diskCacheURL, includingPropertiesForKeys: resourceKeys, options: NSDirectoryEnumerationOptions.SkipsHiddenFiles, errorHandler: nil) { for fileURL in fileEnumerator.allObjects as! [NSURL] { do { let resourceValues = try fileURL.resourceValuesForKeys(resourceKeys) // If it is a Directory. Continue to next file URL. if let isDirectory = resourceValues[NSURLIsDirectoryKey]?.boolValue { if isDirectory { continue } } if let fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey] as? NSNumber { diskCacheSize  = fileSize.unsignedLongValue } } catch _ { } } } dispatch_async(dispatch_get_main_queue -> Void in if let completionHandler = completionHandler { completionHandler(size: diskCacheSize) } }) }) }}

前两个都是简单的方法调用,不需赘述;计算当前缓存所占空间与前面删除过期超量缓存的相关操作极为相似,基本流程为:一、切换线程,二、遍历文件,三、累加文件大小,四、切回主线程执行 completionHandler。

KingfisherOptions 文件包含了对 Kingfisher 操作部分设置参数,其本身并没有太多可讲的,但其代码内用到了 Swift2.0 中所引入的一个新类型 OptionSetType,这段代码可以看做是 OptionSetType 的基本用法引导,具体内容如下:

public struct KingfisherOptions : OptionSetType { public let rawValue: UInt public init(rawValue: UInt) { self.rawValue = rawValue } /// None options. Kingfisher will keep its default behavior. public static let None = KingfisherOptions(rawValue: 0) /// Download in a low priority. public static let LowPriority = KingfisherOptions(rawValue: 1 << 0) /// Try to send request to server first. If response code is 304 (Not Modified), use the cached image. Otherwise, download the image and cache it again. public static var ForceRefresh = KingfisherOptions(rawValue: 1 << 1) /// Only cache downloaded image to memory, not cache in disk. public static var CacheMemoryOnly = KingfisherOptions(rawValue: 1 << 2) /// Decode the image in background thread before using. public static var BackgroundDecode = KingfisherOptions(rawValue: 1 << 3) /// If set it will dispatch callbacks asynchronously to the global queue DISPATCH_QUEUE_PRIORITY_DEFAULT. Otherwise it will use the queue defined at KingfisherManager.DefaultOptions.queue public static var BackgroundCallback = KingfisherOptions(rawValue: 1 << 4) /// Decode the image using the same scale as the main screen. Otherwise it will use the same scale as defined on the KingfisherManager.DefaultOptions.scale. public static var ScreenScale = KingfisherOptions(rawValue: 1 << 5)}

除第一位以外,rawValue的每一个二进制位都代表一个单独的配置参数,这样直接通过判断 rawValue 的值,就能知道哪些选项是被选中的,比如,若 rawValue == 10,其二进制位后四位为1010,即可知道 BackgroundDecode 以及 ForceRefresh 被选中,当然你也可以直接使用 OptionSetType 协议所提供的 contains 函数来判定某选项是否被包含。另外说一点,Kingfisher 这里的 OptionSetType 的用法并不标准,更为常见的用法是不设置 rawValue == 0 时所对应的参数,这样每个配置参数就正好对应一位二进制位了,也更容易理解。

public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> ())
for fileURL in URLsToDelete {
    do {
        try self.fileManager.removeItemAtURL(fileURL)
    } catch _ {
    }
}

GCD相关

我们在 Kingfisher 的源码中,经常可以看到 completionHandler 的存在,这个闭包将在所有操作完成后调用,多用于做结果的处理;但有时候我们为了调用 completionHandler 所要面对的问题要比 Kingfisher 中所涉及到的要复杂的多,比如,我们需要在一个线程执行完分多次提交的多个异步闭包之后调用某个函数或者执行另一个闭包,这样的话,我们就不能像 Kingfisher 里这样单纯的将 completionHandler 放在闭包尾了事了,GCD 提供了先进的特性来解决我们的这种需求,代码如下:

dispatch_group_t group = dispatch_group_create();dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ { // block1 NSLog(@"Block1"); [NSThread sleepForTimeInterval:5.0]; NSLog(@"Block1 End");});dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ { // block2 NSLog(@"Block2"); [NSThread sleepForTimeInterval:8.0]; NSLog(@"Block2 End");});dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ { // block3 NSLog(@"Block3");});dispatch_release;

这是其中一种比较简洁的实现方法,我们先创建一个 dispatch_group_t 实例,通过使用 dispatch_group_async 为其提交异步闭包任务,当这个 group 处理完所有的闭包任务之后,dispatch_group_notify 才会被调用,你就可以把你需要最后执行的 completionHandler 放置在这个地方。

实际上是一个闭包类型,具体会在什么时候调用待会儿会看到。第四个参数类型CompletionHandler也一样是个闭包类型的别名:

  • 一些准备工作,取缓存路径,过期时间等:

缓存的添加与删除

缓存的添加分为三步:写入内存、写入硬盘、执行 completionHandler,其中写入硬盘操作略复杂,代码如下:

 public func storeImage(image: UIImage, originalData: NSData? = nil, forKey key: String, toDisk: Bool, completionHandler:  ->  { memoryCache.setObject(image, forKey: key, cost: image.kf_imageCost) func callHandlerInMainQueue() { if let handler = completionHandler { dispatch_async(dispatch_get_main_queue { handler() } } } if toDisk { dispatch_async(ioQueue, { () -> Void in let imageFormat: ImageFormat if let originalData = originalData { imageFormat = originalData.kf_imageFormat } else { imageFormat = .Unknown } let data: NSData? switch imageFormat { case .PNG: data = UIImagePNGRepresentation case .JPEG: data = UIImageJPEGRepresentation(image, 1.0) case .GIF: data = UIImageGIFRepresentation case .Unknown: data = originalData } if let data = data { if !self.fileManager.fileExistsAtPath(self.diskCachePath) { do { try self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil) } catch _ {} } self.fileManager.createFileAtPath(self.cachePathForKey, contents: data, attributes: nil) callHandlerInMainQueue() } else { callHandlerInMainQueue } else { callHandlerInMainQueue() } }

写入内存操作非常简单,直接调用 NSCache 实例的 setObject 即可,kf_imageCost 为图片的宽乘高乘比例平方的整数值。callHandlerInMainQueue 为定义的嵌套函数,调用后在主线程上执行 completionHandler。我们利用 GCD 将硬盘的读写操作放置于 ioQueue 中执行,这里我要多说两句,其实在这个地方我是很不理解的,我的观点是这样的:

首先,对于 PNG、JPEG 格式的图片:当需要展示的时候,我们从网络中获取到的数据直接可以被 UIImage 的初始化方法识别,所以上文中所提到的 decode 函数并不必要,取到的数据可以直接用来展示;当需要保存的时候,我们可以直接保存当初下载到的网络数据,而不是像上述代码一样,先根据网络数据判断图片类型,再调用对应的 UIImageRepresentation 将成品图片拆分成数据再保存,我个人认为这是多此一举的。在测试中,我截取了同一张图片的 originalData 与拆分成品图片后的 data 作对比,结果如下:

彩世界开奖app官网 2originalData.png彩世界开奖app官网 3data.png

可以看到内容基本相同,而且我将两个 UIImageRepresentation 函数替换为 originalData 后,结果并无不同;此外,若 originalData 为 nil,imageFormat = .Unknown,在后续的代码中 case .Unknown: data = originalData 语句直接将 data 置空,说明了在有图片传入的情况下,originalData 不可为空,不存在只有图片没有数据,必须通过解码得到数据的情况。所以我依此认为,这段代码中 UIImagePNGRepresentation 以及 UIImageJPEGRepresentation 的使用是完全没有必要的,如果前辈们有不同意见,本人愿意接受批评指正。其次,对于 GIF 格式的图片:当需要展示的时候,我认为 kf_animatedImageWithGIFData(gifData data: NSData, scale: CGFloat, duration: NSTimeInterval) -> UIImage? 函数的存在还是有必要的,因为 UIImage 的初始化方法并不能直接处理GIF数据,而 UIImage.animatedImageWithImages 方法所接受的参数也并不是 NSData 而是 [UIImage]。但当需要保存的时候,我同样质疑 UIImageGIFRepresentation 函数存在的必要性,我在翻阅 ImageDownloader 源码的时候发现,若所获得的是GIF数据,kf_animatedImageWithGIFData 也可以直接处理将其转换成GIF动图,即表示,得到的网络数据(也就是这里的 originalData),是可以被直接识别的,并不需要调用 UIImageGIFRepresentation 来把依照 originalData 生成的GIF动图拆分成 data 再保存,而是直接保存 originalData 即可。

继续回到代码中,剩下的操作就非常简单了,在取到数据的情况下,若文件目录不存在,先生成目录再保存文件,最后调用 completionHandler。

删除操作十分简单,同样分三步:删除内存缓存、依照路径删除硬盘缓存、执行 completionHandler,代码如下:

 public func removeImageForKey(key: String, fromDisk: Bool, completionHandler:  ->  { memoryCache.removeObjectForKey func callHandlerInMainQueue() { if let handler = completionHandler { dispatch_async(dispatch_get_main_queue { handler() } } } if fromDisk { dispatch_async(ioQueue, { () -> Void in do { try self.fileManager.removeItemAtPath(self.cachePathForKey } catch _ {} callHandlerInMainQueue } else { callHandlerInMainQueue() } }

memoryCache.removeObjectForKey 删除内存缓存,self.fileManager.removeItemAtPath 删除硬盘缓存。

分析从缓存中获取 tryToRetrieveImageFromCache

Kingfisher中还用到了很多小技巧,比如对关联对象(Associated Object)的使用,解决了extension不能扩展存储属性的问题:

缓存的读取

缓存的读取所完成的操作也十分简单,首先确保 completionHandler 不为空,之后分别尝试从内存和硬盘中读取缓存,若缓存只存在于硬盘中,读取后,我们将其添加到内存中,代码如下:

extension ImageCache { /** Get an image for a key from memory or disk. - parameter key: Key for the image. - parameter options: Options of retrieving image. - 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. */ public func retrieveImageForKey(key: String, options:KingfisherManager.Options, completionHandler: ((UIImage?, CacheType!) ->  -> RetrieveImageDiskTask? { // No completion handler. Not start working and early return. guard let completionHandler = completionHandler else { return nil } var block: RetrieveImageDiskTask? if let image = self.retrieveImageInMemoryCacheForKey { //Found image in memory cache. if options.shouldDecode { dispatch_async(self.processQueue, { () -> Void in let result = image.kf_decodedImage(scale: options.scale) dispatch_async(options.queue, { () -> Void in completionHandler(result, .Memory) }) }) } else { completionHandler(image, .Memory) } } else { var sSelf: ImageCache! = self block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) { // Begin to load image from disk dispatch_async(sSelf.ioQueue, { () -> Void in if let image = sSelf.retrieveImageInDiskCacheForKey(key, scale: options.scale) { if options.shouldDecode { dispatch_async(sSelf.processQueue, { () -> Void in let result = image.kf_decodedImage(scale: options.scale) sSelf.storeImage(result!, forKey: key, toDisk: false, completionHandler: nil) dispatch_async(options.queue, { () -> Void in completionHandler(result, .Memory) sSelf = nil }) }) } else { sSelf.storeImage(image, forKey: key, toDisk: false, completionHandler: nil) dispatch_async(options.queue, { () -> Void in completionHandler(image, .Disk) sSelf = nil }) } } else { // No image found from either memory or disk dispatch_async(options.queue, { () -> Void in completionHandler sSelf = nil }) } }) } dispatch_async(dispatch_get_main_queue(), block!) } return block } /** Get an image for a key from memory. - parameter key: Key for the image. - returns: The image object if it is cached, or `nil` if there is no such key in the cache. */ public func retrieveImageInMemoryCacheForKey(key: String) -> UIImage? { return memoryCache.objectForKey as? UIImage } /** Get an image for a key from disk. - parameter key: Key for the image. - param scale: The scale factor to assume when interpreting the image data. - returns: The image object if it is cached, or `nil` if there is no such key in the cache. */ public func retrieveImageInDiskCacheForKey(key: String, scale: CGFloat = KingfisherManager.DefaultOptions.scale) -> UIImage? { return diskImageForKey(key, scale: scale) }}

这里主要说两点,第一,若提交的 block 将异步执行的话,DISPATCH_BLOCK_INHERIT_QOS_CLASS 需要被传入,同步执行则应传入 DISPATCH_BLOCK_ENFORCE_QOS_CLASS。第二,dispatch_async(dispatch_get_main_queue(), block!) 之中,若将 async 改为 sync, 而这段代码又执行于主线程上时,必然会导致死锁,或者说,在当前线程上调用 dispatch_sync 方法给自身线程分配任务,则必然会导致死锁。因为 dispatch_sync 需要等待内部操作执行完成后才会返回,进而释放当前线程,而如果内部操作又分配在自身线程上时,若自身不释放,内部的操作就会一直等待,就会出现不返回不释放,不释放不执行更不会返回的死锁。而使用 dispatch_async 则不会出现这种问题,因为 dispatch_async 方法不必等待内部操作完成便直接返回,释放当前线程后,block 内部的操作便可以开始执行。

        //不强制刷新则从缓存中取
  func tryToRetrieveImageFromCache(forKey key: String,
                                       with url: URL,
                              retrieveImageTask: RetrieveImageTask,
                                  progressBlock: DownloadProgressBlock?,
                              completionHandler: CompletionHandler?,
                                        options: KingfisherOptionsInfo)
    {


        let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
            completionHandler?(image, error, cacheType, imageURL)
        }

        func handleNoCache() {
            if options.onlyFromCache {
                let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
                diskTaskCompletionHandler(nil, error, .none, url)
                return
            }
            self.downloadAndCacheImage(
                with: url,
                forKey: key,
                retrieveImageTask: retrieveImageTask,
                progressBlock: progressBlock,
                completionHandler: diskTaskCompletionHandler,
                options: options)

        }

        let targetCache = options.targetCache
        // First, try to get the exactly image from cache
        targetCache.retrieveImage(forKey: key, options: options) { image, cacheType in
            // If found, we could finish now.
            if image != nil {
                diskTaskCompletionHandler(image, nil, cacheType, url)
                return
            }

            // If not found, and we are using a default processor, download it!
            let processor = options.processor
            guard processor != DefaultImageProcessor.default else {
                handleNoCache()
                return
            }

            // If processor is not the default one, we have a chance to check whether
            // the original image is already in cache.
            let optionsWithoutProcessor = options.removeAllMatchesIgnoringAssociatedValue(.processor(processor))
            targetCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { image, cacheType in
                // If we found the original image, there is no need to download it again.
                // We could just apply processor to it now.
                guard let image = image else {
                    handleNoCache()
                    return
                }

                guard let processedImage = processor.process(item: .image(image), options: options) else {
                    diskTaskCompletionHandler(nil, nil, .none, url)
                    return
                }
                targetCache.store(processedImage,
                                  original: nil,
                                  forKey: key,
                                  processorIdentifier:options.processor.identifier,
                                  cacheSerializer: options.cacheSerializer,
                                  toDisk: !options.cacheMemoryOnly,
                                  completionHandler: nil)
                diskTaskCompletionHandler(processedImage, nil, .none, url)
            }
        }
    }

它们虽然是全局的,但因为访问权限是private所以只能在当前文件内使用。这段代码思路很清晰,就是通过读取图片数据的头几个字节然后和对应图片格式标准进行比对。对图片格式感兴趣的同学可以看看这篇文章——移动端图片格式调研,作者是最近风头正劲的YYKit的作者ibireme。

MD5加密

MD5加密在 Kingfisher 中被用于缓存时对文件名的加密,由于其内部实现较为复杂,此处仅提供成品代码以备不时之需,代码如下:

import Foundationextension String { func kf_MD5() -> String { if let data = dataUsingEncoding(NSUTF8StringEncoding) { let MD5Calculator = MD5 let MD5Data = MD5Calculator.calculate() let resultBytes = UnsafeMutablePointer<CUnsignedChar>(MD5Data.bytes) let resultEnumerator = UnsafeBufferPointer<CUnsignedChar>(start: resultBytes, count: MD5Data.length) var MD5String = "" for c in resultEnumerator { MD5String  = String(format: "x", c) } return MD5String } else { return self } }}/** array of bytes, little-endian representation */func arrayOfBytes<T>(value:T, length:Int? = nil) -> [UInt8] { let totalBytes = length ?? (sizeofValue * 8) let valuePointer = UnsafeMutablePointer<T>.alloc valuePointer.memory = value let bytesPointer = UnsafeMutablePointer<UInt8>(valuePointer) var bytes = [UInt8](count: totalBytes, repeatedValue: 0) for j in 0..<min,totalBytes) { bytes[totalBytes - 1 - j] = (bytesPointer   j).memory } valuePointer.destroy() valuePointer.dealloc return bytes}extension Int { /** Array of bytes with optional padding (little-endian) */ func bytes(totalBytes: Int = sizeof -> [UInt8] { return arrayOfBytes(self, length: totalBytes) } }extension NSMutableData { /** Convenient way to append bytes */ func appendBytes(arrayOfBytes: [UInt8]) { appendBytes(arrayOfBytes, length: arrayOfBytes.count) } }class HashBase { var message: NSData init(_ message: NSData) { self.message = message } /** Common part for hash calculation. Prepare header data. */ func prepare(len:Int = 64) -> NSMutableData { let tmpMessage: NSMutableData = NSMutableData(data: self.message) // Step 1. Append Padding Bits tmpMessage.appendBytes // append one bit (UInt8 with one bit) to message // append "0" bit until message length in bits ≡ 448  var msgLength = tmpMessage.length; var counter = 0; while msgLength % len !=  { counter   msgLength   } let bufZeros = UnsafeMutablePointer<UInt8>(calloc(counter, sizeof tmpMessage.appendBytes(bufZeros, length: counter) bufZeros.destroy() bufZeros.dealloc return tmpMessage }}func rotateLeft(v:UInt32, n:UInt32) -> UInt32 { return ((v << n) & 0xFFFFFFFF) | (v >> }class MD5 : HashBase { /** specifies the per-round shift amounts */ private let s: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] /** binary integer part of the sines of integers  */ private let k: [UInt32] = [0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee, 0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501, 0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be, 0x6b901122,0xfd987193,0xa679438e,0x49b40821, 0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa, 0xd62f105d,0x2441453,0xd8a1e681,0xe7d3fbc8, 0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed, 0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a, 0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c, 0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70, 0x289b7ec6,0xeaa127fa,0xd4ef3085,0x4881d05, 0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665, 0xf4292244,0x432aff97,0xab9423a7,0xfc93a039, 0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1, 0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1, 0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391] private let h:[UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] func calculate() -> NSData { let tmpMessage = prepare() // hash values var hh = h // Step 2. Append Length a 64-bit representation of lengthInBits let lengthInBits = (message.length * 8) let lengthBytes = lengthInBits.bytes tmpMessage.appendBytes(Array(lengthBytes.reverse; // Process the message in successive 512-bit chunks: let chunkSizeBytes = 512 / 8 // 64 var leftMessageBytes = tmpMessage.length for (var i = 0; i < tmpMessage.length; i = i   chunkSizeBytes, leftMessageBytes -= chunkSizeBytes) { let chunk = tmpMessage.subdataWithRange(NSRange(location: i, length: min(chunkSizeBytes,leftMessageBytes))) // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 var M:[UInt32] = [UInt32](count: 16, repeatedValue: 0) let range = NSRange(location:0, length: M.count * sizeof chunk.getBytes(UnsafeMutablePointer<Void>, range: range) // Initialize hash value for this chunk: var A:UInt32 = hh[0] var B:UInt32 = hh[1] var C:UInt32 = hh[2] var D:UInt32 = hh[3] var dTemp:UInt32 = 0 // Main loop for j in 0..<k.count { var g = 0 var F:UInt32 = 0 switch  { case 0...15: F = (B & C) |  & D) g = j break case 16...31: F = (D & B) | (~D & C) g = (5 * j   1) % 16 break case 32...47: F = B ^ C ^ D g = (3 * j   5) % 16 break case 48...63: F = C ^  g =  % 16 break default: break } dTemp = D D = C C = B B = B &  rotateLeft((A &  F &  k[j] &  M[g]), n: s[j]) A = dTemp } hh[0] = hh[0] &  A hh[1] = hh[1] &  B hh[2] = hh[2] &  C hh[3] = hh[3] &  D } let buf: NSMutableData = NSMutableData(); hh.forEach -> () in var i:UInt32 = item.littleEndian buf.appendBytes(&i, length: sizeofValue return buf.copy() as! NSData; }}

上述代码为 String 添加了 kf_MD5 拓展方法,返回值也为 String,只需对需要加密的 String 调用该拓展方法,即可得到对应的加密字符串。

KingfisherManager 是个单利, swift 创建单利十分简单

Kingfisher 是由 @onevcat 编写的用于下载和缓存网络图片的轻量级Swift工具库,其中涉及到了包括GCD、Swift高级语法、缓存、硬盘读写、网络编程、图像编码、图形绘制、Gif数据生成和处理、MD5、Associated Objects的使用等大量iOS开发知识。

public class RetrieveImageTask {
    public static let empty = RetrieveImageTask()
    var cancelledBeforeDownloadStarting: Bool = false
    public var downloadTask: RetrieveImageDownloadTask?

    /**
    Cancel current task. If this task is already done, do nothing.
    */
    public func cancel() {
        if let downloadTask = downloadTask {
            downloadTask.cancel()
        } else {
            cancelledBeforeDownloadStarting = true
        }
    }
}
if toDisk {
    dispatch_async(ioQueue, { () -> Void in
        let imageFormat: ImageFormat
        //取图片数据
        if let originalData = originalData {
            //解析图片格式
            imageFormat = originalData.kf_imageFormat
        } else {
            imageFormat = .Unknown
        }

        let data: NSData?
        switch imageFormat {
        case .PNG: data = UIImagePNGRepresentation(image)
        case .JPEG: data = UIImageJPEGRepresentation(image, 1.0)
        case .GIF: data = UIImageGIFRepresentation(image)
            //若originalData为nil,重绘图片后解析成PNG数据
        case .Unknown: data = originalData ?? UIImagePNGRepresentation(image.kf_normalizedImage())
        }

        if let data = data {
            //如果目录不存在则创建一个目录
            if !self.fileManager.fileExistsAtPath(self.diskCachePath) {
                do {
                    try self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
                } catch _ {}
            }

            //创建图片文件
            self.fileManager.createFileAtPath(self.cachePathForKey(key), contents: data, attributes: nil)
            //在主线程执行回调(一般是UI操作吧)
            callHandlerInMainQueue()
        } else {
            callHandlerInMainQueue()
        }
    })
}

Kingfisher 源码中所包含的12个文件及其关系如上图所示,从左至右,由深及浅。UIImage Extension 文件内部对 UIImage 以及 NSData 进行了拓展, 包含判定图片类型、图片解码以及Gif数据处理等操作。String MD5 负责图片缓存时对文件名进行MD5加密操作。ImageCache 主要负责将加载过的图片缓存至本地。ImageDownloader 负责下载网络图片。KingfisherOptions 内含配置 Kingfisher 行为的部分参数,包括是否设置下载低优先级、是否强制刷新、是否仅缓存至内存、是否允许图像后台解码等设置。Resource 中的 Resource 结构体记录了图片的下载地址和缓存Key。ImageTransition 文件中的动画效果将在使用 UIImageView 的拓展 API 时被采用,其底层为UIViewAnimationOptions,此外你也可以自己传入相应地动画操作、完成闭包来配置自己的动画效果。ThreadHelper 中的 dispatch_async_safely_main_queue 函数接受一个闭包,利用 NSThread.isMainThread 判定并将其放置在主线程中执行。KingfisherManager 是 Kingfisher 的主控制类,整合了图片下载及缓存操作。KingfisherOptionsInfoItem 被提供给开发者对 Kingfisher 的各种行为进行控制,包含下载设置、缓存设置、动画设置以及 KingfisherOptions 中的全部配置参数。UIImage Kingfisher 以及 UIButton Kingfisher 对 UIImageView 和 UIButton 进行了拓展,即主要用于提供 Kingfisher 的外部接口。

这个枚举的每个枚举项都有关联值,包含了很多信息
TargetCache指定一个缓存器(ImageCache的一个实例),Downloader指定一个下载器(ImageDownloader的一个实例),Transition指定显示图片的动画效果(提供淡入和从上下左右进入这5种效果,也可以传入自定义效果)。
第三个参数类型是DownloadProgressBlock,也是一个别名:

至于pngHeaderjpgHeaderSOIjpgHeaderIFgifHeader这几个东西么,是几个常量:

这个kf_setImage显然是UIImageView的一个extension方法,既然是暴露出来供库的使用者调用的,应该就是抽象层面最高的。于是我command click进去看了一下,它长这个样子,有点长,让我们分析下

private let pngHeader: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
private let jpgHeaderSOI: [UInt8] = [0xFF, 0xD8]
private let jpgHeaderIF: [UInt8] = [0xFF]
private let gifHeader: [UInt8] = [0x47, 0x49, 0x46]
public enum KingfisherOptionsInfoItem {
    case targetCache(ImageCache)
    case downloader(ImageDownloader)
    case transition(ImageTransition)
    case downloadPriority(Float)
    case forceRefresh
    case forceTransition
    case cacheMemoryOnly
    case onlyFromCache
    case backgroundDecode
    case callbackDispatchQueue(DispatchQueue?)
    case scaleFactor(CGFloat)
    case preloadAllAnimationData
    case requestModifier(ImageDownloadRequestModifier)
    case processor(ImageProcessor)
    case cacheSerializer(CacheSerializer)
    case keepCurrentImageWhileLoading
    case onlyLoadFirstFrame
    case cacheOriginalImage
}

在下载完图片之后的完成闭包中(会在下载请求结束后调用),如果服务器返回状态码304,说明服务器图片未更新,我们可以从缓存中取得图片数据,就是调用targetCache(ImageCache的一个实例)retrieveImageForKey。我们进入到ImageCache中看看这个方法的具体逻辑:

第一个参数 resource Resource协议里面包含了两个属性,cacheKey和downloadURL,cacheKey就是原URL的完整字符串,之后会作为缓存的键使用(内存缓存直接使用cacheKey作为NSCache的键,文件缓存把cacheKey进行MD5加密后的字符串作为缓存文件名)
第二个参数类型KingfisherOptionsInfo?是什么呢?它是一个类型别名:public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem],而KingfisherOptionsInfoItem是一个enum:

ImageCache中还有一个删除过期缓存的方法cleanExpiredDiskCacheWithCompletionHander,我觉得也挺关键的,来看一下它的具体逻辑吧:

public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> ())
dispatch_async(dispatch_get_main_queue(), { () -> Void in

    //将已删除的所有文件名进行广播
    if URLsToDelete.count != 0 {

        let cleanedHashes = URLsToDelete.map({ (url) -> String in
            return url.lastPathComponent!
        })

        NSNotificationCenter.defaultCenter().postNotificationName(KingfisherDidCleanDiskCacheNotification, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
    }

    if let completionHandler = completionHandler {
        completionHandler()
    }
})
      let url = URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/masterkingfisher-(indexPath.row   1).jpg")!

        _ = (cell as! CollectionViewCell).cellImageView.kf.setImage(with: url,
                                           placeholder: nil,
                                           options: [.transition(ImageTransition.fade(1))],
                                           progressBlock: { receivedSize, totalSize in
                                            print("(indexPath.row   1): (receivedSize)/(totalSize)")
            },
                                           completionHandler: { image, error, cacheType, imageURL in
                                            print("(indexPath.row   1): Finished")
        })
//全局变量,用来作为关联对象(估计是因为extension里面不能添加储存属性,只能通过关联对象配合计算属性和方法的方式来hack)
// MARK: - Associated Object
private var lastURLKey: Void?
...
public extension UIImageView {
    /// Get the image URL binded to this image view.
    public var kf_webURL: NSURL? {
        return objc_getAssociatedObject(self, &lastURLKey) as? NSURL
    }
    private func kf_setWebURL(URL: NSURL) {
        objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}
//如果内存中有缓存,则直接从内存中读取图片
if let image = self.retrieveImageInMemoryCacheForKey(key) {

    //Found image in memory cache.
    if options.shouldDecode {
        dispatch_async(self.processQueue, { () -> Void in
            let result = image.kf_decodedImage(scale: options.scale)
            dispatch_async(options.queue, { () -> Void in
                completionHandler(result, .Memory)
            })
        })
    } else {
        completionHandler(image, .Memory)
    }
}

最后还是小结一下知识点吧:

//内存缓存,memoryCache是一个NSCache,cost是图片尺寸(像素)
memoryCache.setObject(image, forKey: key, cost: image.kf_imageCost)
  • 给完成闭包进行解包,若为空则提前返回:
//遍历缓存文件,跳过隐藏文件
if let fileEnumerator = self.fileManager.enumeratorAtURL(diskCacheURL, includingPropertiesForKeys: resourceKeys, options: NSDirectoryEnumerationOptions.SkipsHiddenFiles, errorHandler: nil),
        urls = fileEnumerator.allObjects as? [NSURL] {
    for fileURL in urls {

        do {
            let resourceValues = try fileURL.resourceValuesForKeys(resourceKeys)
            //跳过目录
            // If it is a Directory. Continue to next file URL.
            if let isDirectory = resourceValues[NSURLIsDirectoryKey] as? NSNumber {
                if isDirectory.boolValue {
                    continue
                }
            }
            //若文件最新更新日期超过过期日期,则放入待删除队列
            // If this file is expired, add it to URLsToDelete
            if let modificationDate = resourceValues[NSURLContentModificationDateKey] as? NSDate {
                if modificationDate.laterDate(expiredDate) == expiredDate {
                    URLsToDelete.append(fileURL)
                    continue
                }
            }

            if let fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
                diskCacheSize  = fileSize.unsignedLongValue
                cachedFiles[fileURL] = resourceValues
            }
        } catch _ {
        }

    }
}
func downloadAndCacheImageWithURL(URL: NSURL,
    forKey key: String,
    retrieveImageTask: RetrieveImageTask,
    progressBlock: DownloadProgressBlock?,
    completionHandler: CompletionHandler?,
    options: Options,
    targetCache: ImageCache,
    downloader: ImageDownloader)
{
    //下载图片
    downloader.downloadImageWithURL(URL, retrieveImageTask: retrieveImageTask, options: options,
        progressBlock: { receivedSize, totalSize in
            progressBlock?(receivedSize: receivedSize, totalSize: totalSize)
        },
        completionHandler: { image, error, imageURL, originalData in
            //304 NOT MODIFIED,尝试从缓存中取数据
            if let error = error where 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.retrieveImageForKey(key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
                    completionHandler?(image: cacheImage, error: nil, cacheType: cacheType, imageURL: URL)

                })
                return
            }

            if let image = image, originalData = originalData {
                targetCache.storeImage(image, originalData: originalData, forKey: key, toDisk: !options.cacheMemoryOnly, completionHandler: nil)
            }

            completionHandler?(image: image, error: error, cacheType: .None, imageURL: URL)
        }
    )
}
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
      return nil
}
  • 在主线程广播已删除的缓存图片,如果有传入完成闭包的话,就调用它:

整个缓存逻辑就是这样,这里有一个用来解析图片格式的属性kf_imageFormat,它是NSData的一个扩展属性:

//若当前缓存内容超过预设的最大缓存尺寸,则先将文件根据时间排序(旧的在前),然后开始循环删除,直到尺寸降到最大缓存尺寸的一半。
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[NSURLContentModificationDateKey] as? NSDate, let date2 = resourceValue2[NSURLContentModificationDateKey] as? NSDate {
        return date1.compare(date2) == .OrderedAscending
        }
        */
        if let date1 = resourceValue1[NSURLContentModificationDateKey] as? NSDate {
            if let date2 = resourceValue2[NSURLContentModificationDateKey] as? NSDate {
                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.removeItemAtURL(fileURL)
        } catch {

        }

        URLsToDelete.append(fileURL)

        if let fileSize = cachedFiles[fileURL]?[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
            diskCacheSize -= fileSize.unsignedLongValue
        }

        if diskCacheSize < targetSize {
            break
        }
    }
}
  • 缓存到内存:
//会在回调中置空(为了避免retain cycle?)
var sSelf: ImageCache! = self
//创建一个调度对象块(可以使用dispatch_block_cancle(block)在对象块执行前取消对象块),DISPATCH_BLOCK_INHERIT_QOS_CLASS这个flag表明块从它进入的队列那里继承Qos等级
block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {

    // Begin to load image from disk
    dispatch_async(sSelf.ioQueue, { () -> Void in
        //通过key从文件中获取缓存图片
        if let image = sSelf.retrieveImageInDiskCacheForKey(key, scale: options.scale) {
            //需要先解码
            if options.shouldDecode {
                dispatch_async(sSelf.processQueue, { () -> Void in
                    let result = image.kf_decodedImage(scale: options.scale)
                    sSelf.storeImage(result!, forKey: key, toDisk: false, completionHandler: nil)

                    dispatch_async(options.queue, { () -> Void in
                        completionHandler(result, .Memory)
                        sSelf = nil
                    })
                })
            } else {
                sSelf.storeImage(image, forKey: key, toDisk: false, completionHandler: nil)
                dispatch_async(options.queue, { () -> Void in
                    completionHandler(image, .Disk)
                    sSelf = nil
                })
            }
        } else {
            // No image found from either memory or disk
            dispatch_async(options.queue, { () -> Void in
                completionHandler(nil, nil)
                sSelf = nil
            })
        }
    })
}
//调度到主线程队列,retrieveImageForKey函数本身是在主线程中的,所以block会在retrieveImageForKey返回之后执行,而在执行之前,还可以被取消。
dispatch_async(dispatch_get_main_queue(), block!)
  • 如果内存中有缓存则直接从内存中取图片;再判断图片是否需要解码,若需要,则先解码再调用完成闭包,否则直接调用完成闭包:

上篇看完下载模块了,这篇主要是看一下缓存模块。我们是从KingfisherManager中的downloadAndCacheImageWithURL为入口进入到下载模块的,缓存模块也从这里进入。再贴一下downloadAndCacheImageWithURL吧:

上一篇地址:Kingfisher源码阅读(二)
第一篇地址:Kingfisher源码阅读(一)

  • 遍历缓存图片(跳过隐藏文件和文件夹),如果图片过期,则加入待删除队列:
  • 如果方法参数toDisktrue则先将其缓存到文件(如果图片数据存在并能被正确解析的话),然后调用完成闭包:
  • 删除待删除队列中的图片:

本文由彩世界开奖app官网发布于计算机编程,转载请注明出处:深入理解Kingfisher(上)彩世界开奖app官网

关键词: iOS 随笔 Kingfisher Swift第三...