深入理解Kingfisher(下)


 原文链接     by: 我偏笑_NSNirvana

六、ImageDownloader

下载功能的架构以及主要属性介绍

在 Kingfisher 内,该类负责网络图片的下载,是对底层 URLSession 的封装,通过设置 URLSession 并成为 NSURLSessionDataDelegate 来得到图片数据,其主要属性如下所示:

public class ImageDownloader: NSObject {

    class ImageFetchLoad {
        var callbacks = [CallbackPair]()
        var responseData = NSMutableData()
        var shouldDecode = false
        var scale = KingfisherManager.DefaultOptions.scale
    }

    // MARK: - Public property
    /// This closure will be applied to the image download request before it being sent. You can modify the request for some customizing purpose, like adding auth token to the header or do a url mapping.
    public var requestModifier: (NSMutableURLRequest -> Void)?

    /// The duration before the download is timeout. Default is 15 seconds.
    public var downloadTimeout: NSTimeInterval = 15.0

    /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. You can use this set to specify the self-signed site.
    public var trustedHosts: Set<String>?

    /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
    public var sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration()

    /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
    public weak var delegate: ImageDownloaderDelegate?

    // MARK: - Internal property
    let barrierQueue: dispatch_queue_t
    let processQueue: dispatch_queue_t

    typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHander: ImageDownloaderCompletionHandler?)

    var fetchLoads = [NSURL: ImageFetchLoad]()

    // MARK: - Public method
    /// The default downloader.
    public class var defaultDownloader: ImageDownloader {
        return instance
    }

    /**
    Init a downloader with name.

    - parameter name: The name for the downloader. It should not be empty.

    - returns: The downloader object.
    */
    public init(name: String) {
        if name.isEmpty {
            fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
        }

        barrierQueue = dispatch_queue_create(downloaderBarrierName + name, DISPATCH_QUEUE_CONCURRENT)
        processQueue = dispatch_queue_create(imageProcessQueueName + name, DISPATCH_QUEUE_CONCURRENT)
    }

    func fetchLoadForKey(key: NSURL) -> ImageFetchLoad? {
        var fetchLoad: ImageFetchLoad?
        dispatch_sync(barrierQueue, { () -> Void in
            fetchLoad = self.fetchLoads[key]
        })
        return fetchLoad
    }
}

这段代码的重点在于:
这里定义了一个嵌套类 ImageFetchLoad,用于处理每一个 NSURL 的对应下载数据;
每一个 URL 的 ImageFetchLoad 里都包含一个 callbacks: [CallbackPair],而 CallbackPair 是一个元组,其中又包含两个闭包,一个是 progressBlock,一个是 completionHander,progressBlock 在每次接收到数据时都会调用,当下载任务较大时用于展示进度条,completionHander 当最后数据接收完成之后会被调用,只被调用一次。
每次获得的新数据都会被添加入 responseData: NSMutableData 中,最后完整的图片数据也会保存在其中。
通常情况下,我们的 ImageDownloader 往往需要处理多个 URL,也就对应多个 ImageFetchLoad,fetchLoads 是 [NSURL: ImageFetchLoad] 类型的字典,用于存储不同 URL 及其 ImageFetchLoad 之间的对应关系,这就牵扯到了一个问题,当读取 ImageFetchLoad 的时候,我们希望该 ImageFetchLoad 不在被写,写的同时不能进行读操作,我们使用 barrierQueue 来完成该需求,利用 dispatch_sync 阻塞当前线程,完成 ImageFetchLoad 读操作后再返回。
其实针对这种需求 GCD 提供了专门的特性来处理,即 dispatch_barrier_async 方法,使用此方法提交的任务,会等待先于它提交的任务执行完成之后才开始执行,只有当该任务执行完成之后,晚于它提交的任务才会开始执行,确保一段时间内该队列只执行该任务。

下载方法以及 NSURLSession 的设置

