Kingfisher源碼閱讀(二)

上一篇地址:Kingfisher源碼閱讀(一)

開始下載任務(wù)

上次說到了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,嘗試從緩存中取數(shù)據(jù)
        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)
    }
)

調(diào)用了downloaderdownloadImageWithURL方法,然后在completionHandler這個完成閉包中做緩存相關(guān)的操作,我們先不管緩存,先去ImageDownloader(downloader是它的一個實例)里看看downloadImageWithURL這個方法,它是長這樣的:

//默認訪問級別,只能在模塊內(nèi)部使用
internal func downloadImageWithURL(URL: NSURL,
    retrieveImageTask: RetrieveImageTask?,
    options: KingfisherManager.Options,
    progressBlock: ImageDownloaderProgressBlock?,
    completionHandler: ImageDownloaderCompletionHandler?)
{
    //retrieveImageTask為nil不return,繼續(xù)向下執(zhí)行,只是沒有記錄下載任務(wù),無法取消下載過程了
    if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelled {
        return
    }
    
    let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
    //用于創(chuàng)建一個網(wǎng)絡(luò)請求對象,我們可以根據(jù)需要來配置請求報頭等信息。
    // 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
    }
    //下面的步驟一次也不執(zhí)行到話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
    }
}

調(diào)用setupProgressBlock這個方法之前的部分都是發(fā)送網(wǎng)絡(luò)請求之前的處理,需要注意的地方我在注釋里也寫了,我們重點來看看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)) {
    
    //該方法用于對操作設(shè)置屏障,確保在執(zhí)行完任務(wù)后才會執(zhí)行后續(xù)操作。常用于確保線程安全性操作。
    dispatch_barrier_sync(barrierQueue, { () -> Void in
        //----向fetchLoads[URL](如果沒有就創(chuàng)建一個).callbacks添加一個callbackPair(下載進度回調(diào),下載完成回調(diào))
        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是在初始化函數(shù)里創(chuàng)建的一個并發(fā)隊列:

 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中的一個內(nèi)部類,它的聲明如下:

//(下載進度回調(diào),下載完成回調(diào))元組的數(shù)組,響應(yīng)數(shù)據(jù),是否解碼,縮放尺寸。
class ImageFetchLoad {
    var callbacks = [CallbackPair]()
    var responseData = NSMutableData()
    var shouldDecode = false
    var scale = KingfisherManager.DefaultOptions.scale
}

這個類非常關(guān)鍵,我們可以看到在setupProgressBlock先是用圖片的URL去self.fetchLoads里取對應(yīng)的ImageFetchLoad,如果沒有的話就以當前URL為鍵創(chuàng)建一個,然后把傳過來的progressBlockcompletionHandler打包成一個元組,添加到ImageFetchLoad里的callbacks數(shù)組中。這些準備工作都完成之后就可以調(diào)用這兩句開始下載圖片了:

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

這里使用了NSURLSession,是iOS7之后比較主流的用于網(wǎng)絡(luò)請求的API(iOS7以前多使用NSURLConnection),然后指明了以自身實例作為delegatestarted是一個作為參數(shù)傳入的閉包,它長什么樣在downloadImageWithURL中調(diào)用setupProgressBlock時其實我們已經(jīng)見過了:

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
}

這個過程其實就是加載一下請求,配置一下優(yōu)先級,然后開始網(wǎng)絡(luò)任務(wù)。如果retrieveImageTask不為nil的話就把這個網(wǎng)絡(luò)任務(wù)task賦值給retrieveImageTask?.downloadTask,這樣調(diào)用retrieveImageTask .cancle()但時候就可以取消下載了。顯然按我之前的線路走下來retrieveImageTask是有值的,但ImageDownloader還有下面這個方法,調(diào)用downloadImageWithURLretrieveImageTask這個參數(shù)為nil,如果有人調(diào)用了這個方法的話,圖片還是能下載,但是就不能取消下載了:

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

下載代理

前面已經(jīng)看到ImageDownloader指定了NSURLSessiondelegate為自身實例,所以ImageDownloader要遵守NSURLSessionDataDelegate這個協(xié)議:

extension ImageDownloader: NSURLSessionDataDelegate {

我們來看幾個關(guān)鍵的函數(shù):

/**
 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添加一條響應(yīng)數(shù)據(jù)
        fetchLoad.responseData.appendData(data)
        //依次調(diào)用fetchLoads[URL]中的所有過程回調(diào)
        for callbackPair in fetchLoad.callbacks {
            callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength)
        }
    }
}

這個函數(shù)會在接收到數(shù)據(jù)的時候被調(diào)用,我們?nèi)〕鲋疤砑拥?code>fetchLoads[URL].callbacks中的progressBlock依次執(zhí)行。

/**
 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!)
                        //如果指定需要解碼,則先解碼再進行完成回調(diào)
                        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狀態(tài)碼,表示圖片沒有更新,可以直接使用緩存
                        // 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方法一次也沒執(zhí)行,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)
                }
            })
        }
    }
}

這個方法是在請求完成之后調(diào)用的,很關(guān)鍵。雖然比較長,但是思路清晰,我還略顯畫蛇添足地做了些中文注釋,應(yīng)該不用多說了(當然跟之前一樣,我覺得這里把dispatch_async之后的那一整段邏輯提取為一個callbackWithNoErrorFor(task: NSURLSessionTask, URL: NSURL)可讀性會更好,比較對稱)。這里多次使用到的一個callbackWithImage的方法,我們看看它是什么樣子:

//依次調(diào)用fetchLoads[URL]中的所有完成回調(diào),并刪除該URL對應(yīng)的鍵值對
private func callbackWithImage(image: UIImage?, error: NSError?, imageURL: NSURL, originalData: NSData?) {
    if let callbackPairs = fetchLoadForKey(imageURL)?.callbacks {
        //就是調(diào)用了self.fetchLoads.removeValueForKey(URL)
        self.cleanForURL(imageURL)
        
        for callbackPair in callbackPairs {
            callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData)
        }
    }
}

先去取跟imageURL對應(yīng)的fetchLoadcallbacks,取到之后就把fetchLoadsimageURL的鍵值對刪掉(因為閉包元組已經(jīng)取出來了,接下來就要依次調(diào)用完成閉包,這張圖片的fetchLoad在下載模塊中的使命已經(jīng)光榮完成),最后依次調(diào)用callbacks中的完成閉包。

主要的委托方法都看完了,最后還有一個跟身份認證有關(guān)的:

//身份認證
/**
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協(xié)議(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里聲明的一個字符串集合,應(yīng)該就是類似于一個白名單,放到里面的域名是可以信任的。

下載模塊差不多就是這樣,小結(jié)一下知識點:

  • NSMutableURLRequest:用于創(chuàng)建一個網(wǎng)絡(luò)請求對象,可以根據(jù)需要來配置請求報頭等信息。
  • dispatch_barrier_sync:該方法用于對操作設(shè)置屏障,確保在執(zhí)行完任務(wù)后才會執(zhí)行后續(xù)操作,保持同步和線程安全。
  • 關(guān)于NSURLAuthenticationChallenge的委托方法,可以使用白名單對信任的域名做特殊處理。

嗯,下期就是緩存模塊了。話說昨天給Kingfisher提了個萌萌的pull request,喵神接受了誒,喵神真是好人^ ^不過雖然我讀的是最新的版本,但fork的版本比較老了,都忘了這茬,導(dǎo)致了很多沖突,讓喵神不好merge了,真是不好意思。今天再提交一次pull request。

我好蠢- -.png

下一篇地址:Kingfisher源碼閱讀(三)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容