ImageDownloader.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. //
  2. // ImageDownloader.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/6.
  6. //
  7. // Copyright (c) 2016 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. #if os(macOS)
  27. import AppKit
  28. #else
  29. import UIKit
  30. #endif
  31. /// Progress update block of downloader.
  32. public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
  33. /// Completion block of downloader.
  34. public typealias ImageDownloaderCompletionHandler = ((_ image: Image?, _ error: NSError?, _ url: URL?, _ originalData: Data?) -> ())
  35. /// Download task.
  36. public struct RetrieveImageDownloadTask {
  37. let internalTask: URLSessionDataTask
  38. /// Downloader by which this task is intialized.
  39. public private(set) weak var ownerDownloader: ImageDownloader?
  40. /**
  41. Cancel this download task. It will trigger the completion handler with an NSURLErrorCancelled error.
  42. */
  43. public func cancel() {
  44. ownerDownloader?.cancelDownloadingTask(self)
  45. }
  46. /// The original request URL of this download task.
  47. public var url: URL? {
  48. return internalTask.originalRequest?.url
  49. }
  50. /// The relative priority of this download task.
  51. /// It represents the `priority` property of the internal `NSURLSessionTask` of this download task.
  52. /// The value for it is between 0.0~1.0. Default priority is value of 0.5.
  53. /// See documentation on `priority` of `NSURLSessionTask` for more about it.
  54. public var priority: Float {
  55. get {
  56. return internalTask.priority
  57. }
  58. set {
  59. internalTask.priority = newValue
  60. }
  61. }
  62. }
  63. private let defaultDownloaderName = "default"
  64. private let downloaderBarrierName = "com.onevcat.Kingfisher.ImageDownloader.Barrier."
  65. private let imageProcessQueueName = "com.onevcat.Kingfisher.ImageDownloader.Process."
  66. private let instance = ImageDownloader(name: defaultDownloaderName)
  67. /**
  68. The error code.
  69. - badData: The downloaded data is not an image or the data is corrupted.
  70. - notModified: The remote server responsed a 304 code. No image data downloaded.
  71. - invalidStatusCode: The HTTP status code in response is not valid.
  72. - notCached: The image rquested is not in cache but .onlyFromCache is activated.
  73. - invalidURL: The URL is invalid.
  74. - downloadCanelledBeforeStarting: The downloading task is cancelled before started.
  75. */
  76. public enum KingfisherError: Int {
  77. case badData = 10000
  78. case notModified = 10001
  79. case invalidStatusCode = 10002
  80. case notCached = 10003
  81. case invalidURL = 20000
  82. case downloadCanelledBeforeStarting = 30000
  83. }
  84. public let KingfisherErrorStatusCodeKey = "statusCode"
  85. /// Request modifier of image downloader.
  86. public protocol ImageDownloadRequestModifier {
  87. func modified(for request: URLRequest) -> URLRequest?
  88. }
  89. struct NoModifier: ImageDownloadRequestModifier {
  90. static let `default` = NoModifier()
  91. private init() {}
  92. func modified(for request: URLRequest) -> URLRequest? {
  93. return request
  94. }
  95. }
  96. /// Protocol of `ImageDownloader`.
  97. public protocol ImageDownloaderDelegate: class {
  98. /**
  99. Called when the `ImageDownloader` object successfully downloaded an image from specified URL.
  100. - parameter downloader: The `ImageDownloader` object finishes the downloading.
  101. - parameter image: Downloaded image.
  102. - parameter URL: URL of the original request URL.
  103. - parameter response: The response object of the downloading process.
  104. */
  105. func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?)
  106. /**
  107. Check if a received HTTP status code is valid or not.
  108. By default, a status code between 200 to 400 (not included) is considered as valid.
  109. If an invalid code is received, the downloader will raise an .invalidStatusCode error.
  110. It has a `userInfo` which includes this statusCode and localizedString error message.
  111. - parameter code: The received HTTP status code.
  112. - parameter downloader: The `ImageDownloader` object asking for validate status code.
  113. - returns: Whether this HTTP status code is valid or not.
  114. - Note: If the default 200 to 400 valid code does not suit your need,
  115. you can implement this method to change that behavior.
  116. */
  117. func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
  118. }
  119. extension ImageDownloaderDelegate {
  120. public func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) {}
  121. public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
  122. return (200..<400).contains(code)
  123. }
  124. }
  125. /// Protocol indicates that an authentication challenge could be handled.
  126. public protocol AuthenticationChallengeResponable: class {
  127. /**
  128. Called when an session level authentication challenge is received.
  129. This method provide a chance to handle and response to the authentication challenge before downloading could start.
  130. - parameter downloader: The downloader which receives this challenge.
  131. - parameter challenge: An object that contains the request for authentication.
  132. - parameter completionHandler: A handler that your delegate method must call.
  133. - Note: This method is a forward from `URLSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `NSURLSessionDelegate`.
  134. */
  135. func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
  136. }
  137. extension AuthenticationChallengeResponable {
  138. func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
  139. if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
  140. if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
  141. let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
  142. completionHandler(.useCredential, credential)
  143. return
  144. }
  145. }
  146. completionHandler(.performDefaultHandling, nil)
  147. }
  148. }
  149. /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
  150. public class ImageDownloader: NSObject {
  151. class ImageFetchLoad {
  152. var callbacks = [CallbackPair]()
  153. var responseData = NSMutableData()
  154. var options: KingfisherOptionsInfo?
  155. var downloadTaskCount = 0
  156. var downloadTask: RetrieveImageDownloadTask?
  157. }
  158. // MARK: - Public property
  159. /// This closure will be applied to the image download request before it being sent.
  160. /// You can modify the request for some customizing purpose, like adding auth token to the header, do basic HTTP auth or something like url mapping.
  161. @available(*, unavailable, message: "`requestModifier` is removed. Use 'urlRequest(for:byModifying:)' from the 'ImageDownloaderDelegate' instead")
  162. public var requestModifier: ((inout URLRequest) -> Void)?
  163. /// The duration before the download is timeout. Default is 15 seconds.
  164. public var downloadTimeout: TimeInterval = 15.0
  165. /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored.
  166. /// You can use this set to specify the self-signed site. It only will be used if you don't specify the `authenticationChallengeResponder`.
  167. /// If `authenticationChallengeResponder` is set, this property will be ignored and the implemention of `authenticationChallengeResponder` will be used instead.
  168. public var trustedHosts: Set<String>?
  169. /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
  170. /// You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
  171. public var sessionConfiguration = URLSessionConfiguration.ephemeral {
  172. didSet {
  173. session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: OperationQueue.main)
  174. }
  175. }
  176. /// Whether the download requests should use pipeling or not. Default is false.
  177. public var requestsUsePipeling = false
  178. fileprivate let sessionHandler: ImageDownloaderSessionHandler
  179. fileprivate var session: URLSession?
  180. /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
  181. public weak var delegate: ImageDownloaderDelegate?
  182. /// A responder for authentication challenge.
  183. /// Downloader will forward the received authentication challenge for the downloading session to this responder.
  184. public weak var authenticationChallengeResponder: AuthenticationChallengeResponable?
  185. // MARK: - Internal property
  186. let barrierQueue: DispatchQueue
  187. let processQueue: DispatchQueue
  188. typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHander: ImageDownloaderCompletionHandler?)
  189. var fetchLoads = [URL: ImageFetchLoad]()
  190. // MARK: - Public method
  191. /// The default downloader.
  192. public class var `default`: ImageDownloader {
  193. return instance
  194. }
  195. /**
  196. Init a downloader with name.
  197. - parameter name: The name for the downloader. It should not be empty.
  198. - returns: The downloader object.
  199. */
  200. public init(name: String) {
  201. if name.isEmpty {
  202. fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
  203. }
  204. barrierQueue = DispatchQueue(label: downloaderBarrierName + name, attributes: .concurrent)
  205. processQueue = DispatchQueue(label: imageProcessQueueName + name, attributes: .concurrent)
  206. sessionHandler = ImageDownloaderSessionHandler()
  207. super.init()
  208. // Provide a default implement for challenge responder.
  209. authenticationChallengeResponder = sessionHandler
  210. session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: .main)
  211. }
  212. func fetchLoad(for url: URL) -> ImageFetchLoad? {
  213. var fetchLoad: ImageFetchLoad?
  214. barrierQueue.sync { fetchLoad = fetchLoads[url] }
  215. return fetchLoad
  216. }
  217. }
  218. // MARK: - Download method
  219. extension ImageDownloader {
  220. /**
  221. Download an image with a URL.
  222. - parameter url: Target URL.
  223. - parameter progressBlock: Called when the download progress updated.
  224. - parameter completionHandler: Called when the download progress finishes.
  225. - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
  226. */
  227. @discardableResult
  228. public func downloadImage(with url: URL,
  229. progressBlock: ImageDownloaderProgressBlock?,
  230. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  231. {
  232. return downloadImage(with: url, options: nil, progressBlock: progressBlock, completionHandler: completionHandler)
  233. }
  234. /**
  235. Download an image with a URL and option.
  236. - parameter url: Target URL.
  237. - parameter options: The options could control download behavior. See `KingfisherOptionsInfo`.
  238. - parameter progressBlock: Called when the download progress updated.
  239. - parameter completionHandler: Called when the download progress finishes.
  240. - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
  241. */
  242. @discardableResult
  243. public func downloadImage(with url: URL,
  244. options: KingfisherOptionsInfo?,
  245. progressBlock: ImageDownloaderProgressBlock?,
  246. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  247. {
  248. return downloadImage(with: url,
  249. retrieveImageTask: nil,
  250. options: options,
  251. progressBlock: progressBlock,
  252. completionHandler: completionHandler)
  253. }
  254. func downloadImage(with url: URL,
  255. retrieveImageTask: RetrieveImageTask?,
  256. options: KingfisherOptionsInfo?,
  257. progressBlock: ImageDownloaderProgressBlock?,
  258. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  259. {
  260. if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
  261. return nil
  262. }
  263. let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
  264. // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
  265. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
  266. request.httpShouldUsePipelining = requestsUsePipeling
  267. if let modifier = options?.modifier {
  268. guard let r = modifier.modified(for: request) else {
  269. completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCanelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
  270. return nil
  271. }
  272. request = r
  273. }
  274. // There is a possiblility that request modifier changed the url to `nil` or empty.
  275. guard let url = request.url, !url.absoluteString.isEmpty else {
  276. completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
  277. return nil
  278. }
  279. var downloadTask: RetrieveImageDownloadTask?
  280. setup(progressBlock: progressBlock, with: completionHandler, for: url) {(session, fetchLoad) -> Void in
  281. if fetchLoad.downloadTask == nil {
  282. let dataTask = session.dataTask(with: request)
  283. fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
  284. fetchLoad.options = options
  285. dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
  286. dataTask.resume()
  287. // Hold self while the task is executing.
  288. self.sessionHandler.downloadHolder = self
  289. }
  290. fetchLoad.downloadTaskCount += 1
  291. downloadTask = fetchLoad.downloadTask
  292. retrieveImageTask?.downloadTask = downloadTask
  293. }
  294. return downloadTask
  295. }
  296. // A single key may have multiple callbacks. Only download once.
  297. func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, started: ((URLSession, ImageFetchLoad) -> Void)) {
  298. barrierQueue.sync(flags: .barrier) {
  299. let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
  300. let callbackPair = (progressBlock: progressBlock, completionHander: completionHandler)
  301. loadObjectForURL.callbacks.append(callbackPair)
  302. fetchLoads[url] = loadObjectForURL
  303. if let session = session {
  304. started(session, loadObjectForURL)
  305. }
  306. }
  307. }
  308. func cancelDownloadingTask(_ task: RetrieveImageDownloadTask) {
  309. barrierQueue.sync {
  310. if let URL = task.internalTask.originalRequest?.url, let imageFetchLoad = self.fetchLoads[URL] {
  311. imageFetchLoad.downloadTaskCount -= 1
  312. if imageFetchLoad.downloadTaskCount == 0 {
  313. task.internalTask.cancel()
  314. }
  315. }
  316. }
  317. }
  318. func clean(for url: URL) {
  319. barrierQueue.sync(flags: .barrier) {
  320. fetchLoads.removeValue(forKey: url)
  321. return
  322. }
  323. }
  324. }
  325. // MARK: - NSURLSessionDataDelegate
  326. /// Delegate class for `NSURLSessionTaskDelegate`.
  327. /// The session object will hold its delegate until it gets invalidated.
  328. /// If we use `ImageDownloader` as the session delegate, it will not be released.
  329. /// So we need an additional handler to break the retain cycle.
  330. // See https://github.com/onevcat/Kingfisher/issues/235
  331. class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, AuthenticationChallengeResponable {
  332. // The holder will keep downloader not released while a data task is being executed.
  333. // It will be set when the task started, and reset when the task finished.
  334. var downloadHolder: ImageDownloader?
  335. func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
  336. guard let downloader = downloadHolder else {
  337. completionHandler(.cancel)
  338. return
  339. }
  340. if let statusCode = (response as? HTTPURLResponse)?.statusCode,
  341. let url = dataTask.originalRequest?.url,
  342. !(downloader.delegate ?? downloader).isValidStatusCode(statusCode, for: downloader)
  343. {
  344. callback(with: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidStatusCode.rawValue, userInfo: [KingfisherErrorStatusCodeKey: statusCode, NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)]), url: url, originalData: nil)
  345. }
  346. completionHandler(.allow)
  347. }
  348. func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
  349. guard let downloader = downloadHolder else {
  350. return
  351. }
  352. if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
  353. fetchLoad.responseData.append(data)
  354. for callbackPair in fetchLoad.callbacks {
  355. DispatchQueue.main.async(execute: { () -> Void in
  356. callbackPair.progressBlock?(Int64(fetchLoad.responseData.length), dataTask.response!.expectedContentLength)
  357. })
  358. }
  359. }
  360. }
  361. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
  362. if let url = task.originalRequest?.url {
  363. if let error = error { // Error happened
  364. callback(with: nil, error: error as NSError, url: url, originalData: nil)
  365. } else { //Download finished without error
  366. processImage(for: task, url: url)
  367. }
  368. }
  369. }
  370. /**
  371. This method is exposed since the compiler requests. Do not call it.
  372. */
  373. internal func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
  374. guard let downloader = downloadHolder else {
  375. return
  376. }
  377. downloader.authenticationChallengeResponder?.downloader(downloader, didReceive: challenge, completionHandler: completionHandler)
  378. }
  379. private func callback(with image: Image?, error: NSError?, url: URL, originalData: Data?) {
  380. guard let downloader = downloadHolder else {
  381. return
  382. }
  383. if let callbackPairs = downloader.fetchLoad(for: url)?.callbacks {
  384. let options = downloader.fetchLoad(for: url)?.options ?? KingfisherEmptyOptionsInfo
  385. downloader.clean(for: url)
  386. for callbackPair in callbackPairs {
  387. options.callbackDispatchQueue.safeAsync {
  388. callbackPair.completionHander?(image, error, url, originalData)
  389. }
  390. }
  391. if downloader.fetchLoads.isEmpty {
  392. downloadHolder = nil
  393. }
  394. }
  395. }
  396. private func processImage(for task: URLSessionTask, url: URL) {
  397. guard let downloader = downloadHolder else {
  398. return
  399. }
  400. // We are on main queue when receiving this.
  401. downloader.processQueue.async {
  402. guard let fetchLoad = downloader.fetchLoad(for: url) else {
  403. self.callback(with: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil), url: url, originalData: nil)
  404. return
  405. }
  406. let options = fetchLoad.options ?? KingfisherEmptyOptionsInfo
  407. let data = fetchLoad.responseData as Data
  408. if let image = options.processor.process(item: .data(data), options: options) {
  409. downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
  410. if options.backgroundDecode {
  411. self.callback(with: image.kf_decoded(scale: options.scaleFactor), error: nil, url: url, originalData: data)
  412. } else {
  413. self.callback(with: image, error: nil, url: url, originalData: data)
  414. }
  415. } else {
  416. // If server response is 304 (Not Modified), inform the callback handler with NotModified error.
  417. // It should be handled to get an image from cache, which is response of a manager object.
  418. if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
  419. self.callback(with: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil), url: url, originalData: nil)
  420. return
  421. }
  422. self.callback(with: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil), url: url, originalData: nil)
  423. }
  424. }
  425. }
  426. }
  427. // Placeholder. For retrieving extension methods of ImageDownloaderDelegate
  428. extension ImageDownloader: ImageDownloaderDelegate {}