这段主要是为 KingfisherManager 提供封装好下载方法以及设置用于下载的 NSURLSession,代码如下:

    internal func downloadImageWithURL(URL: NSURL,
                       retrieveImageTask: RetrieveImageTask?,
                                 options: KingfisherManager.Options,
                           progressBlock: ImageDownloaderProgressBlock?,
                       completionHandler: ImageDownloaderCompletionHandler?)
    {
        if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelled {
            return
        }

        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.
        let request = NSMutableURLRequest(URL: URL, cachePolicy: .ReloadIgnoringLocalCacheData, timeoutInterval: timeout)
        request.HTTPShouldUsePipelining = true

        self.requestModifier?(request)

        // There is a possiblility that request modifier changed the url to `nil`
        if request.URL == nil {
            completionHandler?(image: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.InvalidURL.rawValue, userInfo: nil), imageURL: nil, originalData: nil)
            return
        }

        setupProgressBlock(progressBlock, completionHandler: completionHandler, forURL: request.URL!) {(session, fetchLoad) -> Void in
            let task = session.dataTaskWithRequest(request)
            task.priority = options.lowPriority ? NSURLSessionTaskPriorityLow : NSURLSessionTaskPriorityDefault
            task.resume()

            fetchLoad.shouldDecode = options.shouldDecode
            fetchLoad.scale = options.scale

            retrieveImageTask?.downloadTask = task
        }
    }

    // A single key may have multiple callbacks. Only download once.
    internal func setupProgressBlock(progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?, forURL URL: NSURL, started: ((NSURLSession, ImageFetchLoad) -> Void)) {

        dispatch_barrier_sync(barrierQueue, { () -> Void in

            var create = false
            var loadObjectForURL = self.fetchLoads[URL]
            if  loadObjectForURL == nil {
                create = true
                loadObjectForURL = ImageFetchLoad()
            }

            let callbackPair = (progressBlock: progressBlock, completionHander: completionHandler)
            loadObjectForURL!.callbacks.append(callbackPair)
            self.fetchLoads[URL] = loadObjectForURL!

            if create {
                let session = NSURLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())
                started(session, loadObjectForURL!)
            }
        })
    }

    func cleanForURL(URL: NSURL) {
        dispatch_barrier_sync(barrierQueue, { () -> Void in
            self.fetchLoads.removeValueForKey(URL)
            return
        })
    }
}

这里首先值得一提的是 NSMutableURLRequest 的实例属性 HTTPShouldUsePipelining,首先一图流解释这个属性的价值:


HTTPShouldUsePipelining.png

将该属性设置为 true 可以极大的提高网络性能,但是 HTTPShouldUsePipelining 也有其局限性,就是服务器必须按照收到请求的顺序返回对应的数据,详细内容在这里

从某 URL 处下载图片时,通过 setupProgressBlock 以及传入的 started 闭包生成对应的 NSURLSession,并依据生成的 session 和之前的 request 生成 NSURLSessionDataTask,并保留引用在 retrieveImageTask?.downloadTask 里,为 KingfisherManager 提供任务终止方法。
当生成 NSURLSession 时所传入的 ephemeralSessionConfiguration() 配置参数意在不保留下载缓存,因为缓存操作已在我们的 ImageCache 文件中处理,所以此处需做如此设置以保证 ImageDownloader 正常工作。

NSURLSessionDataDelegate

let session = NSURLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())

在之前的这行代码里,我们将自身设为了生成的 NSURLSession 的 delegate,所以接下来我们要通过实现 NSURLSessionDataDelegate 来得到返回的图片数据。
代码如下:

