Cache.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. //
  2. // Cache.swift
  3. // Haneke
  4. //
  5. // Created by Luis Ascorbe on 23/07/14.
  6. // Copyright (c) 2014 Haneke. All rights reserved.
  7. //
  8. import UIKit
  9. // Used to add T to NSCache
  10. class ObjectWrapper : NSObject {
  11. let hnk_value: Any
  12. init(value: Any) {
  13. self.hnk_value = value
  14. }
  15. }
  16. extension HanekeGlobals {
  17. // It'd be better to define this in the Cache class but Swift doesn't allow statics in a generic type
  18. public struct Cache {
  19. public static let OriginalFormatName = "original"
  20. public enum ErrorCode : Int {
  21. case objectNotFound = -100
  22. case formatNotFound = -101
  23. }
  24. }
  25. }
  26. open class Cache<T: DataConvertible> where T.Result == T, T : DataRepresentable {
  27. let name: String
  28. var memoryWarningObserver : NSObjectProtocol!
  29. public init(name: String) {
  30. self.name = name
  31. let notifications = NotificationCenter.default
  32. // Using block-based observer to avoid subclassing NSObject
  33. memoryWarningObserver = notifications.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification,
  34. object: nil,
  35. queue: OperationQueue.main,
  36. using: { [unowned self] (notification : Notification!) -> Void in
  37. self.onMemoryWarning()
  38. }
  39. )
  40. let originalFormat = Format<T>(name: HanekeGlobals.Cache.OriginalFormatName)
  41. self.addFormat(originalFormat)
  42. }
  43. deinit {
  44. let notifications = NotificationCenter.default
  45. notifications.removeObserver(memoryWarningObserver, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
  46. }
  47. open func set(value: T, key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed: ((T) -> ())? = nil) {
  48. if let (format, memoryCache, diskCache) = self.formats[formatName] {
  49. self.format(value: value, format: format) { formattedValue in
  50. let wrapper = ObjectWrapper(value: formattedValue)
  51. memoryCache.setObject(wrapper, forKey: key as AnyObject)
  52. // Value data is sent as @autoclosure to be executed in the disk cache queue.
  53. diskCache.setData(self.dataFromValue(formattedValue, format: format), key: key)
  54. succeed?(formattedValue)
  55. }
  56. } else {
  57. assertionFailure("Can't set value before adding format")
  58. }
  59. }
  60. @discardableResult open func fetch(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
  61. let fetch = Cache.buildFetch(failure: fail, success: succeed)
  62. if let (format, memoryCache, diskCache) = self.formats[formatName] {
  63. if let wrapper = memoryCache.object(forKey: key as AnyObject) as? ObjectWrapper, let result = wrapper.hnk_value as? T {
  64. fetch.succeed(result)
  65. diskCache.updateAccessDate(self.dataFromValue(result, format: format), key: key)
  66. return fetch
  67. }
  68. self.fetchFromDiskCache(diskCache, key: key, memoryCache: memoryCache, failure: { error in
  69. fetch.fail(error)
  70. }) { value in
  71. fetch.succeed(value)
  72. }
  73. } else {
  74. let localizedFormat = NSLocalizedString("Format %@ not found", comment: "Error description")
  75. let description = String(format:localizedFormat, formatName)
  76. let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue, description: description)
  77. fetch.fail(error)
  78. }
  79. return fetch
  80. }
  81. @discardableResult open func fetch(fetcher : Fetcher<T>, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
  82. let key = fetcher.key
  83. let fetch = Cache.buildFetch(failure: fail, success: succeed)
  84. self.fetch(key: key, formatName: formatName, failure: { error in
  85. if (error as NSError?)?.code == HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue {
  86. fetch.fail(error)
  87. }
  88. if let (format, _, _) = self.formats[formatName] {
  89. self.fetchAndSet(fetcher, format: format, failure: { error in
  90. fetch.fail(error)
  91. }) {value in
  92. fetch.succeed(value)
  93. }
  94. }
  95. // Unreachable code. Formats can't be removed from Cache.
  96. }) { value in
  97. fetch.succeed(value)
  98. }
  99. return fetch
  100. }
  101. open func remove(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName) {
  102. if let (_, memoryCache, diskCache) = self.formats[formatName] {
  103. memoryCache.removeObject(forKey: key as AnyObject)
  104. diskCache.removeData(with: key)
  105. }
  106. }
  107. open func removeAll(_ completion: (() -> ())? = nil) {
  108. let group = DispatchGroup()
  109. for (_, (_, memoryCache, diskCache)) in self.formats {
  110. memoryCache.removeAllObjects()
  111. group.enter()
  112. diskCache.removeAllData {
  113. group.leave()
  114. }
  115. }
  116. DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
  117. let timeout = DispatchTime.now() + Double(Int64(60 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
  118. if group.wait(timeout: timeout) != .success {
  119. Log.error(message: "removeAll timed out waiting for disk caches")
  120. }
  121. let path = self.cachePath
  122. do {
  123. try FileManager.default.removeItem(atPath: path)
  124. } catch {
  125. Log.error(message: "Failed to remove path \(path)", error: error)
  126. }
  127. if let completion = completion {
  128. DispatchQueue.main.async {
  129. completion()
  130. }
  131. }
  132. }
  133. }
  134. // MARK: Size
  135. open var size: UInt64 {
  136. var size: UInt64 = 0
  137. for (_, (_, _, diskCache)) in self.formats {
  138. diskCache.cacheQueue.sync { size += diskCache.size }
  139. }
  140. return size
  141. }
  142. // MARK: Notifications
  143. func onMemoryWarning() {
  144. for (_, (_, memoryCache, _)) in self.formats {
  145. memoryCache.removeAllObjects()
  146. }
  147. }
  148. // MARK: Formats
  149. public var formats : [String : (Format<T>, NSCache<AnyObject, AnyObject>, DiskCache)] = [:]
  150. open func addFormat(_ format : Format<T>) {
  151. let name = format.name
  152. let formatPath = self.formatPath(withFormatName: name)
  153. let memoryCache = NSCache<AnyObject, AnyObject>()
  154. let diskCache = DiskCache(path: formatPath, capacity : format.diskCapacity)
  155. self.formats[name] = (format, memoryCache, diskCache)
  156. }
  157. // MARK: Internal
  158. lazy var cachePath: String = {
  159. let basePath = DiskCache.basePath()
  160. let cachePath = (basePath as NSString).appendingPathComponent(self.name)
  161. return cachePath
  162. }()
  163. func formatPath(withFormatName formatName: String) -> String {
  164. let formatPath = (self.cachePath as NSString).appendingPathComponent(formatName)
  165. do {
  166. try FileManager.default.createDirectory(atPath: formatPath, withIntermediateDirectories: true, attributes: nil)
  167. } catch {
  168. Log.error(message: "Failed to create directory \(formatPath)", error: error)
  169. }
  170. return formatPath
  171. }
  172. // MARK: Private
  173. func dataFromValue(_ value : T, format : Format<T>) -> Data? {
  174. if let data = format.convertToData?(value) {
  175. return data as Data
  176. }
  177. return value.asData()
  178. }
  179. fileprivate func fetchFromDiskCache(_ diskCache : DiskCache, key: String, memoryCache : NSCache<AnyObject, AnyObject>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) {
  180. diskCache.fetchData(key: key, failure: { error in
  181. if let block = fail {
  182. if (error as NSError?)?.code == NSFileReadNoSuchFileError {
  183. let localizedFormat = NSLocalizedString("Object not found for key %@", comment: "Error description")
  184. let description = String(format:localizedFormat, key)
  185. let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.objectNotFound.rawValue, description: description)
  186. block(error)
  187. } else {
  188. block(error)
  189. }
  190. }
  191. }) { data in
  192. DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: {
  193. let value = T.convertFromData(data)
  194. if let value = value {
  195. let descompressedValue = self.decompressedImageIfNeeded(value)
  196. DispatchQueue.main.async(execute: {
  197. succeed(descompressedValue)
  198. let wrapper = ObjectWrapper(value: descompressedValue)
  199. memoryCache.setObject(wrapper, forKey: key as AnyObject)
  200. })
  201. }
  202. })
  203. }
  204. }
  205. fileprivate func fetchAndSet(_ fetcher : Fetcher<T>, format : Format<T>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) {
  206. fetcher.fetch(failure: { error in
  207. let _ = fail?(error)
  208. }) { value in
  209. self.set(value: value, key: fetcher.key, formatName: format.name, success: succeed)
  210. }
  211. }
  212. fileprivate func format(value : T, format : Format<T>, success succeed : @escaping (T) -> ()) {
  213. // HACK: Ideally Cache shouldn't treat images differently but I can't think of any other way of doing this that doesn't complicate the API for other types.
  214. if format.isIdentity && !(value is UIImage) {
  215. succeed(value)
  216. } else {
  217. DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
  218. var formatted = format.apply(value)
  219. if let formattedImage = formatted as? UIImage {
  220. let originalImage = value as? UIImage
  221. if formattedImage === originalImage {
  222. formatted = self.decompressedImageIfNeeded(formatted)
  223. }
  224. }
  225. DispatchQueue.main.async {
  226. succeed(formatted)
  227. }
  228. }
  229. }
  230. }
  231. fileprivate func decompressedImageIfNeeded(_ value : T) -> T {
  232. if let image = value as? UIImage {
  233. let decompressedImage = image.hnk_decompressedImage() as? T
  234. return decompressedImage!
  235. }
  236. return value
  237. }
  238. fileprivate class func buildFetch(failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
  239. let fetch = Fetch<T>()
  240. if let succeed = succeed {
  241. fetch.onSuccess(succeed)
  242. }
  243. if let fail = fail {
  244. fetch.onFailure(fail)
  245. }
  246. return fetch
  247. }
  248. // MARK: Convenience fetch
  249. // Ideally we would put each of these in the respective fetcher file as a Cache extension. Unfortunately, this fails to link when using the framework in a project as of Xcode 6.1.
  250. open func fetch(key: String, value getValue : @autoclosure @escaping () -> T.Result, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
  251. let fetcher = SimpleFetcher<T>(key: key, value: getValue())
  252. return self.fetch(fetcher: fetcher, formatName: formatName, success: succeed)
  253. }
  254. open func fetch(path: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
  255. let fetcher = DiskFetcher<T>(path: path)
  256. return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed)
  257. }
  258. open func fetch(URL : Foundation.URL, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
  259. let fetcher = NetworkFetcher<T>(URL: URL)
  260. return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed)
  261. }
  262. }