ImagePrefetcher.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. //
  2. // ImagePrefetcher.swift
  3. // Kingfisher
  4. //
  5. // Created by Claire Knight <claire.knight@moggytech.co.uk> on 24/02/2016
  6. //
  7. // Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. #if os(macOS)
  27. import AppKit
  28. #else
  29. import UIKit
  30. #endif
  31. /// Progress update block of prefetcher when initialized with a list of resources.
  32. ///
  33. /// - Parameters:
  34. /// - skippedResources: An array of resources that are already cached before the prefetching begins.
  35. /// - failedResources: An array of resources that fail to be downloaded. This could be because of being cancelled while downloading, encountering an error during downloading, or the download not being started at all.
  36. /// - completedResources: An array of resources that are downloaded and cached successfully.
  37. public typealias PrefetcherProgressBlock =
  38. ((_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void)
  39. /// Progress update block of prefetcher when initialized with a list of resources.
  40. ///
  41. /// - Parameters:
  42. /// - skippedSources: An array of sources that are already cached before the prefetching begins.
  43. /// - failedSources: An array of sources that fail to be fetched.
  44. /// - completedResources: An array of sources that are fetched and cached successfully.
  45. public typealias PrefetcherSourceProgressBlock =
  46. ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
  47. /// Completion block of prefetcher when initialized with a list of sources.
  48. ///
  49. /// - Parameters:
  50. /// - skippedResources: An array of resources that are already cached before the prefetching begins.
  51. /// - failedResources: An array of resources that fail to be downloaded. This could be because of being cancelled while downloading, encountering an error during downloading, or the download not being started at all.
  52. /// - completedResources: An array of resources that are downloaded and cached successfully.
  53. public typealias PrefetcherCompletionHandler =
  54. ((_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void)
  55. /// Completion block of prefetcher when initialized with a list of sources.
  56. ///
  57. /// - Parameters:
  58. /// - skippedSources: An array of sources that are already cached before the prefetching begins.
  59. /// - failedSources: An array of sources that fail to be fetched.
  60. /// - completedSources: An array of sources that are fetched and cached successfully.
  61. public typealias PrefetcherSourceCompletionHandler =
  62. ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
  63. /// ``ImagePrefetcher`` represents a downloading manager for requesting many images via URLs and then caching them.
  64. ///
  65. /// Use this class when you know a list of image resources and want to download them before showing. It also works with
  66. /// some Cocoa prefetching mechanisms like table view or collection view `prefetchDataSource` to start image downloading
  67. /// and caching before they are displayed on screen.
  68. public class ImagePrefetcher: CustomStringConvertible, @unchecked Sendable {
  69. public var description: String {
  70. return "\(Unmanaged.passUnretained(self).toOpaque())"
  71. }
  72. /// The maximum concurrent downloads to use when prefetching images.
  73. ///
  74. /// The default is 5.
  75. public var maxConcurrentDownloads = 5
  76. private let prefetchSources: [Source]
  77. private let optionsInfo: KingfisherParsedOptionsInfo
  78. private var progressBlock: PrefetcherProgressBlock?
  79. private var completionHandler: PrefetcherCompletionHandler?
  80. private var progressSourceBlock: PrefetcherSourceProgressBlock?
  81. private var completionSourceHandler: PrefetcherSourceCompletionHandler?
  82. private var tasks = [String: DownloadTask.WrappedTask]()
  83. private var pendingSources: ArraySlice<Source>
  84. private var skippedSources = [Source]()
  85. private var completedSources = [Source]()
  86. private var failedSources = [Source]()
  87. private var stopped = false
  88. // A manager used for prefetching. We will use the helper methods in manager.
  89. private let manager: KingfisherManager
  90. private let prefetchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.prefetchQueue")
  91. private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue")
  92. private var finished: Bool {
  93. let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count
  94. return totalFinished == prefetchSources.count && tasks.isEmpty
  95. }
  96. /// Creates an image prefetcher with an array of URLs.
  97. ///
  98. /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
  99. /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the
  100. /// prefetching process. The images that are already cached will be skipped without being downloaded again.
  101. ///
  102. /// - Parameters:
  103. /// - urls: The URLs to be prefetched.
  104. /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information.
  105. /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled.
  106. /// - completionHandler: Called when the whole prefetching process is finished.
  107. ///
  108. /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache
  109. /// targets, respectively. You can specify other downloaders or caches by using a customized
  110. /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The
  111. /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method.
  112. public convenience init(
  113. urls: [URL],
  114. options: KingfisherOptionsInfo? = nil,
  115. progressBlock: PrefetcherProgressBlock? = nil,
  116. completionHandler: PrefetcherCompletionHandler? = nil)
  117. {
  118. let resources: [any Resource] = urls.map { $0 }
  119. self.init(
  120. resources: resources,
  121. options: options,
  122. progressBlock: progressBlock,
  123. completionHandler: completionHandler)
  124. }
  125. /// Creates an image prefetcher with an array of ``Resource``s.
  126. ///
  127. /// The prefetcher should be initiated with a list of prefetching targets. The resource list is immutable.
  128. /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the
  129. /// prefetching process. The images that are already cached will be skipped without being downloaded again.
  130. ///
  131. /// - Parameters:
  132. /// - resources: An array of resource to be prefetched. See ``ImageResource``.
  133. /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information.
  134. /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled.
  135. /// - completionHandler: Called when the whole prefetching process is finished.
  136. ///
  137. /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache
  138. /// targets, respectively. You can specify other downloaders or caches by using a customized
  139. /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The
  140. /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method.
  141. public convenience init(
  142. resources: [any Resource],
  143. options: KingfisherOptionsInfo? = nil,
  144. progressBlock: PrefetcherProgressBlock? = nil,
  145. completionHandler: PrefetcherCompletionHandler? = nil)
  146. {
  147. self.init(sources: resources.map { $0.convertToSource() }, options: options)
  148. self.progressBlock = progressBlock
  149. self.completionHandler = completionHandler
  150. }
  151. /// Creates an image prefetcher with an array of ``Source``s.
  152. ///
  153. /// The prefetcher should be initiated with a list of prefetching targets. The source list is immutable.
  154. /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the
  155. /// prefetching process. The images that are already cached will be skipped without being downloaded again.
  156. ///
  157. /// - Parameters:
  158. /// - sources: An array of resource to be prefetched. See ``Source``.
  159. /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information.
  160. /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled.
  161. /// - completionHandler: Called when the whole prefetching process is finished.
  162. ///
  163. /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache
  164. /// targets, respectively. You can specify other downloaders or caches by using a customized
  165. /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The
  166. /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method.
  167. public convenience init(sources: [Source],
  168. options: KingfisherOptionsInfo? = nil,
  169. progressBlock: PrefetcherSourceProgressBlock? = nil,
  170. completionHandler: PrefetcherSourceCompletionHandler? = nil)
  171. {
  172. self.init(sources: sources, options: options)
  173. self.progressSourceBlock = progressBlock
  174. self.completionSourceHandler = completionHandler
  175. }
  176. init(sources: [Source], options: KingfisherOptionsInfo?) {
  177. var options = KingfisherParsedOptionsInfo(options)
  178. prefetchSources = sources
  179. pendingSources = ArraySlice(sources)
  180. // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options.
  181. // Add our own callback dispatch queue to make sure all internal callbacks are
  182. // coming back in our expected queue.
  183. options.callbackQueue = .dispatch(prefetchQueue)
  184. optionsInfo = options
  185. let cache = optionsInfo.targetCache ?? .default
  186. let downloader = optionsInfo.downloader ?? .default
  187. manager = KingfisherManager(downloader: downloader, cache: cache)
  188. }
  189. /// Starts downloading the resources and caching them.
  190. ///
  191. /// This can be useful for the background downloading of assets that are required for later use in an app. This
  192. /// code will not try to update any UI with the results of the process.
  193. public func start() {
  194. prefetchQueue.async {
  195. guard !self.stopped else {
  196. assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.")
  197. self.handleComplete()
  198. return
  199. }
  200. guard self.maxConcurrentDownloads > 0 else {
  201. assertionFailure("There should be concurrent downloads value should be at least 1.")
  202. self.handleComplete()
  203. return
  204. }
  205. // Empty case.
  206. guard self.prefetchSources.count > 0 else {
  207. self.handleComplete()
  208. return
  209. }
  210. let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads)
  211. for _ in 0 ..< initialConcurrentDownloads {
  212. if let resource = self.pendingSources.popFirst() {
  213. self.startPrefetching(resource)
  214. }
  215. }
  216. }
  217. }
  218. /// Stops the current downloading progress and cancels any future prefetching activity that might be occurring.
  219. public func stop() {
  220. prefetchQueue.async {
  221. if self.finished { return }
  222. self.stopped = true
  223. self.tasks.values.forEach { $0.cancel() }
  224. }
  225. }
  226. private func downloadAndCache(_ source: Source) {
  227. let downloadTaskCompletionHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void) = {
  228. result in
  229. self.tasks.removeValue(forKey: source.cacheKey)
  230. do {
  231. let _ = try result.get()
  232. self.completedSources.append(source)
  233. } catch {
  234. self.failedSources.append(source)
  235. }
  236. self.reportProgress()
  237. if self.stopped {
  238. if self.tasks.isEmpty {
  239. self.failedSources.append(contentsOf: self.pendingSources)
  240. self.handleComplete()
  241. }
  242. } else {
  243. self.reportCompletionOrStartNext()
  244. }
  245. }
  246. var downloadTask: DownloadTask.WrappedTask?
  247. ImagePrefetcher.requestingQueue.sync {
  248. let context = RetrievingContext(
  249. options: optionsInfo, originalSource: source
  250. )
  251. downloadTask = manager.loadAndCacheImage(
  252. source: source,
  253. context: context,
  254. completionHandler: downloadTaskCompletionHandler)
  255. }
  256. if let downloadTask = downloadTask {
  257. tasks[source.cacheKey] = downloadTask
  258. }
  259. }
  260. private func append(cached source: Source) {
  261. skippedSources.append(source)
  262. reportProgress()
  263. reportCompletionOrStartNext()
  264. }
  265. private func startPrefetching(_ source: Source)
  266. {
  267. if optionsInfo.forceRefresh {
  268. downloadAndCache(source)
  269. return
  270. }
  271. let cacheType = manager.cache.imageCachedType(
  272. forKey: source.cacheKey,
  273. processorIdentifier: optionsInfo.processor.identifier
  274. )
  275. switch cacheType {
  276. case .memory:
  277. append(cached: source)
  278. case .disk:
  279. if optionsInfo.alsoPrefetchToMemory {
  280. let context = RetrievingContext(options: optionsInfo, originalSource: source)
  281. _ = manager.retrieveImageFromCache(
  282. source: source,
  283. context: context)
  284. {
  285. _ in
  286. self.append(cached: source)
  287. }
  288. } else {
  289. append(cached: source)
  290. }
  291. case .none:
  292. downloadAndCache(source)
  293. }
  294. }
  295. private func reportProgress() {
  296. if progressBlock == nil && progressSourceBlock == nil {
  297. return
  298. }
  299. let skipped = self.skippedSources
  300. let failed = self.failedSources
  301. let completed = self.completedSources
  302. CallbackQueue.mainCurrentOrAsync.execute {
  303. self.progressSourceBlock?(skipped, failed, completed)
  304. self.progressBlock?(
  305. skipped.compactMap { $0.asResource },
  306. failed.compactMap { $0.asResource },
  307. completed.compactMap { $0.asResource }
  308. )
  309. }
  310. }
  311. private func reportCompletionOrStartNext() {
  312. if let resource = self.pendingSources.popFirst() {
  313. // Loose call stack for huge amount of sources.
  314. prefetchQueue.async { self.startPrefetching(resource) }
  315. } else {
  316. guard allFinished else { return }
  317. self.handleComplete()
  318. }
  319. }
  320. var allFinished: Bool {
  321. return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count
  322. }
  323. private func handleComplete() {
  324. if completionHandler == nil && completionSourceHandler == nil {
  325. return
  326. }
  327. // The completion handler should be called on the main thread
  328. CallbackQueue.mainCurrentOrAsync.execute {
  329. self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources)
  330. self.completionHandler?(
  331. self.skippedSources.compactMap { $0.asResource },
  332. self.failedSources.compactMap { $0.asResource },
  333. self.completedSources.compactMap { $0.asResource }
  334. )
  335. self.completionHandler = nil
  336. self.progressBlock = nil
  337. }
  338. }
  339. }