KingfisherManager.swift 43 KB

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