ImageDownloader.swift 19 KB

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