You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

372 lines
16 KiB

  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 pretchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.pretchQueue")
  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. /// - Note:
  104. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  105. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  106. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  107. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  108. public convenience init(
  109. urls: [URL],
  110. options: KingfisherOptionsInfo? = nil,
  111. progressBlock: PrefetcherProgressBlock? = nil,
  112. completionHandler: PrefetcherCompletionHandler? = nil)
  113. {
  114. let resources: [Resource] = urls.map { $0 }
  115. self.init(
  116. resources: resources,
  117. options: options,
  118. progressBlock: progressBlock,
  119. completionHandler: completionHandler)
  120. }
  121. /// Creates an image prefetcher with an array of resources.
  122. ///
  123. /// - Parameters:
  124. /// - resources: The resources which should be prefetched. See `Resource` type for more.
  125. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  126. /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled.
  127. /// - completionHandler: Called when the whole prefetching process finished.
  128. ///
  129. /// - Note:
  130. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  131. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  132. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  133. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  134. public convenience init(
  135. resources: [Resource],
  136. options: KingfisherOptionsInfo? = nil,
  137. progressBlock: PrefetcherProgressBlock? = nil,
  138. completionHandler: PrefetcherCompletionHandler? = nil)
  139. {
  140. self.init(sources: resources.map { $0.convertToSource() }, options: options)
  141. self.progressBlock = progressBlock
  142. self.completionHandler = completionHandler
  143. }
  144. /// Creates an image prefetcher with an array of sources.
  145. ///
  146. /// - Parameters:
  147. /// - sources: The sources which should be prefetched. See `Source` type for more.
  148. /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
  149. /// - progressBlock: Called every time an source fetching successes, fails, is skipped.
  150. /// - completionHandler: Called when the whole prefetching process finished.
  151. ///
  152. /// - Note:
  153. /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
  154. /// the downloader and cache target respectively. You can specify another downloader or cache by using
  155. /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
  156. /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
  157. public convenience init(sources: [Source],
  158. options: KingfisherOptionsInfo? = nil,
  159. progressBlock: PrefetcherSourceProgressBlock? = nil,
  160. completionHandler: PrefetcherSourceCompletionHandler? = nil)
  161. {
  162. self.init(sources: sources, options: options)
  163. self.progressSourceBlock = progressBlock
  164. self.completionSourceHandler = completionHandler
  165. }
  166. init(sources: [Source], options: KingfisherOptionsInfo?) {
  167. var options = KingfisherParsedOptionsInfo(options)
  168. prefetchSources = sources
  169. pendingSources = ArraySlice(sources)
  170. // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options.
  171. // Add our own callback dispatch queue to make sure all internal callbacks are
  172. // coming back in our expected queue.
  173. options.callbackQueue = .dispatch(pretchQueue)
  174. optionsInfo = options
  175. let cache = optionsInfo.targetCache ?? .default
  176. let downloader = optionsInfo.downloader ?? .default
  177. manager = KingfisherManager(downloader: downloader, cache: cache)
  178. }
  179. /// Starts to download the resources and cache them. This can be useful for background downloading
  180. /// of assets that are required for later use in an app. This code will not try and update any UI
  181. /// with the results of the process.
  182. public func start() {
  183. pretchQueue.async {
  184. guard !self.stopped else {
  185. assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.")
  186. self.handleComplete()
  187. return
  188. }
  189. guard self.maxConcurrentDownloads > 0 else {
  190. assertionFailure("There should be concurrent downloads value should be at least 1.")
  191. self.handleComplete()
  192. return
  193. }
  194. // Empty case.
  195. guard self.prefetchSources.count > 0 else {
  196. self.handleComplete()
  197. return
  198. }
  199. let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads)
  200. for _ in 0 ..< initialConcurrentDownloads {
  201. if let resource = self.pendingSources.popFirst() {
  202. self.startPrefetching(resource)
  203. }
  204. }
  205. }
  206. }
  207. /// Stops current downloading progress, and cancel any future prefetching activity that might be occuring.
  208. public func stop() {
  209. pretchQueue.async {
  210. if self.finished { return }
  211. self.stopped = true
  212. self.tasks.values.forEach { $0.cancel() }
  213. }
  214. }
  215. private func downloadAndCache(_ source: Source) {
  216. let downloadTaskCompletionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void) = { result in
  217. self.tasks.removeValue(forKey: source.cacheKey)
  218. do {
  219. let _ = try result.get()
  220. self.completedSources.append(source)
  221. } catch {
  222. self.failedSources.append(source)
  223. }
  224. self.reportProgress()
  225. if self.stopped {
  226. if self.tasks.isEmpty {
  227. self.failedSources.append(contentsOf: self.pendingSources)
  228. self.handleComplete()
  229. }
  230. } else {
  231. self.reportCompletionOrStartNext()
  232. }
  233. }
  234. var downloadTask: DownloadTask.WrappedTask?
  235. ImagePrefetcher.requestingQueue.sync {
  236. let context = RetrievingContext(
  237. options: optionsInfo, originalSource: source
  238. )
  239. downloadTask = manager.loadAndCacheImage(
  240. source: source,
  241. context: context,
  242. completionHandler: downloadTaskCompletionHandler)
  243. }
  244. if let downloadTask = downloadTask {
  245. tasks[source.cacheKey] = downloadTask
  246. }
  247. }
  248. private func append(cached source: Source) {
  249. skippedSources.append(source)
  250. reportProgress()
  251. reportCompletionOrStartNext()
  252. }
  253. private func startPrefetching(_ source: Source)
  254. {
  255. if optionsInfo.forceRefresh {
  256. downloadAndCache(source)
  257. return
  258. }
  259. let cacheType = manager.cache.imageCachedType(
  260. forKey: source.cacheKey,
  261. processorIdentifier: optionsInfo.processor.identifier)
  262. switch cacheType {
  263. case .memory:
  264. append(cached: source)
  265. case .disk:
  266. if optionsInfo.alsoPrefetchToMemory {
  267. let context = RetrievingContext(options: optionsInfo, originalSource: source)
  268. _ = manager.retrieveImageFromCache(
  269. source: source,
  270. context: context)
  271. {
  272. _ in
  273. self.append(cached: source)
  274. }
  275. } else {
  276. append(cached: source)
  277. }
  278. case .none:
  279. downloadAndCache(source)
  280. }
  281. }
  282. private func reportProgress() {
  283. if progressBlock == nil && progressSourceBlock == nil {
  284. return
  285. }
  286. let skipped = self.skippedSources
  287. let failed = self.failedSources
  288. let completed = self.completedSources
  289. CallbackQueue.mainCurrentOrAsync.execute {
  290. self.progressSourceBlock?(skipped, failed, completed)
  291. self.progressBlock?(
  292. skipped.compactMap { $0.asResource },
  293. failed.compactMap { $0.asResource },
  294. completed.compactMap { $0.asResource }
  295. )
  296. }
  297. }
  298. private func reportCompletionOrStartNext() {
  299. if let resource = self.pendingSources.popFirst() {
  300. // Loose call stack for huge ammount of sources.
  301. pretchQueue.async { self.startPrefetching(resource) }
  302. } else {
  303. guard allFinished else { return }
  304. self.handleComplete()
  305. }
  306. }
  307. var allFinished: Bool {
  308. return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count
  309. }
  310. private func handleComplete() {
  311. if completionHandler == nil && completionSourceHandler == nil {
  312. return
  313. }
  314. // The completion handler should be called on the main thread
  315. CallbackQueue.mainCurrentOrAsync.execute {
  316. self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources)
  317. self.completionHandler?(
  318. self.skippedSources.compactMap { $0.asResource },
  319. self.failedSources.compactMap { $0.asResource },
  320. self.completedSources.compactMap { $0.asResource }
  321. )
  322. self.completionHandler = nil
  323. self.progressBlock = nil
  324. }
  325. }
  326. }