前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kingfisher源码阅读(二)

Kingfisher源码阅读(二)

作者头像
Sheepy
发布2018-09-10 12:16:05
7310
发布2018-09-10 12:16:05
举报

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

开始下载任务

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

代码语言:javascript
复制
//下载图片
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这个方法,它是长这样的:

代码语言:javascript
复制
//默认访问级别,只能在模块内部使用
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这个方法:

代码语言:javascript
复制
// 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是在初始化函数里创建的一个并发队列:

代码语言:javascript
复制
 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中的一个内部类,它的声明如下:

代码语言:javascript
复制
//(下载进度回调,下载完成回调)元组的数组,响应数据,是否解码,缩放尺寸。
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数组中。这些准备工作都完成之后就可以调用这两句开始下载图片了:

代码语言:javascript
复制
let session = NSURLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())
started(session, loadObjectForURL!)

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

代码语言:javascript
复制
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,如果有人调用了这个方法的话,图片还是能下载,但是就不能取消下载了:

代码语言:javascript
复制
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这个协议:

代码语言:javascript
复制
extension ImageDownloader: NSURLSessionDataDelegate {

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

代码语言:javascript
复制
/**
 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依次执行。

代码语言:javascript
复制
/**
 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的方法,我们看看它是什么样子:

代码语言:javascript
复制
//依次调用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中的完成闭包。

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

代码语言:javascript
复制
//身份认证
/**
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源码阅读(三)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015.12.03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开始下载任务
  • 下载代理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档