ImageDownloader.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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. #if os(macOS)
  27. import AppKit
  28. #else
  29. import UIKit
  30. #endif
  31. public struct ImageDownloadResult {
  32. public let image: Image
  33. public let url: URL
  34. public let originalData: Data
  35. }
  36. public struct DownloadTask {
  37. let sessionTask: SessionDataTask
  38. let cancelToken: SessionDataTask.CancelToken
  39. public func cancel() {
  40. sessionTask.cancel(token: cancelToken)
  41. }
  42. }
  43. /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
  44. open class ImageDownloader {
  45. /// The default downloader.
  46. public static let `default` = ImageDownloader(name: "default")
  47. // MARK: - Public property
  48. /// The duration before the download is timeout. Default is 15 seconds.
  49. open var downloadTimeout: TimeInterval = 15.0
  50. /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
  51. /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't
  52. /// specify the `authenticationChallengeResponder`.
  53. ///
  54. /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of
  55. /// `authenticationChallengeResponder` will be used instead.
  56. open var trustedHosts: Set<String>?
  57. /// Use this to set supply a configuration for the downloader. By default,
  58. /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
  59. ///
  60. /// You could change the configuration before a downloading task starts.
  61. /// A configuration without persistent storage for caches is requested for downloader working correctly.
  62. open var sessionConfiguration = URLSessionConfiguration.ephemeral {
  63. didSet {
  64. session.invalidateAndCancel()
  65. session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: nil)
  66. }
  67. }
  68. /// Whether the download requests should use pipline or not. Default is false.
  69. open var requestsUsePipelining = false
  70. /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
  71. open weak var delegate: ImageDownloaderDelegate?
  72. /// A responder for authentication challenge.
  73. /// Downloader will forward the received authentication challenge for the downloading session to this responder.
  74. open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
  75. let processQueue: DispatchQueue
  76. private let sessionHandler: SessionDelegate
  77. private var session: URLSession
  78. /// Creates a downloader with name.
  79. ///
  80. /// - Parameter name: The name for the downloader. It should not be empty.
  81. public init(name: String) {
  82. if name.isEmpty {
  83. fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
  84. }
  85. processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)")
  86. sessionHandler = SessionDelegate()
  87. session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: nil)
  88. authenticationChallengeResponder = self
  89. setupSessionHandler()
  90. }
  91. deinit { session.invalidateAndCancel() }
  92. private func setupSessionHandler() {
  93. sessionHandler.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
  94. self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
  95. }
  96. sessionHandler.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
  97. self.authenticationChallengeResponder?.downloader(
  98. self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
  99. }
  100. sessionHandler.onValidStatusCode.delegate(on: self) { (self, code) in
  101. return (self.delegate ?? self).isValidStatusCode(code, for: self)
  102. }
  103. sessionHandler.onDownloadingFinished.delegate(on: self) { (self, result) in
  104. // self.delegate?.imageDownloader(
  105. // self, didFinishDownloadingImageForURL: result.value?.0, with: result.value?.1, error: result.error)
  106. }
  107. sessionHandler.onDidDownloadData.delegate(on: self) { (self, task) in
  108. guard let url = task.task.originalRequest?.url else {
  109. return task.mutableData
  110. }
  111. guard let delegate = self.delegate else {
  112. return task.mutableData
  113. }
  114. return delegate.imageDownloader(self, didDownload: task.mutableData, for: url)
  115. }
  116. }
  117. /// Download an image with a URL and option.
  118. ///
  119. /// - Parameters:
  120. /// - url: Target URL.
  121. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  122. /// - progressBlock: Called when the download progress updated.
  123. /// - completionHandler: Called when the download progress finishes.
  124. /// - Returns: A downloading task. You could call `cancel` on it to stop the downloading process.
  125. @discardableResult
  126. open func downloadImage(with url: URL,
  127. options: KingfisherOptionsInfo? = nil,
  128. progressBlock: DownloadProgressBlock? = nil,
  129. completionHandler: ((Result<ImageDownloadResult>) -> Void)? = nil) -> DownloadTask?
  130. {
  131. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
  132. request.httpShouldUsePipelining = requestsUsePipelining
  133. let options = options ?? .empty
  134. guard let r = options.modifier.modified(for: request) else {
  135. completionHandler?(.failure(KingfisherError2.requestError(reason: .emptyRequest)))
  136. return nil
  137. }
  138. request = r
  139. // There is a possibility that request modifier changed the url to `nil` or empty.
  140. guard let url = request.url, !url.absoluteString.isEmpty else {
  141. completionHandler?(.failure(KingfisherError2.requestError(reason: .invalidURL(request: request))))
  142. return nil
  143. }
  144. let onProgress = Delegate<(Int64, Int64), Void>()
  145. onProgress.delegate(on: self) { (_, progress) in
  146. let (downloaded, total) = progress
  147. progressBlock?(downloaded, total)
  148. }
  149. let onCompleted = Delegate<Result<ImageDownloadResult>, Void>()
  150. onCompleted.delegate(on: self) { (_, result) in
  151. completionHandler?(result)
  152. }
  153. let callback = SessionDataTask.TaskCallback(
  154. onProgress: onProgress, onCompleted: onCompleted, options: options)
  155. let downloadTask = sessionHandler.add(request, in: session, callback: callback)
  156. let task = downloadTask.sessionTask
  157. task.onTaskDone.delegate(on: self) { (self, done) in
  158. let (result, callbacks) = done
  159. self.delegate?.imageDownloader(
  160. self,
  161. didFinishDownloadingImageForURL: url,
  162. with: result.value?.1,
  163. error: result.error)
  164. switch result {
  165. case .success(let (data, response)):
  166. let prosessor = ImageDataProcessor(data: data, callbacks: callbacks)
  167. prosessor.onImageProcessed.delegate(on: self) { (self, result) in
  168. let (result, callback) = result
  169. if let image = result.value {
  170. self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
  171. }
  172. let imageResult = result.map { ImageDownloadResult(image: $0, url: url, originalData: data) }
  173. let queue = callback.options.callbackDispatchQueue
  174. queue.async { callback.onCompleted?.call(imageResult) }
  175. }
  176. self.processQueue.async { prosessor.process() }
  177. case .failure(let error):
  178. callbacks.forEach { callback in
  179. let queue = callback.options.callbackDispatchQueue
  180. queue.async { callback.onCompleted?.call(.failure(error)) }
  181. }
  182. }
  183. }
  184. if !task.started {
  185. delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
  186. task.resume()
  187. }
  188. return downloadTask
  189. }
  190. }
  191. // MARK: - Download method
  192. extension ImageDownloader {
  193. /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished
  194. /// downloading tasks with an NSURLErrorCancelled error.
  195. ///
  196. /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask`
  197. /// returned by the downloading methods.
  198. public func cancelAll() {
  199. sessionHandler.cancelAll()
  200. }
  201. }
  202. extension ImageDownloader: AuthenticationChallengeResponsable {}
  203. // Placeholder. For retrieving extension methods of ImageDownloaderDelegate
  204. extension ImageDownloader: ImageDownloaderDelegate {}
  205. class SessionDelegate: NSObject {
  206. private var tasks: [URL: SessionDataTask] = [:]
  207. private let lock = NSLock()
  208. let onValidStatusCode = Delegate<Int, Bool>()
  209. let onDownloadingFinished = Delegate<Result<(URL, URLResponse)>, Void>()
  210. let onDidDownloadData = Delegate<SessionDataTask, Data?>()
  211. let onReceiveSessionChallenge = Delegate<(
  212. URLSession,
  213. URLAuthenticationChallenge,
  214. (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
  215. ),
  216. Void>()
  217. let onReceiveSessionTaskChallenge = Delegate<(
  218. URLSession,
  219. URLSessionTask,
  220. URLAuthenticationChallenge,
  221. (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
  222. ),
  223. Void>()
  224. func add(
  225. _ requst: URLRequest,
  226. in session: URLSession,
  227. callback: SessionDataTask.TaskCallback) -> DownloadTask
  228. {
  229. lock.lock()
  230. defer { lock.unlock() }
  231. let url = requst.url!
  232. if let task = tasks[url] {
  233. let token = task.addCallback(callback)
  234. return DownloadTask(sessionTask: task, cancelToken: token)
  235. } else {
  236. let task = SessionDataTask(session: session, request: requst)
  237. task.onTaskCancelled.delegate(on: self) { [unowned task] (self, value) in
  238. let (token, callback) = value
  239. let error = KingfisherError2.requestError(reason: .taskCancelled(task: task, token: token))
  240. task.onTaskDone.call((.failure(error), [callback]))
  241. if !task.containsCallbacks {
  242. self.tasks[url] = nil
  243. }
  244. }
  245. let token = task.addCallback(callback)
  246. tasks[url] = task
  247. return DownloadTask(sessionTask: task, cancelToken: token)
  248. }
  249. }
  250. func remove(_ task: URLSessionTask) {
  251. guard let url = task.originalRequest?.url else {
  252. return
  253. }
  254. lock.lock()
  255. defer { lock.unlock() }
  256. tasks[url] = nil
  257. }
  258. func task(for task: URLSessionTask) -> SessionDataTask? {
  259. guard let url = task.originalRequest?.url else {
  260. return nil
  261. }
  262. guard let sessionTask = tasks[url] else {
  263. return nil
  264. }
  265. guard sessionTask.task.taskIdentifier == task.taskIdentifier else {
  266. return nil
  267. }
  268. return sessionTask
  269. }
  270. func cancelAll() {
  271. lock.lock()
  272. defer { lock.unlock() }
  273. for task in tasks.values {
  274. task.forceCancel()
  275. }
  276. }
  277. }
  278. extension SessionDelegate: URLSessionDataDelegate {
  279. func urlSession(
  280. _ session: URLSession,
  281. dataTask: URLSessionDataTask,
  282. didReceive response: URLResponse,
  283. completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
  284. {
  285. lock.lock()
  286. defer { lock.unlock() }
  287. guard let httpResponse = response as? HTTPURLResponse else {
  288. let error = KingfisherError2.responseError(reason: .invalidURLResponse(response: response))
  289. onCompleted(task: dataTask, result: .failure(error))
  290. completionHandler(.cancel)
  291. return
  292. }
  293. let httpStatusCode = httpResponse.statusCode
  294. guard onValidStatusCode.call(httpStatusCode) == true else {
  295. let error = KingfisherError2.responseError(reason: .invalidHTTPStatusCode(response: httpResponse))
  296. onCompleted(task: dataTask, result: .failure(error))
  297. completionHandler(.cancel)
  298. return
  299. }
  300. completionHandler(.allow)
  301. }
  302. func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
  303. lock.lock()
  304. defer { lock.unlock() }
  305. guard let task = self.task(for: dataTask) else {
  306. return
  307. }
  308. task.didReceiveData(data)
  309. if let expectedContentLength = dataTask.response?.expectedContentLength, expectedContentLength != -1 {
  310. DispatchQueue.main.async {
  311. task.callbacks.forEach { callback in
  312. callback.onProgress?.call((Int64(task.mutableData.count), expectedContentLength))
  313. }
  314. }
  315. }
  316. }
  317. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
  318. lock.lock()
  319. defer { lock.unlock() }
  320. guard let sessionTask = self.task(for: task) else {
  321. return
  322. }
  323. let result: Result<(Data, URLResponse?)>
  324. if let error = error {
  325. result = .failure(KingfisherError2.responseError(reason: .URLSessionError(error: error)))
  326. } else {
  327. if let data = onDidDownloadData.call(sessionTask), let finalData = data {
  328. result = .success((finalData, task.response))
  329. } else {
  330. result = .failure(KingfisherError2.responseError(reason: .dataModifyingFailed(task: sessionTask)))
  331. }
  332. }
  333. onCompleted(task: task, result: result)
  334. }
  335. func urlSession(
  336. _ session: URLSession,
  337. didReceive challenge: URLAuthenticationChallenge,
  338. completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
  339. {
  340. onReceiveSessionChallenge.call((session, challenge, completionHandler))
  341. }
  342. func urlSession(
  343. _ session: URLSession,
  344. task: URLSessionTask,
  345. didReceive challenge: URLAuthenticationChallenge,
  346. completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
  347. {
  348. onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler))
  349. }
  350. private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?)>) {
  351. guard let sessionTask = self.task(for: task) else {
  352. return
  353. }
  354. onCompleted(sessionTask: sessionTask, result: result)
  355. }
  356. private func onCompleted(sessionTask: SessionDataTask, result: Result<(Data, URLResponse?)>) {
  357. guard let url = sessionTask.task.originalRequest?.url else {
  358. return
  359. }
  360. tasks[url] = nil
  361. sessionTask.onTaskDone.call((result, Array(sessionTask.callbacks)))
  362. }
  363. }
  364. public class SessionDataTask {
  365. public typealias CancelToken = Int
  366. struct TaskCallback {
  367. let onProgress: Delegate<(Int64, Int64), Void>?
  368. let onCompleted: Delegate<Result<ImageDownloadResult>, Void>?
  369. let options: KingfisherOptionsInfo
  370. }
  371. public private(set) var mutableData: Data
  372. public let task: URLSessionDataTask
  373. private var callbacksStore = [CancelToken: TaskCallback]()
  374. var callbacks: Dictionary<SessionDataTask.CancelToken, SessionDataTask.TaskCallback>.Values {
  375. return callbacksStore.values
  376. }
  377. var currentToken = 0
  378. private let lock = NSLock()
  379. let onTaskDone = Delegate<(Result<(Data, URLResponse?)>, [TaskCallback]), Void>()
  380. let onTaskCancelled = Delegate<(CancelToken, TaskCallback), Void>()
  381. var started = false
  382. var containsCallbacks: Bool {
  383. // We should be able to use `task.state != .running` to check it.
  384. // However, in some rare cases, cancelling the task does not change
  385. // task state to `.cancelling`, but still in `.running`. So we need
  386. // to check callbacks count to for sure that it is safe to remove the
  387. // task in delegate.
  388. return !callbacks.isEmpty
  389. }
  390. init(session: URLSession, request: URLRequest) {
  391. task = session.dataTask(with: request)
  392. mutableData = Data()
  393. }
  394. func addCallback(_ callback: TaskCallback) -> CancelToken {
  395. lock.lock()
  396. defer { lock.unlock() }
  397. callbacksStore[currentToken] = callback
  398. defer { currentToken += 1 }
  399. return currentToken
  400. }
  401. func removeCallback(_ token: CancelToken) -> TaskCallback? {
  402. lock.lock()
  403. defer { lock.unlock() }
  404. if let callback = callbacksStore[token] {
  405. callbacksStore[token] = nil
  406. return callback
  407. }
  408. return nil
  409. }
  410. func resume() {
  411. started = true
  412. task.resume()
  413. }
  414. func cancel(token: CancelToken) {
  415. let result = removeCallback(token)
  416. if let callback = result {
  417. if callbacksStore.count == 0 {
  418. task.cancel()
  419. }
  420. onTaskCancelled.call((token, callback))
  421. }
  422. }
  423. func forceCancel() {
  424. for token in callbacksStore.keys {
  425. cancel(token: token)
  426. }
  427. }
  428. func didReceiveData(_ data: Data) {
  429. mutableData.append(data)
  430. }
  431. }