// MARK: - NSURLSessionTaskDelegate
extension ImageDownloader: NSURLSessionDataDelegate {
    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {

        completionHandler(NSURLSessionResponseDisposition.Allow)
    }

    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {

        if let URL = dataTask.originalRequest?.URL, fetchLoad = fetchLoadForKey(URL) {
            fetchLoad.responseData.appendData(data)

            for callbackPair in fetchLoad.callbacks {
                callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength)
            }
        }
    }

    private func callbackWithImage(image: UIImage?, error: NSError?, imageURL: NSURL, originalData: NSData?) {
        if let callbackPairs = fetchLoadForKey(imageURL)?.callbacks {

            self.cleanForURL(imageURL)

            for callbackPair in callbackPairs {
                callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData)
            }
        }
    }

    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {

        if let URL = task.originalRequest?.URL {
            if let error = error { // Error happened
                callbackWithImage(nil, error: error, imageURL: URL, originalData: nil)
            } else { //Download finished without error

                // We are on main queue when receiving this.
                dispatch_async(processQueue, { () -> Void in

                    if let fetchLoad = self.fetchLoadForKey(URL) {

                        if let image = UIImage.kf_imageWithData(fetchLoad.responseData, scale: fetchLoad.scale) {

                            self.delegate?.imageDownloader?(self, didDownloadImage: image, forURL: URL, withResponse: task.response!)

                            if fetchLoad.shouldDecode {

                                self.callbackWithImage(image.kf_decodedImage(scale: fetchLoad.scale), error: nil, imageURL: URL, originalData: fetchLoad.responseData)
                            } else {

                                self.callbackWithImage(image, error: nil, imageURL: URL, originalData: fetchLoad.responseData)
                            }

                        } else {
                            // If server response is 304 (Not Modified), inform the callback handler with NotModified error.
                            // It should be handled to get an image from cache, which is response of a manager object.
                            if let res = task.response as? NSHTTPURLResponse where res.statusCode == 304 {
                                self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.NotModified.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                                return
                            }

                            self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                        }
                    } else {
                        self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                    }
                })
            }
        }
    }

    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            if let trustedHosts = trustedHosts where trustedHosts.contains(challenge.protectionSpace.host) {
                let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
                completionHandler(.UseCredential, credential)
                return
            }
        }

        completionHandler(.PerformDefaultHandling, nil)
    }

}

其中最重要的是这两个函数:

public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData)

前一个函数当每一次下载到数据的时候都会被调用,我们在该函数中,将每次得到的数据添加在当前 URL 所对应的 fetchLoad 的 responseData 中,之后,我们为传入的 progressBlock 提供当前下载进度。

public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?)

该函数在下载任务完成时会被调用,Kingfisher 在其中进行了各种错误处理,若数据成功下载,callbackWithImage 方法会被调用,返回下载到的图片、图片的 URL、以及下载到的原始数据。
这里我们注意到在图片下载成功之后,自身 ImageDownloaderDelegate 的代理方法会被调用,但我通过翻阅源码发现,并没有其他类接受了这个代理,delegate 始终为 nil,所以不对其进行讲解。
而且大概因为图片的解码操作也比较费时,Kingfisher 将函数主体放在了 processQueue 中执行以避免阻塞主线程。

KingfisherOptionsInfo

该文件主要用于接收配置 Kingfisher 行为的各种参数,包括缓存、下载、加载动画以及之前 KingfisherOptions 所包含的所有属性,代码如下:

/**
*    KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem]. You can use the enum of option item with value to control some behaviors of Kingfisher.
*/
public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]

/**
Item could be added into KingfisherOptionsInfo

- Options:     Item for options. The value of this item should be a KingfisherOptions.
- TargetCache: Item for target cache. The value of this item should be an ImageCache object. Kingfisher will use this cache when handling the related operation, including trying to retrieve the cached images and store the downloaded image to it.
- Downloader:  Item for downloader to use. The value of this item should be an ImageDownloader object. Kingfisher will use this downloader to download the images.
- Transition:  Item for animation transition when using UIImageView.
*/
public enum KingfisherOptionsInfoItem {
    case Options(KingfisherOptions)
    case TargetCache(ImageCache)
    case Downloader(ImageDownloader)
    case Transition(ImageTransition)
}

