added iOS source code
[wl-app.git] / iOS / Pods / Kingfisher / Sources / ImageDownloader.swift
1 //
2 //  ImageDownloader.swift
3 //  Kingfisher
4 //
5 //  Created by Wei Wang on 15/4/6.
6 //
7 //  Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
8 //
9 //  Permission is hereby granted, free of charge, to any person obtaining a copy
10 //  of this software and associated documentation files (the "Software"), to deal
11 //  in the Software without restriction, including without limitation the rights
12 //  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 //  copies of the Software, and to permit persons to whom the Software is
14 //  furnished to do so, subject to the following conditions:
15 //
16 //  The above copyright notice and this permission notice shall be included in
17 //  all copies or substantial portions of the Software.
18 //
19 //  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 //  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 //  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 //  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 //  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 //  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 //  THE SOFTWARE.
26
27 #if os(macOS)
28 import AppKit
29 #else
30 import UIKit
31 #endif
32
33 /// Progress update block of downloader.
34 public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
35
36 /// Completion block of downloader.
37 public typealias ImageDownloaderCompletionHandler = ((_ image: Image?, _ error: NSError?, _ url: URL?, _ originalData: Data?) -> Void)
38
39 /// Download task.
40 public struct RetrieveImageDownloadTask {
41     let internalTask: URLSessionDataTask
42     
43     /// Downloader by which this task is intialized.
44     public private(set) weak var ownerDownloader: ImageDownloader?
45
46     
47     /// Cancel this download task. It will trigger the completion handler with an NSURLErrorCancelled error.
48     /// If you want to cancel all downloading tasks, call `cancelAll()` of `ImageDownloader` instance.
49     public func cancel() {
50         ownerDownloader?.cancel(self)
51     }
52     
53     /// The original request URL of this download task.
54     public var url: URL? {
55         return internalTask.originalRequest?.url
56     }
57     
58     /// The relative priority of this download task. 
59     /// It represents the `priority` property of the internal `NSURLSessionTask` of this download task.
60     /// The value for it is between 0.0~1.0. Default priority is value of 0.5.
61     /// See documentation on `priority` of `NSURLSessionTask` for more about it.
62     public var priority: Float {
63         get {
64             return internalTask.priority
65         }
66         set {
67             internalTask.priority = newValue
68         }
69     }
70 }
71
72 ///The code of errors which `ImageDownloader` might encountered.
73 public enum KingfisherError: Int {
74     
75     /// badData: The downloaded data is not an image or the data is corrupted.
76     case badData = 10000
77     
78     /// notModified: The remote server responsed a 304 code. No image data downloaded.
79     case notModified = 10001
80     
81     /// The HTTP status code in response is not valid. If an invalid
82     /// code error received, you could check the value under `KingfisherErrorStatusCodeKey` 
83     /// in `userInfo` to see the code.
84     case invalidStatusCode = 10002
85     
86     /// notCached: The image rquested is not in cache but .onlyFromCache is activated.
87     case notCached = 10003
88     
89     /// The URL is invalid.
90     case invalidURL = 20000
91     
92     /// The downloading task is cancelled before started.
93     case downloadCancelledBeforeStarting = 30000
94 }
95
96 /// Key will be used in the `userInfo` of `.invalidStatusCode`
97 public let KingfisherErrorStatusCodeKey = "statusCode"
98
99 /// Protocol of `ImageDownloader`.
100 public protocol ImageDownloaderDelegate: class {
101     /**
102     Called when the `ImageDownloader` object successfully downloaded an image from specified URL.
103     
104     - parameter downloader: The `ImageDownloader` object finishes the downloading.
105     - parameter image:      Downloaded image.
106     - parameter url:        URL of the original request URL.
107     - parameter response:   The response object of the downloading process.
108     */
109     func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?)
110     
111     /**
112     Called when the `ImageDownloader` object starts to download an image from specified URL.
113      
114     - parameter downloader: The `ImageDownloader` object starts the downloading.
115     - parameter url:        URL of the original request.
116     - parameter response:   The request object of the downloading process.
117     */
118     func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?)
119     
120     /**
121     Check if a received HTTP status code is valid or not. 
122     By default, a status code between 200 to 400 (excluded) is considered as valid.
123     If an invalid code is received, the downloader will raise an .invalidStatusCode error.
124     It has a `userInfo` which includes this statusCode and localizedString error message.
125      
126     - parameter code: The received HTTP status code.
127     - parameter downloader: The `ImageDownloader` object asking for validate status code.
128      
129     - returns: Whether this HTTP status code is valid or not.
130      
131     - Note: If the default 200 to 400 valid code does not suit your need, 
132             you can implement this method to change that behavior.
133     */
134     func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
135     
136     /**
137      Called when the `ImageDownloader` object successfully downloaded image data from specified URL.
138      
139      - parameter downloader: The `ImageDownloader` object finishes data downloading.
140      - parameter data:       Downloaded data.
141      - parameter url:        URL of the original request URL.
142      
143      - returns: The data from which Kingfisher should use to create an image.
144      
145      - Note: This callback can be used to preprocess raw image data
146              before creation of UIImage instance (i.e. decrypting or verification).
147      */
148     func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data?
149 }
150
151 extension ImageDownloaderDelegate {
152     public func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) {}
153     
154     public func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) {}
155     public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
156         return (200..<400).contains(code)
157     }
158     public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {
159         return data
160     }
161 }
162
163 /// Protocol indicates that an authentication challenge could be handled.
164 public protocol AuthenticationChallengeResponsable: class {
165     /**
166      Called when an session level authentication challenge is received.
167      This method provide a chance to handle and response to the authentication challenge before downloading could start.
168      
169      - parameter downloader:        The downloader which receives this challenge.
170      - parameter challenge:         An object that contains the request for authentication.
171      - parameter completionHandler: A handler that your delegate method must call.
172      
173      - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionDelegate`.
174      */
175     func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
176
177     /**
178      Called when an session level authentication challenge is received.
179      This method provide a chance to handle and response to the authentication challenge before downloading could start.
180      
181      - parameter downloader:        The downloader which receives this challenge.
182      - parameter task:              The task whose request requires authentication.
183      - parameter challenge:         An object that contains the request for authentication.
184      - parameter completionHandler: A handler that your delegate method must call.
185      
186      - Note: This method is a forward from `URLSessionTaskDelegate.urlSession(:task:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionTaskDelegate`.
187      */
188     func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
189 }
190
191 extension AuthenticationChallengeResponsable {
192     
193     func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
194     
195         if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
196             if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
197                 let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
198                 completionHandler(.useCredential, credential)
199                 return
200             }
201         }
202         
203         completionHandler(.performDefaultHandling, nil)
204     }
205     
206     func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
207         
208         completionHandler(.performDefaultHandling, nil)
209     }
210
211 }
212
213 /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
214 open class ImageDownloader {
215     
216     class ImageFetchLoad {
217         var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
218         var responseData = NSMutableData()
219
220         var downloadTaskCount = 0
221         var downloadTask: RetrieveImageDownloadTask?
222         var cancelSemaphore: DispatchSemaphore?
223     }
224     
225     // MARK: - Public property
226     /// The duration before the download is timeout. Default is 15 seconds.
227     open var downloadTimeout: TimeInterval = 15.0
228     
229     /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. 
230     /// You can use this set to specify the self-signed site. It only will be used if you don't specify the `authenticationChallengeResponder`. 
231     /// If `authenticationChallengeResponder` is set, this property will be ignored and the implemention of `authenticationChallengeResponder` will be used instead.
232     open var trustedHosts: Set<String>?
233     
234     /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. 
235     /// You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
236     open var sessionConfiguration = URLSessionConfiguration.ephemeral {
237         didSet {
238             session?.invalidateAndCancel()
239             session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: OperationQueue.main)
240         }
241     }
242     
243     /// Whether the download requests should use pipeling or not. Default is false.
244     open var requestsUsePipelining = false
245     
246     fileprivate let sessionHandler: ImageDownloaderSessionHandler
247     fileprivate var session: URLSession?
248     
249     /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
250     open weak var delegate: ImageDownloaderDelegate?
251     
252     /// A responder for authentication challenge. 
253     /// Downloader will forward the received authentication challenge for the downloading session to this responder.
254     open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
255     
256     // MARK: - Internal property
257     let barrierQueue: DispatchQueue
258     let processQueue: DispatchQueue
259     let cancelQueue: DispatchQueue
260     
261     typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?)
262     
263     var fetchLoads = [URL: ImageFetchLoad]()
264     
265     // MARK: - Public method
266     /// The default downloader.
267     public static let `default` = ImageDownloader(name: "default")
268     
269     /**
270     Init a downloader with name.
271     
272     - parameter name: The name for the downloader. It should not be empty.
273     
274     - returns: The downloader object.
275     */
276     public init(name: String) {
277         if name.isEmpty {
278             fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
279         }
280         
281         barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\(name)", attributes: .concurrent)
282         processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)", attributes: .concurrent)
283         cancelQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Cancel.\(name)")
284         
285         sessionHandler = ImageDownloaderSessionHandler()
286
287         // Provide a default implement for challenge responder.
288         authenticationChallengeResponder = sessionHandler
289         session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: .main)
290     }
291     
292     deinit {
293         session?.invalidateAndCancel()
294     }
295     
296     func fetchLoad(for url: URL) -> ImageFetchLoad? {
297         var fetchLoad: ImageFetchLoad?
298         barrierQueue.sync(flags: .barrier) { fetchLoad = fetchLoads[url] }
299         return fetchLoad
300     }
301     
302     /**
303      Download an image with a URL and option.
304      
305      - parameter url:               Target URL.
306      - parameter retrieveImageTask: The task to cooporate with cache. Pass `nil` if you are not trying to use downloader and cache.
307      - parameter options:           The options could control download behavior. See `KingfisherOptionsInfo`.
308      - parameter progressBlock:     Called when the download progress updated.
309      - parameter completionHandler: Called when the download progress finishes.
310      
311      - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
312      */
313     @discardableResult
314     open func downloadImage(with url: URL,
315                        retrieveImageTask: RetrieveImageTask? = nil,
316                        options: KingfisherOptionsInfo? = nil,
317                        progressBlock: ImageDownloaderProgressBlock? = nil,
318                        completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
319     {
320         if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
321             completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
322             return nil
323         }
324         
325         let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
326         
327         // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
328         var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
329         request.httpShouldUsePipelining = requestsUsePipelining
330
331         if let modifier = options?.modifier {
332             guard let r = modifier.modified(for: request) else {
333                 completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
334                 return nil
335             }
336             request = r
337         }
338         
339         // There is a possiblility that request modifier changed the url to `nil` or empty.
340         guard let url = request.url, !url.absoluteString.isEmpty else {
341             completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
342             return nil
343         }
344         
345         var downloadTask: RetrieveImageDownloadTask?
346         setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
347             if fetchLoad.downloadTask == nil {
348                 let dataTask = session.dataTask(with: request)
349                 
350                 fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
351                 
352                 dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
353                 dataTask.resume()
354                 self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
355                 
356                 // Hold self while the task is executing.
357                 self.sessionHandler.downloadHolder = self
358             }
359             
360             fetchLoad.downloadTaskCount += 1
361             downloadTask = fetchLoad.downloadTask
362             
363             retrieveImageTask?.downloadTask = downloadTask
364         }
365         return downloadTask
366     }
367     
368 }
369
370 // MARK: - Download method
371 extension ImageDownloader {
372     
373     // A single key may have multiple callbacks. Only download once.
374     func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
375
376         func prepareFetchLoad() {
377             barrierQueue.sync(flags: .barrier) {
378                 let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
379                 let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
380                 
381                 loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
382                 
383                 fetchLoads[url] = loadObjectForURL
384                 
385                 if let session = session {
386                     started(session, loadObjectForURL)
387                 }
388             }
389         }
390         
391         if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
392             if fetchLoad.cancelSemaphore == nil {
393                 fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
394             }
395             cancelQueue.async {
396                 _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
397                 fetchLoad.cancelSemaphore = nil
398                 prepareFetchLoad()
399             }
400         } else {
401             prepareFetchLoad()
402         }
403     }
404     
405     private func cancelTaskImpl(_ task: RetrieveImageDownloadTask, fetchLoad: ImageFetchLoad? = nil, ignoreTaskCount: Bool = false) {
406         
407         func getFetchLoad(from task: RetrieveImageDownloadTask) -> ImageFetchLoad? {
408             guard let URL = task.internalTask.originalRequest?.url,
409                   let imageFetchLoad = self.fetchLoads[URL] else
410             {
411                 return nil
412             }
413             return imageFetchLoad
414         }
415         
416         guard let imageFetchLoad = fetchLoad ?? getFetchLoad(from: task) else {
417             return
418         }
419
420         imageFetchLoad.downloadTaskCount -= 1
421         if ignoreTaskCount || imageFetchLoad.downloadTaskCount == 0 {
422             task.internalTask.cancel()
423         }
424     }
425     
426     func cancel(_ task: RetrieveImageDownloadTask) {
427         barrierQueue.sync(flags: .barrier) { cancelTaskImpl(task) }
428     }
429     
430     /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished
431     /// downloading tasks with an NSURLErrorCancelled error.
432     ///
433     /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask`
434     /// returned by the downloading methods.
435     public func cancelAll() {
436         barrierQueue.sync(flags: .barrier) {
437             fetchLoads.forEach { v in
438                 let fetchLoad = v.value
439                 guard let task = fetchLoad.downloadTask else { return }
440                 cancelTaskImpl(task, fetchLoad: fetchLoad, ignoreTaskCount: true)
441             }
442         }
443     }
444 }
445
446 // MARK: - NSURLSessionDataDelegate
447
448 /// Delegate class for `NSURLSessionTaskDelegate`.
449 /// The session object will hold its delegate until it gets invalidated.
450 /// If we use `ImageDownloader` as the session delegate, it will not be released.
451 /// So we need an additional handler to break the retain cycle.
452 // See https://github.com/onevcat/Kingfisher/issues/235
453 final class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, AuthenticationChallengeResponsable {
454     
455     // The holder will keep downloader not released while a data task is being executed.
456     // It will be set when the task started, and reset when the task finished.
457     var downloadHolder: ImageDownloader?
458     
459     func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
460         
461         guard let downloader = downloadHolder else {
462             completionHandler(.cancel)
463             return
464         }
465         
466         if let statusCode = (response as? HTTPURLResponse)?.statusCode,
467            let url = dataTask.originalRequest?.url,
468             !(downloader.delegate ?? downloader).isValidStatusCode(statusCode, for: downloader)
469         {
470             let error = NSError(domain: KingfisherErrorDomain,
471                                 code: KingfisherError.invalidStatusCode.rawValue,
472                                 userInfo: [KingfisherErrorStatusCodeKey: statusCode, NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
473             callCompletionHandlerFailure(error: error, url: url)
474         }
475         
476         completionHandler(.allow)
477     }
478     
479     func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
480
481         guard let downloader = downloadHolder else {
482             return
483         }
484
485         if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
486             fetchLoad.responseData.append(data)
487             
488             if let expectedLength = dataTask.response?.expectedContentLength {
489                 for content in fetchLoad.contents {
490                     DispatchQueue.main.async {
491                         content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
492                     }
493                 }
494             }
495         }
496     }
497     
498     func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
499         
500         guard let url = task.originalRequest?.url else {
501             return
502         }
503         
504         guard error == nil else {
505             callCompletionHandlerFailure(error: error!, url: url)
506             return
507         }
508         
509         processImage(for: task, url: url)
510     }
511     
512     /**
513     This method is exposed since the compiler requests. Do not call it.
514     */
515     func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
516         guard let downloader = downloadHolder else {
517             return
518         }
519         
520         downloader.authenticationChallengeResponder?.downloader(downloader, didReceive: challenge, completionHandler: completionHandler)
521     }
522     
523     func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
524         guard let downloader = downloadHolder else {
525             return
526         }
527         
528         downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler)
529     }
530     
531     private func cleanFetchLoad(for url: URL) {
532         guard let downloader = downloadHolder else {
533             return
534         }
535
536         downloader.barrierQueue.sync(flags: .barrier) {
537             downloader.fetchLoads.removeValue(forKey: url)
538             if downloader.fetchLoads.isEmpty {
539                 downloadHolder = nil
540             }
541         }
542     }
543     
544     private func callCompletionHandlerFailure(error: Error, url: URL) {
545         guard let downloader = downloadHolder, let fetchLoad = downloader.fetchLoad(for: url) else {
546             return
547         }
548         
549         // We need to clean the fetch load first, before actually calling completion handler.
550         cleanFetchLoad(for: url)
551         
552         var leftSignal: Int
553         repeat {
554             leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0
555         } while leftSignal != 0
556         
557         for content in fetchLoad.contents {
558             content.options.callbackDispatchQueue.safeAsync {
559                 content.callback.completionHandler?(nil, error as NSError, url, nil)
560             }
561         }
562     }
563     
564     private func processImage(for task: URLSessionTask, url: URL) {
565
566         guard let downloader = downloadHolder else {
567             return
568         }
569         
570         // We are on main queue when receiving this.
571         downloader.processQueue.async {
572             
573             guard let fetchLoad = downloader.fetchLoad(for: url) else {
574                 return
575             }
576             
577             self.cleanFetchLoad(for: url)
578             
579             let data: Data?
580             let fetchedData = fetchLoad.responseData as Data
581             
582             if let delegate = downloader.delegate {
583                 data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url)
584             } else {
585                 data = fetchedData
586             }
587             
588             // Cache the processed images. So we do not need to re-process the image if using the same processor.
589             // Key is the identifier of processor.
590             var imageCache: [String: Image] = [:]
591             for content in fetchLoad.contents {
592                 
593                 let options = content.options
594                 let completionHandler = content.callback.completionHandler
595                 let callbackQueue = options.callbackDispatchQueue
596                 
597                 let processor = options.processor
598                 var image = imageCache[processor.identifier]
599                 if let data = data, image == nil {
600                     image = processor.process(item: .data(data), options: options)
601                     // Add the processed image to cache. 
602                     // If `image` is nil, nothing will happen (since the key is not existing before).
603                     imageCache[processor.identifier] = image
604                 }
605                 
606                 if let image = image {
607
608                     downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
609
610                     let imageModifier = options.imageModifier
611                     let finalImage = imageModifier.modify(image)
612
613                     if options.backgroundDecode {
614                         let decodedImage = finalImage.kf.decoded
615                         callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
616                     } else {
617                         callbackQueue.safeAsync { completionHandler?(finalImage, nil, url, data) }
618                     }
619                     
620                 } else {
621                     if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
622                         let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil)
623                         completionHandler?(nil, notModified, url, nil)
624                         continue
625                     }
626                     
627                     let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
628                     callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
629                 }
630             }
631         }
632     }
633 }
634
635 // Placeholder. For retrieving extension methods of ImageDownloaderDelegate
636 extension ImageDownloader: ImageDownloaderDelegate {}
637