ImageDownloader.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. //
  2. // ImageDownloader.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/6.
  6. //
  7. // Copyright (c) 2019 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. typealias DownloadResult = Result<ImageLoadingResult, KingfisherError>
  32. /// Represents a success result of an image downloading progress.
  33. public struct ImageLoadingResult {
  34. /// The downloaded image.
  35. public let image: KFCrossPlatformImage
  36. /// Original URL of the image request.
  37. public let url: URL?
  38. /// The raw data received from downloader.
  39. public let originalData: Data
  40. /// Creates an `ImageDownloadResult`
  41. ///
  42. /// - parameter image: Image of the download result
  43. /// - parameter url: URL from where the image was downloaded from
  44. /// - parameter originalData: The image's binary data
  45. public init(image: KFCrossPlatformImage, url: URL? = nil, originalData: Data) {
  46. self.image = image
  47. self.url = url
  48. self.originalData = originalData
  49. }
  50. }
  51. /// Represents a task of an image downloading process.
  52. public struct DownloadTask {
  53. /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer
  54. /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task
  55. /// for the same URL resource at the same time.
  56. ///
  57. /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through.
  58. /// You can use them to identify the cancelled task.
  59. public let sessionTask: SessionDataTask
  60. /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled.
  61. /// To cancel a `DownloadTask`, use `cancel` instead.
  62. public let cancelToken: SessionDataTask.CancelToken
  63. /// Cancel this task if it is running. It will do nothing if this task is not running.
  64. ///
  65. /// - Note:
  66. /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being
  67. /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created
  68. /// and returned when you call related methods, but it will share the session downloading task with a previous task.
  69. /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask`
  70. /// does not affect other `DownloadTask`s.
  71. ///
  72. /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel
  73. /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`.
  74. public func cancel() {
  75. sessionTask.cancel(token: cancelToken)
  76. }
  77. }
  78. actor CancellationDownloadTask {
  79. var task: DownloadTask?
  80. func setTask(_ task: DownloadTask?) {
  81. self.task = task
  82. }
  83. }
  84. extension DownloadTask {
  85. enum WrappedTask {
  86. case download(DownloadTask)
  87. case dataProviding
  88. func cancel() {
  89. switch self {
  90. case .download(let task): task.cancel()
  91. case .dataProviding: break
  92. }
  93. }
  94. var value: DownloadTask? {
  95. switch self {
  96. case .download(let task): return task
  97. case .dataProviding: return nil
  98. }
  99. }
  100. }
  101. }
  102. /// Represents a downloading manager for requesting the image with a URL from server.
  103. open class ImageDownloader {
  104. // MARK: Singleton
  105. /// The default downloader.
  106. public static let `default` = ImageDownloader(name: "default")
  107. // MARK: Public Properties
  108. /// The duration before the downloading is timeout. Default is 15 seconds.
  109. open var downloadTimeout: TimeInterval = 15.0
  110. /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
  111. /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't
  112. /// specify the `authenticationChallengeResponder`.
  113. ///
  114. /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of
  115. /// `authenticationChallengeResponder` will be used instead.
  116. open var trustedHosts: Set<String>?
  117. /// Use this to set supply a configuration for the downloader. By default,
  118. /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
  119. ///
  120. /// You could change the configuration before a downloading task starts.
  121. /// A configuration without persistent storage for caches is requested for downloader working correctly.
  122. open var sessionConfiguration = URLSessionConfiguration.ephemeral {
  123. didSet {
  124. session.invalidateAndCancel()
  125. session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
  126. }
  127. }
  128. open var sessionDelegate: SessionDelegate {
  129. didSet {
  130. session.invalidateAndCancel()
  131. session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
  132. setupSessionHandler()
  133. }
  134. }
  135. /// Whether the download requests should use pipeline or not. Default is false.
  136. open var requestsUsePipelining = false
  137. /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
  138. open weak var delegate: ImageDownloaderDelegate?
  139. /// A responder for authentication challenge.
  140. /// Downloader will forward the received authentication challenge for the downloading session to this responder.
  141. open weak var authenticationChallengeResponder: AuthenticationChallengeResponsible?
  142. private let name: String
  143. private var session: URLSession
  144. // MARK: Initializers
  145. /// Creates a downloader with name.
  146. ///
  147. /// - Parameter name: The name for the downloader. It should not be empty.
  148. public init(name: String) {
  149. if name.isEmpty {
  150. fatalError("[Kingfisher] You should specify a name for the downloader. "
  151. + "A downloader with empty name is not permitted.")
  152. }
  153. self.name = name
  154. sessionDelegate = SessionDelegate()
  155. session = URLSession(
  156. configuration: sessionConfiguration,
  157. delegate: sessionDelegate,
  158. delegateQueue: nil)
  159. authenticationChallengeResponder = self
  160. setupSessionHandler()
  161. }
  162. deinit { session.invalidateAndCancel() }
  163. private func setupSessionHandler() {
  164. sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
  165. self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
  166. }
  167. sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
  168. self.authenticationChallengeResponder?.downloader(
  169. self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
  170. }
  171. sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in
  172. return (self.delegate ?? self).isValidStatusCode(code, for: self)
  173. }
  174. sessionDelegate.onResponseReceived.delegate(on: self) { (self, invoke) in
  175. (self.delegate ?? self).imageDownloader(self, didReceive: invoke.0, completionHandler: invoke.1)
  176. }
  177. sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in
  178. let (url, result) = value
  179. do {
  180. let value = try result.get()
  181. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)
  182. } catch {
  183. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)
  184. }
  185. }
  186. sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
  187. return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task)
  188. }
  189. }
  190. // Wraps `completionHandler` to `onCompleted` respectively.
  191. private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate<DownloadResult, Void>? {
  192. return completionHandler.map { block -> Delegate<DownloadResult, Void> in
  193. let delegate = Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()
  194. delegate.delegate(on: self) { (self, callback) in
  195. block(callback)
  196. }
  197. return delegate
  198. }
  199. }
  200. private func createTaskCallback(
  201. _ completionHandler: ((DownloadResult) -> Void)?,
  202. options: KingfisherParsedOptionsInfo
  203. ) -> SessionDataTask.TaskCallback
  204. {
  205. return SessionDataTask.TaskCallback(
  206. onCompleted: createCompletionCallBack(completionHandler),
  207. options: options
  208. )
  209. }
  210. private func createDownloadContext(
  211. with url: URL,
  212. options: KingfisherParsedOptionsInfo,
  213. done: @escaping ((Result<DownloadingContext, KingfisherError>) -> Void)
  214. )
  215. {
  216. @Sendable func checkRequestAndDone(r: URLRequest) {
  217. // There is a possibility that request modifier changed the url to `nil` or empty.
  218. // In this case, throw an error.
  219. guard let url = r.url, !url.absoluteString.isEmpty else {
  220. done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r))))
  221. return
  222. }
  223. done(.success(DownloadingContext(url: url, request: r, options: options)))
  224. }
  225. // Creates default request.
  226. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
  227. request.httpShouldUsePipelining = requestsUsePipelining
  228. if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil {
  229. request.allowsConstrainedNetworkAccess = false
  230. }
  231. if let requestModifier = options.requestModifier {
  232. // Modifies request before sending.
  233. // FIXME: A temporary solution for keep the sync `ImageDownloadRequestModifier` behavior as before.
  234. // We should be able to combine two cases once the full async support can be introduced to Kingfisher.
  235. if let m = requestModifier as? ImageDownloadRequestModifier {
  236. guard let result = m.modified(for: request) else {
  237. done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
  238. return
  239. }
  240. checkRequestAndDone(r: result)
  241. } else {
  242. Task { [request] in
  243. guard let result = await requestModifier.modified(for: request) else {
  244. done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
  245. return
  246. }
  247. checkRequestAndDone(r: result)
  248. }
  249. }
  250. } else {
  251. checkRequestAndDone(r: request)
  252. }
  253. }
  254. private func addDownloadTask(
  255. context: DownloadingContext,
  256. callback: SessionDataTask.TaskCallback
  257. ) -> DownloadTask
  258. {
  259. // Ready to start download. Add it to session task manager (`sessionHandler`)
  260. let downloadTask: DownloadTask
  261. if let existingTask = sessionDelegate.task(for: context.url) {
  262. downloadTask = sessionDelegate.append(existingTask, callback: callback)
  263. } else {
  264. let sessionDataTask = session.dataTask(with: context.request)
  265. sessionDataTask.priority = context.options.downloadPriority
  266. downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)
  267. }
  268. return downloadTask
  269. }
  270. private func reportWillDownloadImage(url: URL, request: URLRequest) {
  271. delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
  272. }
  273. private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) {
  274. var response: URLResponse?
  275. var err: Error?
  276. do {
  277. response = try result.get().1
  278. } catch {
  279. err = error
  280. }
  281. self.delegate?.imageDownloader(
  282. self,
  283. didFinishDownloadingImageForURL: url,
  284. with: response,
  285. error: err
  286. )
  287. }
  288. private func reportDidProcessImage(
  289. result: Result<KFCrossPlatformImage, KingfisherError>, url: URL, response: URLResponse?
  290. )
  291. {
  292. if let image = try? result.get() {
  293. self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
  294. }
  295. }
  296. private func startDownloadTask(
  297. context: DownloadingContext,
  298. callback: SessionDataTask.TaskCallback
  299. ) -> DownloadTask
  300. {
  301. let downloadTask = addDownloadTask(context: context, callback: callback)
  302. let sessionTask = downloadTask.sessionTask
  303. guard !sessionTask.started else {
  304. return downloadTask
  305. }
  306. sessionTask.onTaskDone.delegate(on: self) { (self, done) in
  307. // Underlying downloading finishes.
  308. // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
  309. let (result, callbacks) = done
  310. // Before processing the downloaded data.
  311. self.reportDidDownloadImageData(result: result, url: context.url)
  312. switch result {
  313. // Download finished. Now process the data to an image.
  314. case .success(let (data, response)):
  315. let processor = ImageDataProcessor(
  316. data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
  317. )
  318. processor.onImageProcessed.delegate(on: self) { (self, done) in
  319. // `onImageProcessed` will be called for `callbacks.count` times, with each
  320. // `SessionDataTask.TaskCallback` as the input parameter.
  321. // result: Result<Image>, callback: SessionDataTask.TaskCallback
  322. let (result, callback) = done
  323. self.reportDidProcessImage(result: result, url: context.url, response: response)
  324. let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
  325. let queue = callback.options.callbackQueue
  326. queue.execute { callback.onCompleted?.call(imageResult) }
  327. }
  328. processor.process()
  329. case .failure(let error):
  330. callbacks.forEach { callback in
  331. let queue = callback.options.callbackQueue
  332. queue.execute { callback.onCompleted?.call(.failure(error)) }
  333. }
  334. }
  335. }
  336. reportWillDownloadImage(url: context.url, request: context.request)
  337. sessionTask.resume()
  338. return downloadTask
  339. }
  340. // MARK: Downloading Task
  341. /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super.
  342. ///
  343. /// - Parameters:
  344. /// - url: Target URL.
  345. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  346. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  347. /// defined in `.callbackQueue` in `options` parameter.
  348. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  349. @discardableResult
  350. open func downloadImage(
  351. with url: URL,
  352. options: KingfisherParsedOptionsInfo,
  353. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  354. {
  355. var downloadTask: DownloadTask?
  356. createDownloadContext(with: url, options: options) { result in
  357. switch result {
  358. case .success(let context):
  359. // `downloadTask` will be set if the downloading started immediately. This is the case when no request
  360. // modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an
  361. // `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil`
  362. // and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted`
  363. // callback.
  364. downloadTask = self.startDownloadTask(
  365. context: context,
  366. callback: self.createTaskCallback(completionHandler, options: options)
  367. )
  368. if let modifier = options.requestModifier {
  369. modifier.onDownloadTaskStarted?(downloadTask)
  370. }
  371. case .failure(let error):
  372. options.callbackQueue.execute {
  373. completionHandler?(.failure(error))
  374. }
  375. }
  376. }
  377. return downloadTask
  378. }
  379. /// Downloads an image with a URL and option.
  380. ///
  381. /// - Parameters:
  382. /// - url: Target URL.
  383. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  384. /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
  385. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  386. /// defined in `.callbackQueue` in `options` parameter.
  387. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  388. @discardableResult
  389. open func downloadImage(
  390. with url: URL,
  391. options: KingfisherOptionsInfo? = nil,
  392. progressBlock: DownloadProgressBlock? = nil,
  393. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  394. {
  395. var info = KingfisherParsedOptionsInfo(options)
  396. if let block = progressBlock {
  397. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  398. }
  399. return downloadImage(
  400. with: url,
  401. options: info,
  402. completionHandler: completionHandler)
  403. }
  404. /// Downloads an image with a URL and option.
  405. ///
  406. /// - Parameters:
  407. /// - url: Target URL.
  408. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  409. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  410. /// defined in `.callbackQueue` in `options` parameter.
  411. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  412. @discardableResult
  413. open func downloadImage(
  414. with url: URL,
  415. options: KingfisherOptionsInfo? = nil,
  416. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  417. {
  418. downloadImage(
  419. with: url,
  420. options: KingfisherParsedOptionsInfo(options),
  421. completionHandler: completionHandler
  422. )
  423. }
  424. }
  425. // Concurrency
  426. extension ImageDownloader {
  427. /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super.
  428. ///
  429. /// - Parameters:
  430. /// - url: Target URL.
  431. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  432. /// - Returns: The image loading result.
  433. public func downloadImage(
  434. with url: URL,
  435. options: KingfisherParsedOptionsInfo
  436. ) async throws -> ImageLoadingResult {
  437. let task = CancellationDownloadTask()
  438. return try await withTaskCancellationHandler {
  439. try await withCheckedThrowingContinuation { continuation in
  440. let downloadTask = downloadImage(with: url, options: options) { result in
  441. continuation.resume(with: result)
  442. }
  443. if Task.isCancelled {
  444. downloadTask?.cancel()
  445. } else {
  446. Task {
  447. await task.setTask(downloadTask)
  448. }
  449. }
  450. }
  451. } onCancel: {
  452. Task {
  453. await task.task?.cancel()
  454. }
  455. }
  456. }
  457. /// Downloads an image with a URL and option.
  458. ///
  459. /// - Parameters:
  460. /// - url: Target URL.
  461. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  462. /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
  463. /// - Returns: The image loading result.
  464. public func downloadImage(
  465. with url: URL,
  466. options: KingfisherOptionsInfo? = nil,
  467. progressBlock: DownloadProgressBlock? = nil
  468. ) async throws -> ImageLoadingResult
  469. {
  470. var info = KingfisherParsedOptionsInfo(options)
  471. if let block = progressBlock {
  472. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  473. }
  474. return try await downloadImage(with: url, options: info)
  475. }
  476. /// Downloads an image with a URL and option.
  477. ///
  478. /// - Parameters:
  479. /// - url: Target URL.
  480. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  481. /// - Returns: The image loading result.
  482. public func downloadImage(
  483. with url: URL,
  484. options: KingfisherOptionsInfo? = nil
  485. ) async throws -> ImageLoadingResult
  486. {
  487. return try await downloadImage(with: url, options: KingfisherParsedOptionsInfo(options))
  488. }
  489. }
  490. // MARK: Cancelling Task
  491. extension ImageDownloader {
  492. /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers
  493. /// for all not-yet-finished downloading tasks.
  494. ///
  495. /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask`
  496. /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url,
  497. /// use `ImageDownloader.cancel(url:)`.
  498. public func cancelAll() {
  499. sessionDelegate.cancelAll()
  500. }
  501. /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for
  502. /// all not-yet-finished downloading tasks for the URL.
  503. ///
  504. /// - Parameter url: The URL which you want to cancel downloading.
  505. public func cancel(url: URL) {
  506. sessionDelegate.cancel(url: url)
  507. }
  508. }
  509. // Use the default implementation from extension of `AuthenticationChallengeResponsible`.
  510. extension ImageDownloader: AuthenticationChallengeResponsible {}
  511. // Use the default implementation from extension of `ImageDownloaderDelegate`.
  512. extension ImageDownloader: ImageDownloaderDelegate {}
  513. extension ImageDownloader {
  514. struct DownloadingContext {
  515. let url: URL
  516. let request: URLRequest
  517. let options: KingfisherParsedOptionsInfo
  518. }
  519. }