func ==(a: KingfisherOptionsInfoItem, b: KingfisherOptionsInfoItem) -> Bool {
    switch (a, b) {
    case (.Options(_), .Options(_)): return true
    case (.TargetCache(_), .TargetCache(_)): return true
    case (.Downloader(_), .Downloader(_)): return true
    case (.Transition(_), .Transition(_)): return true
    default: return false
    }
}

extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {
    func kf_findFirstMatch(target: Generator.Element) -> Generator.Element? {

        let index = indexOf {
            e in
            return e == target
        }

        return (index != nil) ? self[index!] : nil
    }
}

这段代码之中有两个亮点,其一是借助了对 == 运算符的重载实现了判断传入参数类型的作用;其二是通过对 CollectionType 的拓展来为其添加迅速找出对应类型配置参数的功能,该函数中出现的第二个 == 运算符即使用到了上方对 == 的拓展用法,第一个 == 运算符用于判断 Generator.Element 的类型,其重载在 CollectionType 内部实现。

KingfisherManager

该类是 Kingfisher 的核心类,封装了之前讲到的 ImageCache、ImageDownloader 与 KingfisherOptionsInfo,集成了缓存以及下载两大功能,并直接为 UIImageView+Kingfisher 以及 UIButton+Kingfisher 提供操作方法。
该类的功能主要可以分为两部分:一是根据传入的 URL 返回对应的网络图片,二是解析传入的配置参数并对相关功能模块进行配置。
其中第二个部分又是第一个功能的组成部分,我们先来看第二部分,代码如下:

    func parseOptionsInfo(optionsInfo: KingfisherOptionsInfo?) -> (Options, ImageCache, ImageDownloader) {
        var options = KingfisherManager.DefaultOptions
        var targetCache = self.cache
        var targetDownloader = self.downloader

        guard let optionsInfo = optionsInfo else {
            return (options, targetCache, targetDownloader)
        }

        if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)), case .Options(let optionsInOptionsInfo) = optionsItem {

            let queue = optionsInOptionsInfo.contains(KingfisherOptions.BackgroundCallback) ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) : KingfisherManager.DefaultOptions.queue
            let scale = optionsInOptionsInfo.contains(KingfisherOptions.ScreenScale) ? UIScreen.mainScreen().scale : KingfisherManager.DefaultOptions.scale

            options = (forceRefresh: optionsInOptionsInfo.contains(KingfisherOptions.ForceRefresh),
                lowPriority: optionsInOptionsInfo.contains(KingfisherOptions.LowPriority),
                cacheMemoryOnly: optionsInOptionsInfo.contains(KingfisherOptions.CacheMemoryOnly),
                shouldDecode: optionsInOptionsInfo.contains(KingfisherOptions.BackgroundDecode),
                queue: queue, scale: scale)
        }

        if let optionsItem = optionsInfo.kf_findFirstMatch(.TargetCache(self.cache)), case .TargetCache(let cache) = optionsItem {
            targetCache = cache
        }

        if let optionsItem = optionsInfo.kf_findFirstMatch(.Downloader(self.downloader)), case .Downloader(let downloader) = optionsItem {
            targetDownloader = downloader
        }

        return (options, targetCache, targetDownloader)
    }

我们可以看到三个 if let 语句分别对应 KingfisherOptionsInfoItem 的前三种枚举配置类型,而配置 KingfisherOptions 相关参数的时候又用到了 OptionSetType 的协议拓展方法 contains,来获取对应属性的配置参数。对最后一种 ImageTransition 的配置, Kingfisher 放在了 UIImageView+Kingfisher 中进行。

