ImageDownloader.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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. /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
  90. public class ImageDownloader: NSObject {
  91. class ImageFetchLoad {
  92. var callbacks = [CallbackPair]()
  93. var responseData = NSMutableData()
  94. var options: KingfisherOptionsInfo?
  95. var downloadTaskCount = 0
  96. var downloadTask: RetrieveImageDownloadTask?
  97. }
  98. // MARK: - Public property
  99. /// 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.
  100. public var requestModifier: (NSMutableURLRequest -> Void)?
  101. /// The duration before the download is timeout. Default is 15 seconds.
  102. public var downloadTimeout: NSTimeInterval = 15.0
  103. /// 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.
  104. public var trustedHosts: Set<String>?
  105. /// 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.
  106. public var sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration() {
  107. didSet {
  108. session = NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
  109. }
  110. }
  111. private var session: NSURLSession?
  112. /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
  113. public weak var delegate: ImageDownloaderDelegate?
  114. // MARK: - Internal property
  115. let barrierQueue: dispatch_queue_t
  116. let processQueue: dispatch_queue_t
  117. typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHander: ImageDownloaderCompletionHandler?)
  118. var fetchLoads = [NSURL: ImageFetchLoad]()
  119. // MARK: - Public method
  120. /// The default downloader.
  121. public class var defaultDownloader: ImageDownloader {
  122. return instance
  123. }
  124. /**
  125. Init a downloader with name.
  126. - parameter name: The name for the downloader. It should not be empty.
  127. - returns: The downloader object.
  128. */
  129. public init(name: String) {
  130. if name.isEmpty {
  131. fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
  132. }
  133. barrierQueue = dispatch_queue_create(downloaderBarrierName + name, DISPATCH_QUEUE_CONCURRENT)
  134. processQueue = dispatch_queue_create(imageProcessQueueName + name, DISPATCH_QUEUE_CONCURRENT)
  135. super.init()
  136. session = NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
  137. }
  138. func fetchLoadForKey(key: NSURL) -> ImageFetchLoad? {
  139. var fetchLoad: ImageFetchLoad?
  140. dispatch_sync(barrierQueue, { () -> Void in
  141. fetchLoad = self.fetchLoads[key]
  142. })
  143. return fetchLoad
  144. }
  145. }
  146. // MARK: - Download method
  147. extension ImageDownloader {
  148. /**
  149. Download an image with a URL.
  150. - parameter URL: Target URL.
  151. - parameter progressBlock: Called when the download progress updated.
  152. - parameter completionHandler: Called when the download progress finishes.
  153. - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
  154. */
  155. public func downloadImageWithURL(URL: NSURL,
  156. progressBlock: ImageDownloaderProgressBlock?,
  157. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  158. {
  159. return downloadImageWithURL(URL, options: nil, progressBlock: progressBlock, completionHandler: completionHandler)
  160. }
  161. /**
  162. Download an image with a URL and option.
  163. - parameter URL: Target URL.
  164. - parameter options: The options could control download behavior. See `KingfisherOptionsInfo`.
  165. - parameter progressBlock: Called when the download progress updated.
  166. - parameter completionHandler: Called when the download progress finishes.
  167. - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
  168. */
  169. public func downloadImageWithURL(URL: NSURL,
  170. options: KingfisherOptionsInfo?,
  171. progressBlock: ImageDownloaderProgressBlock?,
  172. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  173. {
  174. return downloadImageWithURL(URL,
  175. retrieveImageTask: nil,
  176. options: options,
  177. progressBlock: progressBlock,
  178. completionHandler: completionHandler)
  179. }
  180. internal func downloadImageWithURL(URL: NSURL,
  181. retrieveImageTask: RetrieveImageTask?,
  182. options: KingfisherOptionsInfo?,
  183. progressBlock: ImageDownloaderProgressBlock?,
  184. completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
  185. {
  186. if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelledBeforeDownlodStarting {
  187. return nil
  188. }
  189. let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
  190. // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
  191. let request = NSMutableURLRequest(URL: URL, cachePolicy: .ReloadIgnoringLocalCacheData, timeoutInterval: timeout)
  192. request.HTTPShouldUsePipelining = true
  193. self.requestModifier?(request)
  194. // There is a possiblility that request modifier changed the url to `nil` or empty.
  195. if request.URL == nil || request.URL!.absoluteString.isEmpty {
  196. completionHandler?(image: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.InvalidURL.rawValue, userInfo: nil), imageURL: nil, originalData: nil)
  197. return nil
  198. }
  199. var downloadTask: RetrieveImageDownloadTask?
  200. setupProgressBlock(progressBlock, completionHandler: completionHandler, forURL: request.URL!) {(session, fetchLoad) -> Void in
  201. if fetchLoad.downloadTask == nil {
  202. let dataTask = session.dataTaskWithRequest(request)
  203. fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
  204. fetchLoad.options = options
  205. dataTask.priority = options?.downloadPriority ?? NSURLSessionTaskPriorityDefault
  206. dataTask.resume()
  207. }
  208. fetchLoad.downloadTaskCount += 1
  209. downloadTask = fetchLoad.downloadTask
  210. retrieveImageTask?.downloadTask = downloadTask
  211. }
  212. return downloadTask
  213. }
  214. // A single key may have multiple callbacks. Only download once.
  215. internal func setupProgressBlock(progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?, forURL URL: NSURL, started: ((NSURLSession, ImageFetchLoad) -> Void)) {
  216. dispatch_barrier_sync(barrierQueue, { () -> Void in
  217. let loadObjectForURL = self.fetchLoads[URL] ?? ImageFetchLoad()
  218. let callbackPair = (progressBlock: progressBlock, completionHander: completionHandler)
  219. loadObjectForURL.callbacks.append(callbackPair)
  220. self.fetchLoads[URL] = loadObjectForURL
  221. if let session = self.session {
  222. started(session, loadObjectForURL)
  223. }
  224. })
  225. }
  226. func cancelDownloadingTask(task: RetrieveImageDownloadTask) {
  227. dispatch_barrier_sync(barrierQueue) { () -> Void in
  228. if let URL = task.internalTask.originalRequest?.URL, imageFetchLoad = self.fetchLoads[URL] {
  229. imageFetchLoad.downloadTaskCount -= 1
  230. if imageFetchLoad.downloadTaskCount == 0 {
  231. task.internalTask.cancel()
  232. }
  233. }
  234. }
  235. }
  236. func cleanForURL(URL: NSURL) {
  237. dispatch_barrier_sync(barrierQueue, { () -> Void in
  238. self.fetchLoads.removeValueForKey(URL)
  239. return
  240. })
  241. }
  242. }
  243. // MARK: - NSURLSessionTaskDelegate
  244. extension ImageDownloader: NSURLSessionDataDelegate {
  245. /**
  246. This method is exposed since the compiler requests. Do not call it.
  247. */
  248. public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
  249. completionHandler(NSURLSessionResponseDisposition.Allow)
  250. }
  251. /**
  252. This method is exposed since the compiler requests. Do not call it.
  253. */
  254. public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
  255. if let URL = dataTask.originalRequest?.URL, fetchLoad = fetchLoadForKey(URL) {
  256. fetchLoad.responseData.appendData(data)
  257. for callbackPair in fetchLoad.callbacks {
  258. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  259. callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength)
  260. })
  261. }
  262. }
  263. }
  264. /**
  265. This method is exposed since the compiler requests. Do not call it.
  266. */
  267. public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
  268. if let URL = task.originalRequest?.URL {
  269. if let error = error { // Error happened
  270. callbackWithImage(nil, error: error, imageURL: URL, originalData: nil)
  271. } else { //Download finished without error
  272. processImageForTask(task, URL: URL)
  273. }
  274. }
  275. }
  276. /**
  277. This method is exposed since the compiler requests. Do not call it.
  278. */
  279. public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
  280. if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
  281. if let trustedHosts = trustedHosts where trustedHosts.contains(challenge.protectionSpace.host) {
  282. let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
  283. completionHandler(.UseCredential, credential)
  284. return
  285. }
  286. }
  287. completionHandler(.PerformDefaultHandling, nil)
  288. }
  289. private func callbackWithImage(image: Image?, error: NSError?, imageURL: NSURL, originalData: NSData?) {
  290. if let callbackPairs = fetchLoadForKey(imageURL)?.callbacks {
  291. let options = fetchLoadForKey(imageURL)?.options ?? KingfisherEmptyOptionsInfo
  292. self.cleanForURL(imageURL)
  293. for callbackPair in callbackPairs {
  294. dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in
  295. callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData)
  296. })
  297. }
  298. }
  299. }
  300. private func processImageForTask(task: NSURLSessionTask, URL: NSURL) {
  301. // We are on main queue when receiving this.
  302. dispatch_async(processQueue, { () -> Void in
  303. if let fetchLoad = self.fetchLoadForKey(URL) {
  304. let options = fetchLoad.options ?? KingfisherEmptyOptionsInfo
  305. if let image = Image.kf_imageWithData(fetchLoad.responseData, scale: options.scaleFactor) {
  306. self.delegate?.imageDownloader?(self, didDownloadImage: image, forURL: URL, withResponse: task.response!)
  307. if options.backgroundDecode {
  308. self.callbackWithImage(image.kf_decodedImage(scale: options.scaleFactor), error: nil, imageURL: URL, originalData: fetchLoad.responseData)
  309. } else {
  310. self.callbackWithImage(image, error: nil, imageURL: URL, originalData: fetchLoad.responseData)
  311. }
  312. } else {
  313. // If server response is 304 (Not Modified), inform the callback handler with NotModified error.
  314. // It should be handled to get an image from cache, which is response of a manager object.
  315. if let res = task.response as? NSHTTPURLResponse where res.statusCode == 304 {
  316. self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.NotModified.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
  317. return
  318. }
  319. self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
  320. }
  321. } else {
  322. self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
  323. }
  324. })
  325. }
  326. }