KingfisherManager.swift 42 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? = nil,
  224. downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
  225. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  226. {
  227. var info = options
  228. if let block = progressBlock {
  229. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  230. }
  231. return retrieveImage(
  232. with: source,
  233. options: info,
  234. downloadTaskUpdated: downloadTaskUpdated,
  235. progressiveImageSetter: nil,
  236. completionHandler: completionHandler)
  237. }
  238. func retrieveImage(
  239. with source: Source,
  240. options: KingfisherParsedOptionsInfo,
  241. downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
  242. progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,
  243. referenceTaskIdentifierChecker: (() -> Bool)? = nil,
  244. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  245. {
  246. var options = options
  247. let retryStrategy = options.retryStrategy
  248. if let provider = ImageProgressiveProvider(options: options, refresh: { image in
  249. guard let setter = progressiveImageSetter else {
  250. return
  251. }
  252. guard let strategy = options.progressiveJPEG?.onImageUpdated(image) else {
  253. setter(image)
  254. return
  255. }
  256. switch strategy {
  257. case .default: setter(image)
  258. case .keepCurrent: break
  259. case .replace(let newImage): setter(newImage)
  260. }
  261. }) {
  262. options.onDataReceived = (options.onDataReceived ?? []) + [provider]
  263. }
  264. if let checker = referenceTaskIdentifierChecker {
  265. options.onDataReceived?.forEach {
  266. $0.onShouldApply = checker
  267. }
  268. }
  269. let retrievingContext = RetrievingContext(options: options, originalSource: source)
  270. @Sendable func startNewRetrieveTask(
  271. with source: Source,
  272. retryContext: RetryContext?,
  273. downloadTaskUpdated: DownloadTaskUpdatedBlock?
  274. ) {
  275. let newTask = self.retrieveImage(with: source, context: retrievingContext) { result in
  276. handler(currentSource: source, retryContext: retryContext, result: result)
  277. }
  278. downloadTaskUpdated?(newTask)
  279. }
  280. @Sendable func failCurrentSource(_ source: Source, retryContext: RetryContext?, with error: KingfisherError) {
  281. // Skip alternative sources if the user cancelled it.
  282. guard !error.isTaskCancelled else {
  283. completionHandler?(.failure(error))
  284. return
  285. }
  286. // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly.
  287. guard !error.isLowDataModeConstrained else {
  288. if let source = retrievingContext.options.lowDataModeSource {
  289. retrievingContext.options.lowDataModeSource = nil
  290. startNewRetrieveTask(with: source, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated)
  291. } else {
  292. // This should not happen.
  293. completionHandler?(.failure(error))
  294. }
  295. return
  296. }
  297. if let nextSource = retrievingContext.popAlternativeSource() {
  298. retrievingContext.appendError(error, to: source)
  299. startNewRetrieveTask(with: nextSource, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated)
  300. } else {
  301. // No other alternative source. Finish with error.
  302. if retrievingContext.propagationErrors.isEmpty {
  303. completionHandler?(.failure(error))
  304. } else {
  305. retrievingContext.appendError(error, to: source)
  306. let finalError = KingfisherError.imageSettingError(
  307. reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors)
  308. )
  309. completionHandler?(.failure(finalError))
  310. }
  311. }
  312. }
  313. @Sendable func handler(
  314. currentSource: Source,
  315. retryContext: RetryContext?,
  316. result: (Result<RetrieveImageResult, KingfisherError>)
  317. ) -> Void {
  318. switch result {
  319. case .success:
  320. completionHandler?(result)
  321. case .failure(let error):
  322. if let retryStrategy = retryStrategy {
  323. let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error)
  324. retryStrategy.retry(context: context) { decision in
  325. switch decision {
  326. case .retry(let userInfo):
  327. context.userInfo = userInfo
  328. startNewRetrieveTask(with: source, retryContext: context, downloadTaskUpdated: downloadTaskUpdated)
  329. case .stop:
  330. failCurrentSource(currentSource, retryContext: context, with: error)
  331. }
  332. }
  333. } else {
  334. failCurrentSource(currentSource, retryContext: retryContext, with: error)
  335. }
  336. }
  337. }
  338. return retrieveImage(
  339. with: source,
  340. context: retrievingContext)
  341. {
  342. result in
  343. handler(currentSource: source, retryContext: nil, result: result)
  344. }
  345. }
  346. private func retrieveImage(
  347. with source: Source,
  348. context: RetrievingContext<Source>,
  349. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
  350. {
  351. let options = context.options
  352. if options.forceRefresh {
  353. return loadAndCacheImage(
  354. source: source,
  355. context: context,
  356. completionHandler: completionHandler)?.value
  357. } else {
  358. let loadedFromCache = retrieveImageFromCache(
  359. source: source,
  360. context: context,
  361. completionHandler: completionHandler)
  362. if loadedFromCache {
  363. return nil
  364. }
  365. if options.onlyFromCache {
  366. let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey))
  367. completionHandler?(.failure(error))
  368. return nil
  369. }
  370. return loadAndCacheImage(
  371. source: source,
  372. context: context,
  373. completionHandler: completionHandler)?.value
  374. }
  375. }
  376. func provideImage(
  377. provider: any ImageDataProvider,
  378. options: KingfisherParsedOptionsInfo,
  379. completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)?)
  380. {
  381. guard let completionHandler = completionHandler else { return }
  382. provider.data { result in
  383. switch result {
  384. case .success(let data):
  385. (options.processingQueue ?? self.processingQueue).execute {
  386. let processor = options.processor
  387. let processingItem = ImageProcessItem.data(data)
  388. guard let image = processor.process(item: processingItem, options: options) else {
  389. options.callbackQueue.execute {
  390. let error = KingfisherError.processorError(
  391. reason: .processingFailed(processor: processor, item: processingItem))
  392. completionHandler(.failure(error))
  393. }
  394. return
  395. }
  396. options.callbackQueue.execute {
  397. let result = ImageLoadingResult(image: image, url: nil, originalData: data)
  398. completionHandler(.success(result))
  399. }
  400. }
  401. case .failure(let error):
  402. options.callbackQueue.execute {
  403. let error = KingfisherError.imageSettingError(
  404. reason: .dataProviderError(provider: provider, error: error))
  405. completionHandler(.failure(error))
  406. }
  407. }
  408. }
  409. }
  410. private func cacheImage(
  411. source: Source,
  412. options: KingfisherParsedOptionsInfo,
  413. context: RetrievingContext<Source>,
  414. result: Result<ImageLoadingResult, KingfisherError>,
  415. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?
  416. )
  417. {
  418. switch result {
  419. case .success(let value):
  420. let needToCacheOriginalImage = options.cacheOriginalImage &&
  421. options.processor != DefaultImageProcessor.default
  422. let coordinator = CacheCallbackCoordinator(
  423. shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage)
  424. let result = RetrieveImageResult(
  425. image: options.imageModifier?.modify(value.image) ?? value.image,
  426. cacheType: .none,
  427. source: source,
  428. originalSource: context.originalSource,
  429. data: { value.originalData }
  430. )
  431. // Add image to cache.
  432. let targetCache = options.targetCache ?? self.cache
  433. targetCache.store(
  434. value.image,
  435. original: value.originalData,
  436. forKey: source.cacheKey,
  437. options: options,
  438. toDisk: !options.cacheMemoryOnly)
  439. {
  440. _ in
  441. coordinator.apply(.cachingImage) {
  442. completionHandler?(.success(result))
  443. }
  444. }
  445. // Add original image to cache if necessary.
  446. if needToCacheOriginalImage {
  447. let originalCache = options.originalCache ?? targetCache
  448. originalCache.storeToDisk(
  449. value.originalData,
  450. forKey: source.cacheKey,
  451. processorIdentifier: DefaultImageProcessor.default.identifier,
  452. expiration: options.diskCacheExpiration)
  453. {
  454. _ in
  455. coordinator.apply(.cachingOriginalImage) {
  456. completionHandler?(.success(result))
  457. }
  458. }
  459. }
  460. coordinator.apply(.cacheInitiated) {
  461. completionHandler?(.success(result))
  462. }
  463. case .failure(let error):
  464. completionHandler?(.failure(error))
  465. }
  466. }
  467. @discardableResult
  468. func loadAndCacheImage(
  469. source: Source,
  470. context: RetrievingContext<Source>,
  471. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
  472. {
  473. let options = context.options
  474. @Sendable func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
  475. cacheImage(
  476. source: source,
  477. options: options,
  478. context: context,
  479. result: result,
  480. completionHandler: completionHandler
  481. )
  482. }
  483. switch source {
  484. case .network(let resource):
  485. let downloader = options.downloader ?? self.downloader
  486. let task = downloader.downloadImage(
  487. with: resource.downloadURL, options: options, completionHandler: _cacheImage
  488. )
  489. // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when
  490. // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler.
  491. // Let's fallback to a traditional style before it can be fixed in Swift.
  492. //
  493. // https://github.com/onevcat/Kingfisher/issues/1436
  494. //
  495. // return task.map(DownloadTask.WrappedTask.download)
  496. if task.isInitialized {
  497. return .download(task)
  498. } else {
  499. return nil
  500. }
  501. case .provider(let provider):
  502. provideImage(provider: provider, options: options, completionHandler: _cacheImage)
  503. return .dataProviding
  504. }
  505. }
  506. /// Retrieves an image from either memory or disk cache.
  507. ///
  508. /// - Parameters:
  509. /// - source: The target source from which to retrieve the image.
  510. /// - key: The key to use for caching the image.
  511. /// - url: The image request URL. This is not used when retrieving an image from the cache; it is solely used for
  512. /// compatibility with ``RetrieveImageResult`` callbacks.
  513. /// - options: Options on how to retrieve the image from the image cache.
  514. /// - completionHandler: Called when the image retrieval is complete, either with a successful
  515. /// ``RetrieveImageResult`` or an error.
  516. ///
  517. /// - Returns: `true` if the requested image or the original image before processing exists in the cache. Otherwise, this method returns `false`.
  518. ///
  519. /// - Note: Image retrieval can occur in either the memory cache or the disk cache. The
  520. /// ``KingfisherOptionsInfoItem/processor(_:)`` option in `options` is considered when searching the cache. If no
  521. /// processed image is found, Kingfisher attempts to determine whether an original version of the image exists. If
  522. /// an original exists, Kingfisher retrieves it from the cache and processes it. Subsequently, the processed image
  523. /// is stored back in the cache for future use.
  524. ///
  525. func retrieveImageFromCache(
  526. source: Source,
  527. context: RetrievingContext<Source>,
  528. completionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool
  529. {
  530. let options = context.options
  531. // 1. Check whether the image was already in target cache. If so, just get it.
  532. let targetCache = options.targetCache ?? cache
  533. let key = source.cacheKey
  534. let targetImageCached = targetCache.imageCachedType(
  535. forKey: key, processorIdentifier: options.processor.identifier)
  536. let validCache = targetImageCached.cached &&
  537. (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory)
  538. if validCache {
  539. targetCache.retrieveImage(forKey: key, options: options) { result in
  540. guard let completionHandler = completionHandler else { return }
  541. // TODO: Optimize it when we can use async across all the project.
  542. @Sendable func checkResultImageAndCallback(_ inputImage: KFCrossPlatformImage) {
  543. var image = inputImage
  544. if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, let data = image.kf.animatedImageData {
  545. // Always recreate animated image representation since it is possible to be loaded in different options.
  546. // https://github.com/onevcat/Kingfisher/issues/1923
  547. image = options.processor.process(item: .data(data), options: options) ?? .init()
  548. }
  549. if let modifier = options.imageModifier {
  550. image = modifier.modify(image)
  551. }
  552. let value = result.map {
  553. RetrieveImageResult(
  554. image: image,
  555. cacheType: $0.cacheType,
  556. source: source,
  557. originalSource: context.originalSource,
  558. data: { [image] in options.cacheSerializer.data(with: image, original: nil) }
  559. )
  560. }
  561. completionHandler(value)
  562. }
  563. result.match { cacheResult in
  564. options.callbackQueue.execute {
  565. guard let image = cacheResult.image else {
  566. completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))
  567. return
  568. }
  569. if options.cacheSerializer.originalDataUsed {
  570. let processor = options.processor
  571. (options.processingQueue ?? self.processingQueue).execute {
  572. let item = ImageProcessItem.image(image)
  573. guard let processedImage = processor.process(item: item, options: options) else {
  574. let error = KingfisherError.processorError(
  575. reason: .processingFailed(processor: processor, item: item))
  576. options.callbackQueue.execute { completionHandler(.failure(error)) }
  577. return
  578. }
  579. options.callbackQueue.execute {
  580. checkResultImageAndCallback(processedImage)
  581. }
  582. }
  583. } else {
  584. checkResultImageAndCallback(image)
  585. }
  586. }
  587. } onFailure: { error in
  588. options.callbackQueue.execute {
  589. completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))
  590. }
  591. }
  592. }
  593. return true
  594. }
  595. // 2. Check whether the original image exists. If so, get it, process it, save to storage and return.
  596. let originalCache = options.originalCache ?? targetCache
  597. // No need to store the same file in the same cache again.
  598. if originalCache === targetCache && options.processor == DefaultImageProcessor.default {
  599. return false
  600. }
  601. // Check whether the unprocessed image existing or not.
  602. let originalImageCacheType = originalCache.imageCachedType(
  603. forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier)
  604. let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh
  605. let canUseOriginalImageCache =
  606. (canAcceptDiskCache && originalImageCacheType.cached) ||
  607. (!canAcceptDiskCache && originalImageCacheType == .memory)
  608. if canUseOriginalImageCache {
  609. // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove
  610. // any processor from options first.
  611. var optionsWithoutProcessor = options
  612. optionsWithoutProcessor.processor = DefaultImageProcessor.default
  613. originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in
  614. result.match(
  615. onSuccess: { cacheResult in
  616. guard let image = cacheResult.image else {
  617. assertionFailure("The image (under key: \(key) should be existing in the original cache.")
  618. return
  619. }
  620. let processor = options.processor
  621. (options.processingQueue ?? self.processingQueue).execute {
  622. let item = ImageProcessItem.image(image)
  623. guard let processedImage = processor.process(item: item, options: options) else {
  624. let error = KingfisherError.processorError(
  625. reason: .processingFailed(processor: processor, item: item))
  626. options.callbackQueue.execute { completionHandler?(.failure(error)) }
  627. return
  628. }
  629. var cacheOptions = options
  630. cacheOptions.callbackQueue = .untouch
  631. let coordinator = CacheCallbackCoordinator(
  632. shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false)
  633. let image = options.imageModifier?.modify(processedImage) ?? processedImage
  634. let result = RetrieveImageResult(
  635. image: image,
  636. cacheType: .none,
  637. source: source,
  638. originalSource: context.originalSource,
  639. data: { options.cacheSerializer.data(with: processedImage, original: nil) }
  640. )
  641. targetCache.store(
  642. processedImage,
  643. forKey: key,
  644. options: cacheOptions,
  645. toDisk: !options.cacheMemoryOnly)
  646. {
  647. _ in
  648. coordinator.apply(.cachingImage) {
  649. options.callbackQueue.execute { completionHandler?(.success(result)) }
  650. }
  651. }
  652. coordinator.apply(.cacheInitiated) {
  653. options.callbackQueue.execute { completionHandler?(.success(result)) }
  654. }
  655. }
  656. },
  657. onFailure: { _ in
  658. // This should not happen actually, since we already confirmed `originalImageCached` is `true`.
  659. // Just in case...
  660. options.callbackQueue.execute {
  661. completionHandler?(
  662. .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))
  663. )
  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. }