ImageDownloader.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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(OSX)
  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?, imageURL: NSURL?, originalData: NSData?) -> ())
  35. /// Download task.
  36. public struct RetrieveImageDownloadTask {
  37. let internalTask: NSURLSessionDataTask
  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: NSURL? {
  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. - InvalidURL: The URL is invalid.
  72. */
  73. public enum KingfisherError: Int {
  74. case BadData = 10000
  75. case NotModified = 10001
  76. case InvalidURL = 20000
  77. }
  78. /// Protocol of `ImageDownloader`.
  79. @objc public protocol ImageDownloaderDelegate {
  80. /**
  81. Called when the `ImageDownloader` object successfully downloaded an image from specified URL.
  82. - parameter downloader: The `ImageDownloader` object finishes the downloading.
  83. - parameter image: Downloaded image.
  84. - parameter URL: URL of the original request URL.
  85. - parameter response: The response object of the downloading process.
  86. */
  87. optional func imageDownloader(downloader: ImageDownloader, didDownloadImage image: Image, forURL URL: NSURL, withResponse response: NSURLResponse)
  88. }
  89. /// Protocol indicates that an authentication challenge could be handled.
  90. public protocol AuthenticationChallengeResponable: class {
  91. /**
  92. Called when an session level authentication challenge is received.
  93. This method provide a chance to handle and response to the authentication challenge before downloading could start.
  94. - parameter downloader: The downloader which receives this challenge.
  95. - parameter challenge: An object that contains the request for authentication.
  96. - parameter completionHandler: A handler that your delegate method must call.
  97. - Note: This method is a forward from `URLSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `NSURLSessionDelegate`.
  98. */
  99. func downloder(downloader: ImageDownloader, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void)
  100. }
  101. extension AuthenticationChallengeResponable {
  102. func downloder(downloader: ImageDownloader, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
  103. if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
  104. if let trustedHosts = downloader.trustedHosts where trustedHosts.contains(challenge.protectionSpace.host) {
  105. let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
  106. completionHandler(.UseCredential, credential)
  107. return
  108. }
  109. }
  110. completionHandler(.PerformDefaultHandling, nil)
  111. }
  112. }
  113. /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
  114. public class ImageDownloader: NSObject {
  115. class ImageFetchLoad {
  116. var callbacks = [CallbackPair]()
  117. var responseData = NSMutableData()
  118. var options: KingfisherOptionsInfo?
  119. var downloadTaskCount = 0
  120. var downloadTask: RetrieveImageDownloadTask?
  121. }
  122. // MARK: - Public property
  123. /// This closure will be applied to the image download request before it being sent. You can modify the request for some customizing purpose, like adding auth token to the header or do a url mapping.
  124. public var requestModifier: (NSMutableURLRequest -> Void)?
  125. /// The duration before the download is timeout. Default is 15 seconds.
  126. public var downloadTimeout: NSTimeInterval = 15.0
  127. /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't specify the `authenticationChallengeResponder`. If `authenticationChallengeResponder` is set, this property will be ignored and the implemention of `authenticationChallengeResponder` will be used instead.
  128. public var trustedHosts: Set<String>?
  129. /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
  130. public var sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration() {
  131. didSet {
  132. session = NSURLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: NSOperationQueue.mainQueue())
  133. }
  134. }
  135. /// Whether the download requests should use pipeling or not. Default is false.
  136. public var requestsUsePipeling = false
  137. private let sessionHandler: ImageDownloaderSessionHandler
  138. private var session: NSURLSession?
  139. /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
  140. public weak var delegate: ImageDownloaderDelegate?
  141. /// A responder for authentication challenge.
  142. /// Downloader will forward the received authentication challenge for the downloading session to this responder.
  143. public weak var authenticationChallengeResponder: AuthenticationChallengeResponable?
  144. // MARK: - Internal property
  145. let barrierQueue: dispatch_queue_t
  146. let processQueue: dispatch_queue_t
  147. typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHander: ImageDownloaderCompletionHandler?)
  148. var fetchLoads = [NSURL: ImageFetchLoad]()
  149. // MARK: - Public method
  150. /// The default downloader.
  151. public class var defaultDownloader: ImageDownloader {
  152. return instance
  153. }
  154. /**
  155. Init a downloader with name.
  156. - parameter name: The name for the downloader. It should not be empty.
  157. - returns: The downloader object.
  158. */
  159. public init(name: String) {
  160. if name.isEmpty {
  161. fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
  162. }
  163. barrierQueue = dispatch_queue_create(downloaderBarrierName + name, DISPATCH_QUEUE_CONCURRENT)
  164. processQueue = dispatch_queue_create(imageProcessQueueName + name, DISPATCH_QUEUE_CONCURRENT)
  165. sessionHandler = ImageDownloaderSessionHandler()
  166. super.init()
  167. // Provide a default implement for challenge responder.
  168. authenticationChallengeResponder = sessionHandler
  169. session = NSURLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: NSOperationQueue.mainQueue())
  170. }
  171. func fetchLoadForKey(key: NSURL) -> ImageFetchLoad? {
  172. var fetchLoad: ImageFetchLoad?
  173. dispatch_sync(barrierQueue, { () -> Void in
  174. fetchLoad = self.fetchLoads[key]
  175. })
  176. return fetchLoad
  177. }
  178. }
  179. // MARK: - Download method
  180. extension ImageDownloader {
  181. /**
  182. Download an image with a URL.
  183. - parameter URL: Target URL.
  184. - parameter progressBlock: Called when the download progress updated.
  185. - parameter completionHandler: Called when the download progress finishes.
  186. - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
  187. */
  188. public func downloadImageWithURL(URL: NSURL,
  189. progressBlock: ImageDownloaderProgressBlock?,
  190. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  191. {
  192. return downloadImageWithURL(URL, options: nil, progressBlock: progressBlock, completionHandler: completionHandler)
  193. }
  194. /**
  195. Download an image with a URL and option.
  196. - parameter URL: Target URL.
  197. - parameter options: The options could control download behavior. See `KingfisherOptionsInfo`.
  198. - parameter progressBlock: Called when the download progress updated.
  199. - parameter completionHandler: Called when the download progress finishes.
  200. - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
  201. */
  202. public func downloadImageWithURL(URL: NSURL,
  203. options: KingfisherOptionsInfo?,
  204. progressBlock: ImageDownloaderProgressBlock?,
  205. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  206. {
  207. return downloadImageWithURL(URL,
  208. retrieveImageTask: nil,
  209. options: options,
  210. progressBlock: progressBlock,
  211. completionHandler: completionHandler)
  212. }
  213. internal func downloadImageWithURL(URL: NSURL,
  214. retrieveImageTask: RetrieveImageTask?,
  215. options: KingfisherOptionsInfo?,
  216. progressBlock: ImageDownloaderProgressBlock?,
  217. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  218. {
  219. if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelledBeforeDownlodStarting {
  220. return nil
  221. }
  222. let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
  223. // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
  224. let request = NSMutableURLRequest(URL: URL, cachePolicy: .ReloadIgnoringLocalCacheData, timeoutInterval: timeout)
  225. request.HTTPShouldUsePipelining = requestsUsePipeling
  226. self.requestModifier?(request)
  227. // There is a possiblility that request modifier changed the url to `nil` or empty.
  228. if request.URL == nil || request.URL!.absoluteString.isEmpty {
  229. completionHandler?(image: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.InvalidURL.rawValue, userInfo: nil), imageURL: nil, originalData: nil)
  230. return nil
  231. }
  232. var downloadTask: RetrieveImageDownloadTask?
  233. setupProgressBlock(progressBlock, completionHandler: completionHandler, forURL: request.URL!) {(session, fetchLoad) -> Void in
  234. if fetchLoad.downloadTask == nil {
  235. let dataTask = session.dataTaskWithRequest(request)
  236. fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
  237. fetchLoad.options = options
  238. dataTask.priority = options?.downloadPriority ?? NSURLSessionTaskPriorityDefault
  239. dataTask.resume()
  240. // Hold self while the task is executing.
  241. self.sessionHandler.downloadHolder = self
  242. }
  243. fetchLoad.downloadTaskCount += 1
  244. downloadTask = fetchLoad.downloadTask
  245. retrieveImageTask?.downloadTask = downloadTask
  246. }
  247. return downloadTask
  248. }
  249. // A single key may have multiple callbacks. Only download once.
  250. internal func setupProgressBlock(progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?, forURL URL: NSURL, started: ((NSURLSession, ImageFetchLoad) -> Void)) {
  251. dispatch_barrier_sync(barrierQueue, { () -> Void in
  252. let loadObjectForURL = self.fetchLoads[URL] ?? ImageFetchLoad()
  253. let callbackPair = (progressBlock: progressBlock, completionHander: completionHandler)
  254. loadObjectForURL.callbacks.append(callbackPair)
  255. self.fetchLoads[URL] = loadObjectForURL
  256. if let session = self.session {
  257. started(session, loadObjectForURL)
  258. }
  259. })
  260. }
  261. func cancelDownloadingTask(task: RetrieveImageDownloadTask) {
  262. dispatch_barrier_sync(barrierQueue) { () -> Void in
  263. if let URL = task.internalTask.originalRequest?.URL, imageFetchLoad = self.fetchLoads[URL] {
  264. imageFetchLoad.downloadTaskCount -= 1
  265. if imageFetchLoad.downloadTaskCount == 0 {
  266. task.internalTask.cancel()
  267. }
  268. }
  269. }
  270. }
  271. func cleanForURL(URL: NSURL) {
  272. dispatch_barrier_sync(barrierQueue, { () -> Void in
  273. self.fetchLoads.removeValueForKey(URL)
  274. return
  275. })
  276. }
  277. }
  278. // MARK: - NSURLSessionDataDelegate
  279. // See https://github.com/onevcat/Kingfisher/issues/235
  280. /// Delegate class for `NSURLSessionTaskDelegate`.
  281. /// The session object will hold its delegate until it gets invalidated.
  282. /// If we use `ImageDownloader` as the session delegate, it will not be released.
  283. /// So we need an additional handler to break the retain cycle.
  284. class ImageDownloaderSessionHandler: NSObject, NSURLSessionDataDelegate, AuthenticationChallengeResponable {
  285. // The holder will keep downloader not released while a data task is being executed.
  286. // It will be set when the task started, and reset when the task finished.
  287. var downloadHolder: ImageDownloader?
  288. /**
  289. This method is exposed since the compiler requests. Do not call it.
  290. */
  291. internal func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
  292. completionHandler(NSURLSessionResponseDisposition.Allow)
  293. }
  294. /**
  295. This method is exposed since the compiler requests. Do not call it.
  296. */
  297. internal func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
  298. guard let downloader = downloadHolder else {
  299. return
  300. }
  301. if let URL = dataTask.originalRequest?.URL, fetchLoad = downloader.fetchLoadForKey(URL) {
  302. fetchLoad.responseData.appendData(data)
  303. for callbackPair in fetchLoad.callbacks {
  304. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  305. callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength)
  306. })
  307. }
  308. }
  309. }
  310. /**
  311. This method is exposed since the compiler requests. Do not call it.
  312. */
  313. internal func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
  314. if let URL = task.originalRequest?.URL {
  315. if let error = error { // Error happened
  316. callbackWithImage(nil, error: error, imageURL: URL, originalData: nil)
  317. } else { //Download finished without error
  318. processImageForTask(task, URL: URL)
  319. }
  320. }
  321. }
  322. /**
  323. This method is exposed since the compiler requests. Do not call it.
  324. */
  325. internal func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
  326. guard let downloader = downloadHolder else {
  327. return
  328. }
  329. downloader.authenticationChallengeResponder?.downloder(downloader, didReceiveChallenge: challenge, completionHandler: completionHandler)
  330. }
  331. private func callbackWithImage(image: Image?, error: NSError?, imageURL: NSURL, originalData: NSData?) {
  332. guard let downloader = downloadHolder else {
  333. return
  334. }
  335. if let callbackPairs = downloader.fetchLoadForKey(imageURL)?.callbacks {
  336. let options = downloader.fetchLoadForKey(imageURL)?.options ?? KingfisherEmptyOptionsInfo
  337. downloader.cleanForURL(imageURL)
  338. for callbackPair in callbackPairs {
  339. dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in
  340. callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData)
  341. })
  342. }
  343. if downloader.fetchLoads.isEmpty {
  344. downloadHolder = nil
  345. }
  346. }
  347. }
  348. private func processImageForTask(task: NSURLSessionTask, URL: NSURL) {
  349. guard let downloader = downloadHolder else {
  350. return
  351. }
  352. // We are on main queue when receiving this.
  353. dispatch_async(downloader.processQueue, { () -> Void in
  354. if let fetchLoad = downloader.fetchLoadForKey(URL) {
  355. let options = fetchLoad.options ?? KingfisherEmptyOptionsInfo
  356. if let image = Image.kf_imageWithData(fetchLoad.responseData, scale: options.scaleFactor) {
  357. downloader.delegate?.imageDownloader?(downloader, didDownloadImage: image, forURL: URL, withResponse: task.response!)
  358. if options.backgroundDecode {
  359. self.callbackWithImage(image.kf_decodedImage(scale: options.scaleFactor), error: nil, imageURL: URL, originalData: fetchLoad.responseData)
  360. } else {
  361. self.callbackWithImage(image, error: nil, imageURL: URL, originalData: fetchLoad.responseData)
  362. }
  363. } else {
  364. // If server response is 304 (Not Modified), inform the callback handler with NotModified error.
  365. // It should be handled to get an image from cache, which is response of a manager object.
  366. if let res = task.response as? NSHTTPURLResponse where res.statusCode == 304 {
  367. self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.NotModified.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
  368. return
  369. }
  370. self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
  371. }
  372. } else {
  373. self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
  374. }
  375. })
  376. }
  377. }