KingfisherManager.swift 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041
  1. //
  2. // KingfisherManager.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. import Foundation
  27. #if os(macOS)
  28. import AppKit
  29. #else
  30. import UIKit
  31. #endif
  32. /// Represents the type for a downloading progress block.
  33. ///
  34. /// This block type is used to monitor the progress of data being downloaded. It takes two parameters:
  35. ///
  36. /// 1. `receivedSize`: The size of the data received in the current response.
  37. /// 2. `expectedSize`: The total expected data length from the response's "Content-Length" header. If the expected
  38. /// length is not available, this block will not be called.
  39. ///
  40. /// You can use this progress block to track the download progress and update user interfaces or perform additional
  41. /// actions based on the progress.
  42. ///
  43. /// - Parameters:
  44. /// - receivedSize: The size of the data received.
  45. /// - expectedSize: The expected total data length from the "Content-Length" header.
  46. public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void)
  47. /// Represents the result of a Kingfisher image retrieval task.
  48. ///
  49. /// This type encapsulates the outcome of an image retrieval operation performed by Kingfisher.
  50. /// It holds a successful result with the retrieved image.
  51. public struct RetrieveImageResult: Sendable {
  52. /// Retrieves the image object from this result.
  53. public let image: KFCrossPlatformImage
  54. /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved.
  55. ///
  56. /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned.
  57. /// Otherwise, either ``CacheType/memory`` or ``CacheType/disk`` will be returned, allowing you to determine whether
  58. /// the image was retrieved from memory or disk cache.
  59. public let cacheType: CacheType
  60. /// The ``Source`` to which this result is related. This indicates where the `image` referenced by `self` is located.
  61. public let source: Source
  62. /// The original ``Source`` from which the retrieval task begins. It may differ from the ``source`` property.
  63. /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the
  64. /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process.
  65. public let originalSource: Source
  66. /// Retrieves the data associated with this result.
  67. ///
  68. /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns
  69. /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache
  70. /// serializer from the loading options and returns the result.
  71. ///
  72. /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to
  73. /// use it multiple times and avoid frequent calls to this method.
  74. public let data: @Sendable () -> Data?
  75. /// The network metrics collected during the download process.
  76. ///
  77. /// This property contains network performance metrics when the image was downloaded from the network
  78. /// (`cacheType == .none`). For cached images (`cacheType == .memory` or `.disk`), this will be `nil`.
  79. public let metrics: NetworkMetrics?
  80. /// Creates a RetrieveImageResult.
  81. ///
  82. /// - Parameters:
  83. /// - image: The retrieved image.
  84. /// - cacheType: The cache source type.
  85. /// - source: The source of the image.
  86. /// - originalSource: The original source that initiated the retrieval.
  87. /// - data: A closure that provides the image data.
  88. /// - metrics: The network metrics collected during download. Defaults to nil for cached images.
  89. public init(
  90. image: KFCrossPlatformImage,
  91. cacheType: CacheType,
  92. source: Source,
  93. originalSource: Source,
  94. data: @escaping @Sendable () -> Data?,
  95. metrics: NetworkMetrics? = nil
  96. ) {
  97. self.image = image
  98. self.cacheType = cacheType
  99. self.source = source
  100. self.originalSource = originalSource
  101. self.data = data
  102. self.metrics = metrics
  103. }
  104. }
  105. /// A structure that stores related information about a ``KingfisherError``. It provides contextual information
  106. /// to facilitate the identification of the error.
  107. public struct PropagationError: Sendable {
  108. /// The ``Source`` to which current `error` is bound.
  109. public let source: Source
  110. /// The actual error happens in framework.
  111. public let error: KingfisherError
  112. }
  113. /// The block type used for handling updates during the downloading task.
  114. ///
  115. /// The `newTask` parameter represents the updated task for the image loading process. It is `nil` if the image loading
  116. /// doesn't involve a downloading process. When an image download is initiated, this value will contain the actual
  117. /// ``DownloadTask`` instance, allowing you to retain it or cancel it later if necessary.
  118. public typealias DownloadTaskUpdatedBlock = (@Sendable (_ newTask: DownloadTask?) -> Void)
  119. /// The main manager class of Kingfisher. It connects the Kingfisher downloader and cache to offer a set of convenient
  120. /// methods for working with Kingfisher tasks.
  121. ///
  122. /// You can utilize this class to retrieve an image via a specified URL from the web or cache.
  123. public class KingfisherManager: @unchecked Sendable {
  124. private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KingfisherManagerPropertyQueue")
  125. /// Represents a shared manager used across Kingfisher.
  126. /// Use this instance for getting or storing images with Kingfisher.
  127. public static let shared = KingfisherManager()
  128. // Mark: Public Properties
  129. private var _cache: ImageCache
  130. /// The ``ImageCache`` utilized by this manager, which defaults to ``ImageCache/default``.
  131. ///
  132. /// If a cache is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/targetCache(_:)``,
  133. /// those specified values will take precedence when Kingfisher attempts to retrieve or store images in the cache.
  134. public var cache: ImageCache {
  135. get { propertyQueue.sync { _cache } }
  136. set { propertyQueue.sync { _cache = newValue } }
  137. }
  138. private var _downloader: ImageDownloader
  139. /// The ``ImageDownloader`` utilized by this manager, which defaults to ``ImageDownloader/default``.
  140. ///
  141. /// If a downloader is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/downloader(_:)``,
  142. /// those specified values will take precedence when Kingfisher attempts to download the image data from a remote
  143. /// server.
  144. public var downloader: ImageDownloader {
  145. get { propertyQueue.sync { _downloader } }
  146. set { propertyQueue.sync { _downloader = newValue } }
  147. }
  148. /// The default options used by the ``KingfisherManager`` instance.
  149. ///
  150. /// These options are utilized in Kingfisher manager-related methods, as well as all view extension methods.
  151. /// You can also pass additional options for each image task by providing an `options` parameter to Kingfisher's APIs.
  152. ///
  153. /// Per-image options will override the default ones if there is a conflict.
  154. public var defaultOptions = KingfisherOptionsInfo.empty
  155. // Use `defaultOptions` to overwrite the `downloader` and `cache`.
  156. var currentDefaultOptions: KingfisherOptionsInfo {
  157. return [.downloader(downloader), .targetCache(cache)] + defaultOptions
  158. }
  159. private let processingQueue: CallbackQueue
  160. private convenience init() {
  161. self.init(downloader: .default, cache: .default)
  162. }
  163. /// Creates an image setting manager with the specified downloader and cache.
  164. ///
  165. /// - Parameters:
  166. /// - downloader: The image downloader used for image downloads.
  167. /// - cache: The image cache that stores images in memory and on disk.
  168. ///
  169. public init(downloader: ImageDownloader, cache: ImageCache) {
  170. _downloader = downloader
  171. _cache = cache
  172. let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)"
  173. processingQueue = .dispatch(DispatchQueue(label: processQueueName))
  174. }
  175. // MARK: - Getting Images
  176. /// Retrieves an image from a specified resource.
  177. ///
  178. /// - Parameters:
  179. /// - resource: The ``Resource`` object defining data information, such as a key or URL.
  180. /// - options: Options to use when creating the image.
  181. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response
  182. /// contains an `expectedContentLength` and always runs on the main queue.
  183. /// - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This
  184. /// typically occurs when an alternative source is used to replace the original (failed) task. You can update your
  185. /// reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task.
  186. /// - completionHandler: Called when the image retrieval and setting are completed. This completion handler is
  187. /// invoked from the `options.callbackQueue`. If not specified, the main queue is used.
  188. ///
  189. /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource,
  190. /// the started ``DownloadTask`` is returned; otherwise, `nil` is returned.
  191. ///
  192. /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached,
  193. /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads
  194. /// the `resource`, stores it in the cache, and then calls the `completionHandler`.
  195. ///
  196. @discardableResult
  197. public func retrieveImage(
  198. with resource: any Resource,
  199. options: KingfisherOptionsInfo? = nil,
  200. progressBlock: DownloadProgressBlock? = nil,
  201. downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
  202. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  203. {
  204. return retrieveImage(
  205. with: resource.convertToSource(),
  206. options: options,
  207. progressBlock: progressBlock,
  208. downloadTaskUpdated: downloadTaskUpdated,
  209. completionHandler: completionHandler
  210. )
  211. }
  212. /// Retrieves an image from a specified source.
  213. ///
  214. /// - Parameters:
  215. /// - source: The ``Source`` object defining data information, such as a key or URL.
  216. /// - options: Options to use when creating the image.
  217. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response
  218. /// contains an `expectedContentLength` and always runs on the main queue.
  219. /// - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This
  220. /// typically occurs when an alternative source is used to replace the original (failed) task. You can update your
  221. /// reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task.
  222. /// - completionHandler: Called when the image retrieval and setting are completed. This completion handler is
  223. /// invoked from the `options.callbackQueue`. If not specified, the main queue is used.
  224. ///
  225. /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource,
  226. /// the started ``DownloadTask`` is returned; otherwise, `nil` is returned.
  227. ///
  228. /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached,
  229. /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads
  230. /// the `source`, stores it in the cache, and then calls the `completionHandler`.
  231. ///
  232. @discardableResult
  233. public func retrieveImage(
  234. with source: Source,
  235. options: KingfisherOptionsInfo? = nil,
  236. progressBlock: DownloadProgressBlock? = nil,
  237. downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
  238. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  239. {
  240. let options = currentDefaultOptions + (options ?? .empty)
  241. let info = KingfisherParsedOptionsInfo(options)
  242. return retrieveImage(
  243. with: source,
  244. options: info,
  245. progressBlock: progressBlock,
  246. downloadTaskUpdated: downloadTaskUpdated,
  247. completionHandler: completionHandler)
  248. }
  249. func retrieveImage(
  250. with source: Source,
  251. options: KingfisherParsedOptionsInfo,
  252. progressBlock: DownloadProgressBlock?,
  253. downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
  254. progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,
  255. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  256. {
  257. var info = options
  258. if let block = progressBlock {
  259. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  260. }
  261. return retrieveImage(
  262. with: source,
  263. options: info,
  264. downloadTaskUpdated: downloadTaskUpdated,
  265. progressiveImageSetter: progressiveImageSetter,
  266. completionHandler: completionHandler)
  267. }
  268. func retrieveImage(
  269. with source: Source,
  270. options: KingfisherParsedOptionsInfo,
  271. downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
  272. progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,
  273. referenceTaskIdentifierChecker: (() -> Bool)? = nil,
  274. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  275. {
  276. var options = options
  277. let retryStrategy = options.retryStrategy
  278. let progressiveJPEG = options.progressiveJPEG
  279. if let provider = ImageProgressiveProvider(options: options, refresh: { image in
  280. guard let setter = progressiveImageSetter else {
  281. return
  282. }
  283. guard let strategy = progressiveJPEG?.onImageUpdated(image) else {
  284. setter(image)
  285. return
  286. }
  287. switch strategy {
  288. case .default: setter(image)
  289. case .keepCurrent: break
  290. case .replace(let newImage): setter(newImage)
  291. }
  292. }) {
  293. options.onDataReceived = (options.onDataReceived ?? []) + [provider]
  294. }
  295. if let checker = referenceTaskIdentifierChecker {
  296. options.onDataReceived?.forEach {
  297. $0.onShouldApply = checker
  298. }
  299. }
  300. let retrievingContext = RetrievingContext(options: options, originalSource: source)
  301. @Sendable func startNewRetrieveTask(
  302. with source: Source,
  303. retryContext: RetryContext?,
  304. downloadTaskUpdated: DownloadTaskUpdatedBlock?
  305. ) {
  306. let newTask = self.retrieveImage(with: source, context: retrievingContext) { result in
  307. handler(currentSource: source, retryContext: retryContext, result: result)
  308. }
  309. downloadTaskUpdated?(newTask)
  310. }
  311. @Sendable func failCurrentSource(_ source: Source, retryContext: RetryContext?, with error: KingfisherError) {
  312. // Skip alternative sources if the user cancelled it.
  313. guard !error.isTaskCancelled else {
  314. completionHandler?(.failure(error))
  315. return
  316. }
  317. // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly.
  318. guard !error.isLowDataModeConstrained else {
  319. if let source = retrievingContext.options.lowDataModeSource {
  320. retrievingContext.options.lowDataModeSource = nil
  321. startNewRetrieveTask(with: source, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated)
  322. } else {
  323. // This should not happen.
  324. completionHandler?(.failure(error))
  325. }
  326. return
  327. }
  328. if let nextSource = retrievingContext.popAlternativeSource() {
  329. retrievingContext.appendError(error, to: source)
  330. startNewRetrieveTask(with: nextSource, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated)
  331. } else {
  332. // No other alternative source. Finish with error.
  333. if retrievingContext.propagationErrors.isEmpty {
  334. completionHandler?(.failure(error))
  335. } else {
  336. retrievingContext.appendError(error, to: source)
  337. let finalError = KingfisherError.imageSettingError(
  338. reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors)
  339. )
  340. completionHandler?(.failure(finalError))
  341. }
  342. }
  343. }
  344. @Sendable func handler(
  345. currentSource: Source,
  346. retryContext: RetryContext?,
  347. result: (Result<RetrieveImageResult, KingfisherError>)
  348. ) -> Void {
  349. switch result {
  350. case .success:
  351. completionHandler?(result)
  352. case .failure(let error):
  353. if let retryStrategy = retryStrategy {
  354. let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error)
  355. retryStrategy.retry(context: context) { decision in
  356. switch decision {
  357. case .retry(let userInfo):
  358. context.userInfo = userInfo
  359. startNewRetrieveTask(with: source, retryContext: context, downloadTaskUpdated: downloadTaskUpdated)
  360. case .stop:
  361. failCurrentSource(currentSource, retryContext: context, with: error)
  362. }
  363. }
  364. } else {
  365. failCurrentSource(currentSource, retryContext: retryContext, with: error)
  366. }
  367. }
  368. }
  369. return retrieveImage(
  370. with: source,
  371. context: retrievingContext)
  372. {
  373. result in
  374. handler(currentSource: source, retryContext: nil, result: result)
  375. }
  376. }
  377. private func retrieveImage(
  378. with source: Source,
  379. context: RetrievingContext<Source>,
  380. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  381. {
  382. let options = context.options
  383. if options.forceRefresh {
  384. return loadAndCacheImage(
  385. source: source,
  386. context: context,
  387. completionHandler: completionHandler)?.value
  388. } else {
  389. let loadedFromCache = retrieveImageFromCache(
  390. source: source,
  391. context: context,
  392. completionHandler: completionHandler)
  393. if loadedFromCache {
  394. return nil
  395. }
  396. if options.onlyFromCache {
  397. let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey))
  398. completionHandler?(.failure(error))
  399. return nil
  400. }
  401. return loadAndCacheImage(
  402. source: source,
  403. context: context,
  404. completionHandler: completionHandler)?.value
  405. }
  406. }
  407. func provideImage(
  408. provider: any ImageDataProvider,
  409. options: KingfisherParsedOptionsInfo,
  410. completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)?)
  411. {
  412. guard let completionHandler = completionHandler else { return }
  413. provider.data { result in
  414. switch result {
  415. case .success(let data):
  416. (options.processingQueue ?? self.processingQueue).execute {
  417. let processor = options.processor
  418. let processingItem = ImageProcessItem.data(data)
  419. guard let image = processor.process(item: processingItem, options: options) else {
  420. options.callbackQueue.execute {
  421. let error = KingfisherError.processorError(
  422. reason: .processingFailed(processor: processor, item: processingItem))
  423. completionHandler(.failure(error))
  424. }
  425. return
  426. }
  427. options.callbackQueue.execute {
  428. let result = ImageLoadingResult(image: image, url: nil, originalData: data)
  429. completionHandler(.success(result))
  430. }
  431. }
  432. case .failure(let error):
  433. options.callbackQueue.execute {
  434. let error = KingfisherError.imageSettingError(
  435. reason: .dataProviderError(provider: provider, error: error))
  436. completionHandler(.failure(error))
  437. }
  438. }
  439. }
  440. }
  441. private func cacheImage(
  442. source: Source,
  443. options: KingfisherParsedOptionsInfo,
  444. context: RetrievingContext<Source>,
  445. result: Result<ImageLoadingResult, KingfisherError>,
  446. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?
  447. )
  448. {
  449. switch result {
  450. case .success(let value):
  451. let needToCacheOriginalImage = options.cacheOriginalImage &&
  452. options.processor != DefaultImageProcessor.default
  453. let coordinator = CacheCallbackCoordinator(
  454. shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage)
  455. let result = RetrieveImageResult(
  456. image: options.imageModifier?.modify(value.image) ?? value.image,
  457. cacheType: .none,
  458. source: source,
  459. originalSource: context.originalSource,
  460. data: { value.originalData },
  461. metrics: value.metrics
  462. )
  463. // Add image to cache.
  464. let targetCache = options.targetCache ?? self.cache
  465. targetCache.store(
  466. value.image,
  467. original: value.originalData,
  468. forKey: source.cacheKey,
  469. options: options,
  470. toDisk: !options.cacheMemoryOnly)
  471. {
  472. _ in
  473. coordinator.apply(.cachingImage) {
  474. completionHandler?(.success(result))
  475. }
  476. }
  477. // Add original image to cache if necessary.
  478. if needToCacheOriginalImage {
  479. let originalCache = options.originalCache ?? targetCache
  480. originalCache.storeToDisk(
  481. value.originalData,
  482. forKey: source.cacheKey,
  483. processorIdentifier: DefaultImageProcessor.default.identifier,
  484. expiration: options.diskCacheExpiration)
  485. {
  486. _ in
  487. coordinator.apply(.cachingOriginalImage) {
  488. completionHandler?(.success(result))
  489. }
  490. }
  491. }
  492. coordinator.apply(.cacheInitiated) {
  493. completionHandler?(.success(result))
  494. }
  495. case .failure(let error):
  496. completionHandler?(.failure(error))
  497. }
  498. }
  499. @discardableResult
  500. func loadAndCacheImage(
  501. source: Source,
  502. context: RetrievingContext<Source>,
  503. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
  504. {
  505. let options = context.options
  506. @Sendable func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
  507. cacheImage(
  508. source: source,
  509. options: options,
  510. context: context,
  511. result: result,
  512. completionHandler: completionHandler
  513. )
  514. }
  515. switch source {
  516. case .network(let resource):
  517. let downloader = options.downloader ?? self.downloader
  518. let task = downloader.downloadImage(
  519. with: resource.downloadURL, options: options, completionHandler: _cacheImage
  520. )
  521. // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when
  522. // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler.
  523. // Let's fallback to a traditional style before it can be fixed in Swift.
  524. //
  525. // https://github.com/onevcat/Kingfisher/issues/1436
  526. //
  527. // return task.map(DownloadTask.WrappedTask.download)
  528. if task.isInitialized {
  529. return .download(task)
  530. } else {
  531. return nil
  532. }
  533. case .provider(let provider):
  534. provideImage(provider: provider, options: options, completionHandler: _cacheImage)
  535. return .dataProviding
  536. }
  537. }
  538. /// Retrieves an image from either memory or disk cache.
  539. ///
  540. /// - Parameters:
  541. /// - source: The target source from which to retrieve the image.
  542. /// - key: The key to use for caching the image.
  543. /// - url: The image request URL. This is not used when retrieving an image from the cache; it is solely used for
  544. /// compatibility with ``RetrieveImageResult`` callbacks.
  545. /// - options: Options on how to retrieve the image from the image cache.
  546. /// - completionHandler: Called when the image retrieval is complete, either with a successful
  547. /// ``RetrieveImageResult`` or an error.
  548. ///
  549. /// - Returns: `true` if the requested image or the original image before processing exists in the cache. Otherwise, this method returns `false`.
  550. ///
  551. /// - Note: Image retrieval can occur in either the memory cache or the disk cache. The
  552. /// ``KingfisherOptionsInfoItem/processor(_:)`` option in `options` is considered when searching the cache. If no
  553. /// processed image is found, Kingfisher attempts to determine whether an original version of the image exists. If
  554. /// an original exists, Kingfisher retrieves it from the cache and processes it. Subsequently, the processed image
  555. /// is stored back in the cache for future use.
  556. ///
  557. func retrieveImageFromCache(
  558. source: Source,
  559. context: RetrievingContext<Source>,
  560. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool
  561. {
  562. let options = context.options
  563. // 1. Check whether the image was already in target cache. If so, just get it.
  564. let targetCache = options.targetCache ?? cache
  565. let key = source.cacheKey
  566. let targetImageCached = targetCache.imageCachedType(
  567. forKey: key, processorIdentifier: options.processor.identifier)
  568. let validCache = targetImageCached.cached &&
  569. (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory)
  570. if validCache {
  571. targetCache.retrieveImage(forKey: key, options: options) { result in
  572. guard let completionHandler = completionHandler else { return }
  573. // TODO: Optimize it when we can use async across all the project.
  574. @Sendable func checkResultImageAndCallback(_ inputImage: KFCrossPlatformImage) {
  575. var image = inputImage
  576. if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, options.imageCreatingOptions != image.kf.imageCreatingOptions, let data = image.kf.animatedImageData {
  577. // Recreate animated image representation when loaded in different options.
  578. // https://github.com/onevcat/Kingfisher/issues/1923
  579. image = options.processor.process(item: .data(data), options: options) ?? .init()
  580. }
  581. if let modifier = options.imageModifier {
  582. image = modifier.modify(image)
  583. }
  584. let value = result.map {
  585. RetrieveImageResult(
  586. image: image,
  587. cacheType: $0.cacheType,
  588. source: source,
  589. originalSource: context.originalSource,
  590. data: { [image] in options.cacheSerializer.data(with: image, original: nil) }
  591. )
  592. }
  593. completionHandler(value)
  594. }
  595. result.match { cacheResult in
  596. options.callbackQueue.execute {
  597. guard let image = cacheResult.image else {
  598. completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))
  599. return
  600. }
  601. if options.cacheSerializer.originalDataUsed {
  602. let processor = options.processor
  603. (options.processingQueue ?? self.processingQueue).execute {
  604. let item = ImageProcessItem.image(image)
  605. guard let processedImage = processor.process(item: item, options: options) else {
  606. let error = KingfisherError.processorError(
  607. reason: .processingFailed(processor: processor, item: item))
  608. options.callbackQueue.execute { completionHandler(.failure(error)) }
  609. return
  610. }
  611. options.callbackQueue.execute {
  612. checkResultImageAndCallback(processedImage)
  613. }
  614. }
  615. } else {
  616. checkResultImageAndCallback(image)
  617. }
  618. }
  619. } onFailure: { error in
  620. options.callbackQueue.execute {
  621. completionHandler(.failure(error))
  622. }
  623. }
  624. }
  625. return true
  626. }
  627. // 2. Check whether the original image exists. If so, get it, process it, save to storage and return.
  628. let originalCache = options.originalCache ?? targetCache
  629. // No need to store the same file in the same cache again.
  630. if originalCache === targetCache && options.processor == DefaultImageProcessor.default {
  631. return false
  632. }
  633. // Check whether the unprocessed image existing or not.
  634. let originalImageCacheType = originalCache.imageCachedType(
  635. forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier)
  636. let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh
  637. let canUseOriginalImageCache =
  638. (canAcceptDiskCache && originalImageCacheType.cached) ||
  639. (!canAcceptDiskCache && originalImageCacheType == .memory)
  640. if canUseOriginalImageCache {
  641. // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove
  642. // any processor from options first.
  643. var optionsWithoutProcessor = options
  644. optionsWithoutProcessor.processor = DefaultImageProcessor.default
  645. originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in
  646. result.match(
  647. onSuccess: { cacheResult in
  648. guard let image = cacheResult.image else {
  649. assertionFailure("The image (under key: \(key) should be existing in the original cache.")
  650. return
  651. }
  652. let processor = options.processor
  653. (options.processingQueue ?? self.processingQueue).execute {
  654. let item = ImageProcessItem.image(image)
  655. guard let processedImage = processor.process(item: item, options: options) else {
  656. let error = KingfisherError.processorError(
  657. reason: .processingFailed(processor: processor, item: item))
  658. options.callbackQueue.execute { completionHandler?(.failure(error)) }
  659. return
  660. }
  661. var cacheOptions = options
  662. cacheOptions.callbackQueue = .untouch
  663. let coordinator = CacheCallbackCoordinator(
  664. shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false)
  665. let image = options.imageModifier?.modify(processedImage) ?? processedImage
  666. let result = RetrieveImageResult(
  667. image: image,
  668. cacheType: .none,
  669. source: source,
  670. originalSource: context.originalSource,
  671. data: { options.cacheSerializer.data(with: processedImage, original: nil) }
  672. )
  673. targetCache.store(
  674. processedImage,
  675. forKey: key,
  676. options: cacheOptions,
  677. toDisk: !options.cacheMemoryOnly)
  678. {
  679. _ in
  680. coordinator.apply(.cachingImage) {
  681. options.callbackQueue.execute { completionHandler?(.success(result)) }
  682. }
  683. }
  684. coordinator.apply(.cacheInitiated) {
  685. options.callbackQueue.execute { completionHandler?(.success(result)) }
  686. }
  687. }
  688. },
  689. onFailure: { error in
  690. // This should not happen actually, since we already confirmed `originalImageCached` is `true`.
  691. // Just in case...
  692. if let completionHandler = completionHandler {
  693. options.callbackQueue.execute { completionHandler(.failure(error)) }
  694. }
  695. }
  696. )
  697. }
  698. return true
  699. }
  700. return false
  701. }
  702. }
  703. // Concurrency
  704. extension KingfisherManager {
  705. /// Retrieves an image from a specified resource.
  706. ///
  707. /// - Parameters:
  708. /// - resource: The ``Resource`` object defining data information, such as a key or URL.
  709. /// - options: Options to use when creating the image.
  710. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response
  711. /// contains an `expectedContentLength` and always runs on the main queue.
  712. ///
  713. /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type.
  714. /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress.
  715. ///
  716. /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached,
  717. /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads
  718. /// the `resource`, stores it in the cache, and then calls the `completionHandler`.
  719. ///
  720. public func retrieveImage(
  721. with resource: any Resource,
  722. options: KingfisherOptionsInfo? = nil,
  723. progressBlock: DownloadProgressBlock? = nil
  724. ) async throws -> RetrieveImageResult
  725. {
  726. try await retrieveImage(
  727. with: resource.convertToSource(),
  728. options: options,
  729. progressBlock: progressBlock
  730. )
  731. }
  732. /// Retrieves an image from a specified source.
  733. ///
  734. /// - Parameters:
  735. /// - source: The ``Source`` object defining data information, such as a key or URL.
  736. /// - options: Options to use when creating the image.
  737. /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response
  738. /// contains an `expectedContentLength` and always runs on the main queue.
  739. ///
  740. /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type.
  741. /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress.
  742. ///
  743. /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached,
  744. /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads
  745. /// the `source`, stores it in the cache, and then calls the `completionHandler`.
  746. ///
  747. public func retrieveImage(
  748. with source: Source,
  749. options: KingfisherOptionsInfo? = nil,
  750. progressBlock: DownloadProgressBlock? = nil
  751. ) async throws -> RetrieveImageResult
  752. {
  753. let options = currentDefaultOptions + (options ?? .empty)
  754. let info = KingfisherParsedOptionsInfo(options)
  755. return try await retrieveImage(
  756. with: source,
  757. options: info,
  758. progressBlock: progressBlock
  759. )
  760. }
  761. func retrieveImage(
  762. with source: Source,
  763. options: KingfisherParsedOptionsInfo,
  764. progressBlock: DownloadProgressBlock? = nil
  765. ) async throws -> RetrieveImageResult
  766. {
  767. var info = options
  768. if let block = progressBlock {
  769. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  770. }
  771. return try await retrieveImage(
  772. with: source,
  773. options: info,
  774. progressiveImageSetter: nil
  775. )
  776. }
  777. func retrieveImage(
  778. with source: Source,
  779. options: KingfisherParsedOptionsInfo,
  780. progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,
  781. referenceTaskIdentifierChecker: (() -> Bool)? = nil
  782. ) async throws -> RetrieveImageResult
  783. {
  784. // Early cancellation check
  785. if Task.isCancelled {
  786. throw CancellationError()
  787. }
  788. let task = CancellationDownloadTask()
  789. return try await withTaskCancellationHandler {
  790. try await withCheckedThrowingContinuation { continuation in
  791. // Use an actor to ensure continuation is only resumed once in a Swift 6 compatible way
  792. actor ContinuationState {
  793. var isResumed = false
  794. func tryResume() -> Bool {
  795. if !isResumed {
  796. isResumed = true
  797. return true
  798. }
  799. return false
  800. }
  801. }
  802. let state = ContinuationState()
  803. @Sendable func safeResume(with result: Result<RetrieveImageResult, KingfisherError>) {
  804. Task {
  805. if await state.tryResume() {
  806. continuation.resume(with: result)
  807. }
  808. }
  809. }
  810. let downloadTask = retrieveImage(
  811. with: source,
  812. options: options,
  813. downloadTaskUpdated: { newTask in
  814. Task {
  815. await task.setTask(newTask)
  816. }
  817. },
  818. progressiveImageSetter: progressiveImageSetter,
  819. referenceTaskIdentifierChecker: referenceTaskIdentifierChecker,
  820. completionHandler: { result in
  821. safeResume(with: result)
  822. }
  823. )
  824. // Check for cancellation that may have occurred during setup
  825. if Task.isCancelled {
  826. downloadTask?.cancel()
  827. let error: KingfisherError
  828. if let sessionTask = downloadTask?.sessionTask, let cancelToken = downloadTask?.cancelToken {
  829. error = .requestError(reason: .taskCancelled(task: sessionTask, token: cancelToken))
  830. } else {
  831. error = .requestError(reason: .asyncTaskContextCancelled)
  832. }
  833. safeResume(with: .failure(error))
  834. } else {
  835. Task {
  836. await task.setTask(downloadTask)
  837. }
  838. }
  839. }
  840. } onCancel: {
  841. Task {
  842. await task.task?.cancel()
  843. }
  844. }
  845. }
  846. }
  847. class RetrievingContext<SourceType>: @unchecked Sendable {
  848. private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.RetrievingContextPropertyQueue")
  849. private var _options: KingfisherParsedOptionsInfo
  850. var options: KingfisherParsedOptionsInfo {
  851. get { propertyQueue.sync { _options } }
  852. set { propertyQueue.sync { _options = newValue } }
  853. }
  854. let originalSource: SourceType
  855. var propagationErrors: [PropagationError] = []
  856. init(options: KingfisherParsedOptionsInfo, originalSource: SourceType) {
  857. self.originalSource = originalSource
  858. _options = options
  859. }
  860. func popAlternativeSource() -> Source? {
  861. var localOptions = options
  862. guard var alternativeSources = localOptions.alternativeSources, !alternativeSources.isEmpty else {
  863. return nil
  864. }
  865. let nextSource = alternativeSources.removeFirst()
  866. localOptions.alternativeSources = alternativeSources
  867. options = localOptions
  868. return nextSource
  869. }
  870. @discardableResult
  871. func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] {
  872. let item = PropagationError(source: source, error: error)
  873. propagationErrors.append(item)
  874. return propagationErrors
  875. }
  876. }
  877. class CacheCallbackCoordinator: @unchecked Sendable {
  878. enum State {
  879. case idle
  880. case imageCached
  881. case originalImageCached
  882. case done
  883. }
  884. enum Action {
  885. case cacheInitiated
  886. case cachingImage
  887. case cachingOriginalImage
  888. }
  889. private let shouldWaitForCache: Bool
  890. private let shouldCacheOriginal: Bool
  891. private let stateQueue: DispatchQueue
  892. private var threadSafeState: State = .idle
  893. private(set) var state: State {
  894. set { stateQueue.sync { threadSafeState = newValue } }
  895. get { stateQueue.sync { threadSafeState } }
  896. }
  897. init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) {
  898. self.shouldWaitForCache = shouldWaitForCache
  899. self.shouldCacheOriginal = shouldCacheOriginal
  900. let stateQueueName = "com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\(UUID().uuidString)"
  901. self.stateQueue = DispatchQueue(label: stateQueueName)
  902. }
  903. func apply(_ action: Action, trigger: () -> Void) {
  904. switch (state, action) {
  905. case (.done, _):
  906. break
  907. // From .idle
  908. case (.idle, .cacheInitiated):
  909. if !shouldWaitForCache {
  910. state = .done
  911. trigger()
  912. }
  913. case (.idle, .cachingImage):
  914. if shouldCacheOriginal {
  915. state = .imageCached
  916. } else {
  917. state = .done
  918. trigger()
  919. }
  920. case (.idle, .cachingOriginalImage):
  921. state = .originalImageCached
  922. // From .imageCached
  923. case (.imageCached, .cachingOriginalImage):
  924. state = .done
  925. trigger()
  926. // From .originalImageCached
  927. case (.originalImageCached, .cachingImage):
  928. state = .done
  929. trigger()
  930. default:
  931. assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)")
  932. }
  933. }
  934. }