ImagePrefetcher.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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. /// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
  34. /// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while
  35. /// downloading, encountered an error when 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: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void)
  39. /// Progress update block of prefetcher when initialized with a list of resources.
  40. ///
  41. /// - `skippedSources`: An array of sources that are already cached before the prefetching starting.
  42. /// - `failedSources`: An array of sources that fail to be fetched.
  43. /// - `completedResources`: An array of sources that are fetched and cached successfully.
  44. public typealias PrefetcherSourceProgressBlock =
  45. ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
  46. /// Completion block of prefetcher when initialized with a list of sources.
  47. ///
  48. /// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
  49. /// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while
  50. /// downloading, encountered an error when downloading or the download not being started at all.
  51. /// - `completedResources`: An array of resources that are downloaded and cached successfully.
  52. public typealias PrefetcherCompletionHandler =
  53. ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void)
  54. /// Completion block of prefetcher when initialized with a list of sources.
  55. ///
  56. /// - `skippedSources`: An array of sources that are already cached before the prefetching starting.
  57. /// - `failedSources`: An array of sources that fail to be fetched.
  58. /// - `completedSources`: An array of sources that are fetched and cached successfully.
  59. public typealias PrefetcherSourceCompletionHandler =
  60. ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
  61. /// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them.
  62. /// This is useful when you know a list of image resources and want to download them before showing. It also works with
  63. /// some Cocoa prefetching mechanism like table view or collection view `prefetchDataSource`, to start image downloading
  64. /// and caching before they display on screen.
  65. public class ImagePrefetcher: CustomStringConvertible {
  66. public var description: String {
  67. return "\(Unmanaged.passUnretained(self).toOpaque())"
  68. }
  69. /// The maximum concurrent downloads to use when prefetching images. Default is 5.
  70. public var maxConcurrentDownloads = 5
  71. private let prefetchSources: [Source]
  72. private let optionsInfo: KingfisherParsedOptionsInfo
  73. private var progressBlock: PrefetcherProgressBlock?
  74. private var completionHandler: PrefetcherCompletionHandler?
  75. private var progressSourceBlock: PrefetcherSourceProgressBlock?
  76. private var completionSourceHandler: PrefetcherSourceCompletionHandler?
  77. private var tasks = [String: DownloadTask.WrappedTask]()
  78. private var pendingSources: ArraySlice<Source>
  79. private var skippedSources = [Source]()
  80. private var completedSources = [Source]()
  81. private var failedSources = [Source]()
  82. private var stopped = false
  83. // A manager used for prefetching. We will use the helper methods in manager.
  84. private let manager: KingfisherManager
  85. private let prefetchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.prefetchQueue")
  86. private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue")
  87. private var finished: Bool {
  88. let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count
  89. return totalFinished == prefetchSources.count && tasks.isEmpty
  90. }
  91. /// Creates an image prefetcher with an array of URLs.
  92. ///
  93. /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
  94. /// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process.
  95. /// The images which are already cached will be skipped without downloading again.
  96. ///
  97. /// - Parameters:
  98. /// - urls: The URLs which should be prefetched.
  99. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  100. /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled.
  101. /// - completionHandler: Called when the whole prefetching process finished.
  102. ///
  103. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  104. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  105. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  106. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  107. public convenience init(
  108. urls: [URL],
  109. options: KingfisherOptionsInfo? = nil,
  110. progressBlock: PrefetcherProgressBlock? = nil,
  111. completionHandler: PrefetcherCompletionHandler? = nil)
  112. {
  113. let resources: [Resource] = urls.map { $0 }
  114. self.init(
  115. resources: resources,
  116. options: options,
  117. progressBlock: progressBlock,
  118. completionHandler: completionHandler)
  119. }
  120. /// Creates an image prefetcher with an array of URLs.
  121. ///
  122. /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
  123. /// After you get a valid `ImagePrefetcher` object, you call ``start()`` on it to begin the prefetching process.
  124. /// The images which are already cached will be skipped without downloading again.
  125. ///
  126. /// - Parameters:
  127. /// - urls: The URLs which should be prefetched.
  128. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  129. /// - completionHandler: Called when the whole prefetching process finished.
  130. ///
  131. /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as
  132. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  133. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  134. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  135. public convenience init(
  136. urls: [URL],
  137. options: KingfisherOptionsInfo? = nil,
  138. completionHandler: PrefetcherCompletionHandler? = nil)
  139. {
  140. let resources: [Resource] = urls.map { $0 }
  141. self.init(
  142. resources: resources,
  143. options: options,
  144. progressBlock: nil,
  145. completionHandler: completionHandler)
  146. }
  147. /// Creates an image prefetcher with an array of resources.
  148. ///
  149. /// - Parameters:
  150. /// - resources: The resources which should be prefetched. See `Resource` type for more.
  151. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  152. /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled.
  153. /// - completionHandler: Called when the whole prefetching process finished.
  154. ///
  155. /// - Note:
  156. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  157. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  158. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  159. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  160. public convenience init(
  161. resources: [Resource],
  162. options: KingfisherOptionsInfo? = nil,
  163. progressBlock: PrefetcherProgressBlock? = nil,
  164. completionHandler: PrefetcherCompletionHandler? = nil)
  165. {
  166. self.init(sources: resources.map { $0.convertToSource() }, options: options)
  167. self.progressBlock = progressBlock
  168. self.completionHandler = completionHandler
  169. }
  170. /// Creates an image prefetcher with an array of resources.
  171. ///
  172. /// - Parameters:
  173. /// - resources: The resources which should be prefetched. See `Resource` type for more.
  174. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  175. /// - completionHandler: Called when the whole prefetching process finished.
  176. ///
  177. /// - Note:
  178. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  179. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  180. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  181. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  182. public convenience init(
  183. resources: [Resource],
  184. options: KingfisherOptionsInfo? = nil,
  185. completionHandler: PrefetcherCompletionHandler? = nil)
  186. {
  187. self.init(sources: resources.map { $0.convertToSource() }, options: options)
  188. self.completionHandler = completionHandler
  189. }
  190. /// Creates an image prefetcher with an array of sources.
  191. ///
  192. /// - Parameters:
  193. /// - sources: The sources which should be prefetched. See `Source` type for more.
  194. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  195. /// - progressBlock: Called every time an source fetching successes, fails, is skipped.
  196. /// - completionHandler: Called when the whole prefetching process finished.
  197. ///
  198. /// - Note:
  199. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  200. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  201. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  202. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  203. public convenience init(sources: [Source],
  204. options: KingfisherOptionsInfo? = nil,
  205. progressBlock: PrefetcherSourceProgressBlock? = nil,
  206. completionHandler: PrefetcherSourceCompletionHandler? = nil)
  207. {
  208. self.init(sources: sources, options: options)
  209. self.progressSourceBlock = progressBlock
  210. self.completionSourceHandler = completionHandler
  211. }
  212. /// Creates an image prefetcher with an array of sources.
  213. ///
  214. /// - Parameters:
  215. /// - sources: The sources which should be prefetched. See `Source` type for more.
  216. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  217. /// - completionHandler: Called when the whole prefetching process finished.
  218. ///
  219. /// - Note:
  220. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  221. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  222. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  223. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  224. public convenience init(sources: [Source],
  225. options: KingfisherOptionsInfo? = nil,
  226. completionHandler: PrefetcherSourceCompletionHandler? = nil)
  227. {
  228. self.init(sources: sources, options: options)
  229. self.completionSourceHandler = completionHandler
  230. }
  231. init(sources: [Source], options: KingfisherOptionsInfo?) {
  232. var options = KingfisherParsedOptionsInfo(options)
  233. prefetchSources = sources
  234. pendingSources = ArraySlice(sources)
  235. // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options.
  236. // Add our own callback dispatch queue to make sure all internal callbacks are
  237. // coming back in our expected queue.
  238. options.callbackQueue = .dispatch(prefetchQueue)
  239. optionsInfo = options
  240. let cache = optionsInfo.targetCache ?? .default
  241. let downloader = optionsInfo.downloader ?? .default
  242. manager = KingfisherManager(downloader: downloader, cache: cache)
  243. }
  244. /// Starts to download the resources and cache them. This can be useful for background downloading
  245. /// of assets that are required for later use in an app. This code will not try and update any UI
  246. /// with the results of the process.
  247. public func start() {
  248. prefetchQueue.async {
  249. guard !self.stopped else {
  250. assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.")
  251. self.handleComplete()
  252. return
  253. }
  254. guard self.maxConcurrentDownloads > 0 else {
  255. assertionFailure("There should be concurrent downloads value should be at least 1.")
  256. self.handleComplete()
  257. return
  258. }
  259. // Empty case.
  260. guard self.prefetchSources.count > 0 else {
  261. self.handleComplete()
  262. return
  263. }
  264. let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads)
  265. for _ in 0 ..< initialConcurrentDownloads {
  266. if let resource = self.pendingSources.popFirst() {
  267. self.startPrefetching(resource)
  268. }
  269. }
  270. }
  271. }
  272. /// Stops current downloading progress, and cancel any future prefetching activity that might be occuring.
  273. public func stop() {
  274. prefetchQueue.async {
  275. if self.finished { return }
  276. self.stopped = true
  277. self.tasks.values.forEach { $0.cancel() }
  278. }
  279. }
  280. private func downloadAndCache(_ source: Source) {
  281. let downloadTaskCompletionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void) = { result in
  282. self.tasks.removeValue(forKey: source.cacheKey)
  283. do {
  284. let _ = try result.get()
  285. self.completedSources.append(source)
  286. } catch {
  287. self.failedSources.append(source)
  288. }
  289. self.reportProgress()
  290. if self.stopped {
  291. if self.tasks.isEmpty {
  292. self.failedSources.append(contentsOf: self.pendingSources)
  293. self.handleComplete()
  294. }
  295. } else {
  296. self.reportCompletionOrStartNext()
  297. }
  298. }
  299. var downloadTask: DownloadTask.WrappedTask?
  300. ImagePrefetcher.requestingQueue.sync {
  301. let context = RetrievingContext(
  302. options: optionsInfo, originalSource: source
  303. )
  304. downloadTask = manager.loadAndCacheImage(
  305. source: source,
  306. context: context,
  307. completionHandler: downloadTaskCompletionHandler)
  308. }
  309. if let downloadTask = downloadTask {
  310. tasks[source.cacheKey] = downloadTask
  311. }
  312. }
  313. private func append(cached source: Source) {
  314. skippedSources.append(source)
  315. reportProgress()
  316. reportCompletionOrStartNext()
  317. }
  318. private func startPrefetching(_ source: Source)
  319. {
  320. if optionsInfo.forceRefresh {
  321. downloadAndCache(source)
  322. return
  323. }
  324. let cacheType = manager.cache.imageCachedType(
  325. forKey: source.cacheKey,
  326. processorIdentifier: optionsInfo.processor.identifier)
  327. switch cacheType {
  328. case .memory:
  329. append(cached: source)
  330. case .disk:
  331. if optionsInfo.alsoPrefetchToMemory {
  332. let context = RetrievingContext(options: optionsInfo, originalSource: source)
  333. _ = manager.retrieveImageFromCache(
  334. source: source,
  335. context: context)
  336. {
  337. _ in
  338. self.append(cached: source)
  339. }
  340. } else {
  341. append(cached: source)
  342. }
  343. case .none:
  344. downloadAndCache(source)
  345. }
  346. }
  347. private func reportProgress() {
  348. if progressBlock == nil && progressSourceBlock == nil {
  349. return
  350. }
  351. let skipped = self.skippedSources
  352. let failed = self.failedSources
  353. let completed = self.completedSources
  354. CallbackQueue.mainCurrentOrAsync.execute {
  355. self.progressSourceBlock?(skipped, failed, completed)
  356. self.progressBlock?(
  357. skipped.compactMap { $0.asResource },
  358. failed.compactMap { $0.asResource },
  359. completed.compactMap { $0.asResource }
  360. )
  361. }
  362. }
  363. private func reportCompletionOrStartNext() {
  364. if let resource = self.pendingSources.popFirst() {
  365. // Loose call stack for huge ammount of sources.
  366. prefetchQueue.async { self.startPrefetching(resource) }
  367. } else {
  368. guard allFinished else { return }
  369. self.handleComplete()
  370. }
  371. }
  372. var allFinished: Bool {
  373. return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count
  374. }
  375. private func handleComplete() {
  376. if completionHandler == nil && completionSourceHandler == nil {
  377. return
  378. }
  379. // The completion handler should be called on the main thread
  380. CallbackQueue.mainCurrentOrAsync.execute {
  381. self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources)
  382. self.completionHandler?(
  383. self.skippedSources.compactMap { $0.asResource },
  384. self.failedSources.compactMap { $0.asResource },
  385. self.completedSources.compactMap { $0.asResource }
  386. )
  387. self.completionHandler = nil
  388. self.progressBlock = nil
  389. }
  390. }
  391. }