Kingfisher源码阅读(二)


 原文链接     by: Sheepy

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

开始下载任务

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

//下载图片
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)
    }
)

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

//默认访问级别,只能在模块内部使用
internal func downloadImageWithURL(URL: NSURL,
    retrieveImageTask: RetrieveImageTask?,
    options: KingfisherManager.Options,
    progressBlock: ImageDownloaderProgressBlock?,
    completionHandler: ImageDownloaderCompletionHandler?)
{
    //retrieveImageTask为nil不return,继续向下执行,只是没有记录下载任务,无法取消下载过程了
    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
    }
    //下面的步骤一次也不执行到话self.fetchLoads[URL]就为nil
    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
    }
}

调用setupProgressBlock这个方法之前的部分都是发送网络请求之前的处理,需要注意的地方我在注释里也写了,我们重点来看看setupProgressBlock这个方法:

// 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
        //----向fetchLoads[URL](如果没有就创建一个).callbacks添加一个callbackPair(下载进度回调,下载完成回调)
        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!)
        }
    })
}

barrierQueue是在初始化函数里创建的一个并发队列:

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

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

//(下载进度回调,下载完成回调)元组的数组,响应数据,是否解码,缩放尺寸。
class ImageFetchLoad {
    var callbacks = [CallbackPair]()
    var responseData = NSMutableData()
    var shouldDecode = false
    var scale = KingfisherManager.DefaultOptions.scale
}

这个类非常关键,我们可以看到在setupProgressBlock先是用图片的URL去self.fetchLoads里取对应的ImageFetchLoad,如果没有的话就以当前URL为键创建一个,然后把传过来的progressBlockcompletionHandler打包成一个元组,添加到ImageFetchLoad里的callbacks数组中。这些准备工作都完成之后就可以调用这两句开始下载图片了:

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

这里使用了NSURLSession,是iOS7之后比较主流的用于网络请求的API(iOS7以前多使用NSURLConnection),然后指明了以自身实例作为delegatestarted是一个作为参数传入的闭包,它长什么样在downloadImageWithURL中调用setupProgressBlock时其实我们已经见过了:

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
}

这个过程其实就是加载一下请求,配置一下优先级,然后开始网络任务。如果retrieveImageTask不为nil的话就把这个网络任务task赋值给retrieveImageTask?.downloadTask,这样调用retrieveImageTask .cancle()但时候就可以取消下载了。显然按我之前的线路走下来retrieveImageTask是有值的,但ImageDownloader还有下面这个方法,调用downloadImageWithURLretrieveImageTask这个参数为nil,如果有人调用了这个方法的话,图片还是能下载,但是就不能取消下载了:

public func downloadImageWithURL(URL: NSURL,
    options: KingfisherManager.Options,
    progressBlock: ImageDownloaderProgressBlock?,
    completionHandler: ImageDownloaderCompletionHandler?)
{
    downloadImageWithURL(URL,
        retrieveImageTask: nil,
        options: options,
        progressBlock: progressBlock,
        completionHandler: completionHandler)
}

下载代理

前面已经看到ImageDownloader指定了NSURLSessiondelegate为自身实例,所以ImageDownloader要遵守NSURLSessionDataDelegate这个协议:

extension ImageDownloader: NSURLSessionDataDelegate {

我们来看几个关键的函数:

/**
 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) {
        //向fetchLoads[URL].responseData添加一条响应数据
        fetchLoad.responseData.appendData(data)
        //依次调用fetchLoads[URL]中的所有过程回调
        for callbackPair in fetchLoad.callbacks {
            callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength)
        }
    }
}

这个函数会在接收到数据的时候被调用,我们取出之前添加到fetchLoads[URL].callbacks中的progressBlock依次执行。

/**
 This method is exposed since the compiler requests. Do not call it.
 */
public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
    //原始请求的URL
    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
                //获取fetchLoads[URL]
                if let fetchLoad = self.fetchLoadForKey(URL) {

                    if let image = UIImage.kf_imageWithData(fetchLoad.responseData, scale: fetchLoad.scale) {
                        //下载完成后可以进行的自定义操作,用户可以自行指定delegate
                        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 {
                        //不能生成图片,返回304状态码,表示图片没有更新,可以直接使用缓存
                        // 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
                        }
                        //不能生成图片,报BadData错误
                        self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                    }
                } else {
                    //fatchLoads[URL] = nil,说明setupProgressBlock方法一次也没执行,let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelled,request.URL == nil
                    self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                }
            })
        }
    }
}

这个方法是在请求完成之后调用的,很关键。虽然比较长,但是思路清晰,我还略显画蛇添足地做了些中文注释,应该不用多说了(当然跟之前一样,我觉得这里把dispatch_async之后的那一整段逻辑提取为一个callbackWithNoErrorFor(task: NSURLSessionTask, URL: NSURL)可读性会更好,比较对称)。这里多次使用到的一个callbackWithImage的方法,我们看看它是什么样子:

//依次调用fetchLoads[URL]中的所有完成回调,并删除该URL对应的键值对
private func callbackWithImage(image: UIImage?, error: NSError?, imageURL: NSURL, originalData: NSData?) {
    if let callbackPairs = fetchLoadForKey(imageURL)?.callbacks {
        //就是调用了self.fetchLoads.removeValueForKey(URL)
        self.cleanForURL(imageURL)

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

先去取跟imageURL对应的fetchLoadcallbacks,取到之后就把fetchLoadsimageURL的键值对删掉(因为闭包元组已经取出来了,接下来就要依次调用完成闭包,这张图片的fetchLoad在下载模块中的使命已经光荣完成),最后依次调用callbacks中的完成闭包。

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

//身份认证
/**
This method is exposed since the compiler requests. Do not call it.
*/
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    //一般用于SSL/TLS协议(https)
    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)
}

我之前并没有用过这个方法,查了一点资料,大概主要是用来对https做处理的吧,trustedHostsImageDownloader里声明的一个字符串集合,应该就是类似于一个白名单,放到里面的域名是可以信任的。

下载模块差不多就是这样,小结一下知识点:

  • NSMutableURLRequest:用于创建一个网络请求对象,可以根据需要来配置请求报头等信息。
  • dispatch_barrier_sync:该方法用于对操作设置屏障,确保在执行完任务后才会执行后续操作,保持同步和线程安全。
  • 关于NSURLAuthenticationChallenge的委托方法,可以使用白名单对信任的域名做特殊处理。

嗯,下期就是缓存模块了。话说昨天给Kingfisher提了个萌萌的pull request,喵神接受了诶,喵神真是好人^ ^不过虽然我读的是最新的版本,但fork的版本比较老了,都忘了这茬,导致了很多冲突,让喵神不好merge了,真是不好意思。今天再提交一次pull request。


我好蠢- -.png

下一篇地址:Kingfisher源码阅读(三)

相关代码:

Kingfisher