接下来我们来讲第一部分,第一部分的主要功能函数有两个,代码如下:

    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)
        }) { (image, error, imageURL, originalData) -> () in

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

该函数负责下载传入URL所对应的网络图片并将其缓存,主要调用了 downloader.downloadImageWithURL 来下载所需图片数据,之后调用 targetCache.storeImage 来缓存数据。

public func retrieveImageWithResource(resource: Resource,
        optionsInfo: KingfisherOptionsInfo?,
        progressBlock: DownloadProgressBlock?,
        completionHandler: CompletionHandler?) -> RetrieveImageTask
    {
        let task = RetrieveImageTask()

        // There is a bug in Swift compiler which prevents to write `let (options, targetCache) = parseOptionsInfo(optionsInfo)`
        // It will cause a compiler error.
        let parsedOptions = parseOptionsInfo(optionsInfo)
        let (options, targetCache, downloader) = (parsedOptions.0, parsedOptions.1, parsedOptions.2)

        if options.forceRefresh {
            downloadAndCacheImageWithURL(resource.downloadURL,
                forKey: resource.cacheKey,
                retrieveImageTask: task,
                progressBlock: progressBlock,
                completionHandler: completionHandler,
                options: options,
                targetCache: targetCache,
                downloader: downloader)
        } else {
            let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
                // Break retain cycle created inside diskTask closure below
                task.diskRetrieveTask = nil
                completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
            }
            let diskTask = targetCache.retrieveImageForKey(resource.cacheKey, options: options, completionHandler: { (image, cacheType) -> () in
                if image != nil {
                    diskTaskCompletionHandler(image: image, error: nil, cacheType:cacheType, imageURL: resource.downloadURL)
                } else {
                    self.downloadAndCacheImageWithURL(resource.downloadURL,
                        forKey: resource.cacheKey,
                        retrieveImageTask: task,
                        progressBlock: progressBlock,
                        completionHandler: diskTaskCompletionHandler,
                        options: options,
                        targetCache: targetCache,
                        downloader: downloader)
                }
            })
            task.diskRetrieveTask = diskTask
        }

        return task
    }

首先我们对传入的配置参数使用 parseOptionsInfo 方法进行解析,如果 options.forceRefresh,被设置为 true,便直接调用 downloadAndCacheImageWithURL 方法下载并缓存该 URL 所对应的图片,若被设置为 false,则先调用 targetCache.retrieveImageForKey 尝试从缓存中取出所需图片,如果取不到,说明缓存中没有对应图片,则调用 downloadAndCacheImageWithURL 下载并缓存对应图片。

UIImageView+Kingfisher 以及 UIButton+Kingfisher

这两个类主要是对 UIImageView 和 UIButton进行拓展,功能的实现部分均为对 KingfisherManager 内相应函数的调用,Kingfisher 的文档内详细介绍了这两个类的对外拓展接口,这里就不赘述了。
不过其内部仍包含一个值得我们学习的知识点。

Associated Objects

Associated Objects(关联对象)或者叫作关联引用(Associative References),是作为Objective-C 2.0 运行时功能被引入到 Mac OS X 10.6 Snow Leopard(及iOS4)系统。与它相关在<objc/runtime.h>中有3个C函数,它们可以让对象在运行时关联任何值:

  • objc_setAssociatedObject
  • objc_getAssociatedObject
  • objc_removeAssociatedObjects

我们并不能在类型拓展中放置存储属性,所以需要使用 Associated Objects 来向某些系统类(NSObject 的子类)中增添所需的属性。
这里将介绍其最简单的用法,代码如下:

private var lastURLKey: Void?
public extension UIImageView {
    /// Get the image URL binded to this image view.
    public var kf_webURL: NSURL? {
        get {
            return objc_getAssociatedObject(self, &lastURLKey) as? NSURL
        }
    }

    private func kf_setWebURL(URL: NSURL) {
        objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

这里为 UIImageView 成功添加了 kf_webURL 属性,我们通过 kf_setWebURL 对其赋值,通过 kf_webURL 获取其值。
如果你想知道更多 Associated Objects 的相关内容,可以看这里

结语

由于笔者只是大三在校生,并没有工作经验,对iOS开发的学习也完全出于兴趣,所以文章出现纰漏之处在所难免,恳请前辈们批评指正,不胜感激。

相关代码:

Kingfisher