EGOCache.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. //
  2. // EGOCache.m
  3. // enormego
  4. //
  5. // Created by Shaun Harrison.
  6. // Copyright (c) 2009-2017 enormego.
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy
  9. // of this software and associated documentation files (the "Software"), to deal
  10. // in the Software without restriction, including without limitation the rights
  11. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  12. // copies of the Software, and to permit persons to whom the Software is
  13. // furnished to do so, subject to the following conditions:
  14. //
  15. // The above copyright notice and this permission notice shall be included in
  16. // all copies or substantial portions of the Software.
  17. //
  18. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  24. // THE SOFTWARE.
  25. //
  26. #import "EGOCache.h"
  27. #if DEBUG
  28. # define CHECK_FOR_EGOCACHE_PLIST() if([key isEqualToString:@"EGOCache.plist"]) { \
  29. NSLog(@"EGOCache.plist is a reserved key and can not be modified."); \
  30. return; }
  31. #else
  32. # define CHECK_FOR_EGOCACHE_PLIST() if([key isEqualToString:@"EGOCache.plist"]) return;
  33. #endif
  34. static inline NSString* cachePathForKey(NSString* directory, NSString* key) {
  35. key = [key stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
  36. return [directory stringByAppendingPathComponent:key];
  37. }
  38. #pragma mark -
  39. @interface EGOCache () {
  40. dispatch_queue_t _cacheInfoQueue;
  41. dispatch_queue_t _frozenCacheInfoQueue;
  42. dispatch_queue_t _diskQueue;
  43. NSMutableDictionary* _cacheInfo;
  44. NSString* _directory;
  45. BOOL _needsSave;
  46. }
  47. @property(nonatomic,copy) NSDictionary* frozenCacheInfo;
  48. @end
  49. @implementation EGOCache
  50. + (instancetype)globalCache {
  51. static id instance;
  52. static dispatch_once_t onceToken;
  53. dispatch_once(&onceToken, ^{
  54. instance = [[[self class] alloc] init];
  55. });
  56. return instance;
  57. }
  58. - (instancetype)init {
  59. NSString* cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
  60. NSString* oldCachesDirectory = [[[cachesDirectory stringByAppendingPathComponent:[[NSProcessInfo processInfo] processName]] stringByAppendingPathComponent:@"EGOCache"] copy];
  61. if([[NSFileManager defaultManager] fileExistsAtPath:oldCachesDirectory]) {
  62. [[NSFileManager defaultManager] removeItemAtPath:oldCachesDirectory error:NULL];
  63. }
  64. cachesDirectory = [[[cachesDirectory stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] stringByAppendingPathComponent:@"EGOCache"] copy];
  65. return [self initWithCacheDirectory:cachesDirectory];
  66. }
  67. - (instancetype)initWithCacheDirectory:(NSString*)cacheDirectory {
  68. if((self = [super init])) {
  69. _cacheInfoQueue = dispatch_queue_create("com.enormego.egocache.info", DISPATCH_QUEUE_SERIAL);
  70. dispatch_queue_t priority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
  71. dispatch_set_target_queue(priority, _cacheInfoQueue);
  72. _frozenCacheInfoQueue = dispatch_queue_create("com.enormego.egocache.info.frozen", DISPATCH_QUEUE_SERIAL);
  73. priority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
  74. dispatch_set_target_queue(priority, _frozenCacheInfoQueue);
  75. _diskQueue = dispatch_queue_create("com.enormego.egocache.disk", DISPATCH_QUEUE_CONCURRENT);
  76. priority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
  77. dispatch_set_target_queue(priority, _diskQueue);
  78. _directory = cacheDirectory;
  79. _cacheInfo = [[NSDictionary dictionaryWithContentsOfFile:cachePathForKey(_directory, @"EGOCache.plist")] mutableCopy];
  80. if(!_cacheInfo) {
  81. _cacheInfo = [[NSMutableDictionary alloc] init];
  82. }
  83. [[NSFileManager defaultManager] createDirectoryAtPath:_directory withIntermediateDirectories:YES attributes:nil error:NULL];
  84. NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate];
  85. NSMutableArray* removedKeys = [[NSMutableArray alloc] init];
  86. for(NSString* key in _cacheInfo) {
  87. if([_cacheInfo[key] timeIntervalSinceReferenceDate] <= now) {
  88. [[NSFileManager defaultManager] removeItemAtPath:cachePathForKey(_directory, key) error:NULL];
  89. [removedKeys addObject:key];
  90. }
  91. }
  92. [_cacheInfo removeObjectsForKeys:removedKeys];
  93. self.frozenCacheInfo = _cacheInfo;
  94. [self setDefaultTimeoutInterval:86400];
  95. }
  96. return self;
  97. }
  98. - (void)clearCache {
  99. dispatch_sync(_cacheInfoQueue, ^{
  100. for(NSString* key in _cacheInfo) {
  101. [[NSFileManager defaultManager] removeItemAtPath:cachePathForKey(_directory, key) error:NULL];
  102. }
  103. [_cacheInfo removeAllObjects];
  104. dispatch_sync(_frozenCacheInfoQueue, ^{
  105. self.frozenCacheInfo = [_cacheInfo copy];
  106. });
  107. [self setNeedsSave];
  108. });
  109. }
  110. - (void)removeCacheForKey:(NSString*)key {
  111. CHECK_FOR_EGOCACHE_PLIST();
  112. dispatch_async(_diskQueue, ^{
  113. [[NSFileManager defaultManager] removeItemAtPath:cachePathForKey(_directory, key) error:NULL];
  114. });
  115. [self setCacheTimeoutInterval:0 forKey:key];
  116. }
  117. - (BOOL)hasCacheForKey:(NSString*)key {
  118. NSDate* date = [self dateForKey:key];
  119. if(date == nil) return NO;
  120. if([date timeIntervalSinceReferenceDate] < CFAbsoluteTimeGetCurrent()) return NO;
  121. return [[NSFileManager defaultManager] fileExistsAtPath:cachePathForKey(_directory, key)];
  122. }
  123. - (NSDate*)dateForKey:(NSString*)key {
  124. __block NSDate* date = nil;
  125. dispatch_sync(_frozenCacheInfoQueue, ^{
  126. date = (self.frozenCacheInfo)[key];
  127. });
  128. return date;
  129. }
  130. - (NSArray*)allKeys {
  131. __block NSArray* keys = nil;
  132. dispatch_sync(_frozenCacheInfoQueue, ^{
  133. keys = [self.frozenCacheInfo allKeys];
  134. });
  135. return keys;
  136. }
  137. - (void)setCacheTimeoutInterval:(NSTimeInterval)timeoutInterval forKey:(NSString*)key {
  138. NSDate* date = timeoutInterval > 0 ? [NSDate dateWithTimeIntervalSinceNow:timeoutInterval] : nil;
  139. // Temporarily store in the frozen state for quick reads
  140. dispatch_sync(_frozenCacheInfoQueue, ^{
  141. NSMutableDictionary* info = [self.frozenCacheInfo mutableCopy];
  142. if(date) {
  143. info[key] = date;
  144. } else {
  145. [info removeObjectForKey:key];
  146. }
  147. self.frozenCacheInfo = info;
  148. });
  149. // Save the final copy (this may be blocked by other operations)
  150. dispatch_async(_cacheInfoQueue, ^{
  151. if(date) {
  152. _cacheInfo[key] = date;
  153. } else {
  154. [_cacheInfo removeObjectForKey:key];
  155. }
  156. dispatch_sync(_frozenCacheInfoQueue, ^{
  157. self.frozenCacheInfo = [_cacheInfo copy];
  158. });
  159. [self setNeedsSave];
  160. });
  161. }
  162. #pragma mark -
  163. #pragma mark Copy file methods
  164. - (void)copyFilePath:(NSString*)filePath asKey:(NSString*)key {
  165. [self copyFilePath:filePath asKey:key withTimeoutInterval:self.defaultTimeoutInterval];
  166. }
  167. - (void)copyFilePath:(NSString*)filePath asKey:(NSString*)key withTimeoutInterval:(NSTimeInterval)timeoutInterval {
  168. dispatch_async(_diskQueue, ^{
  169. [[NSFileManager defaultManager] copyItemAtPath:filePath toPath:cachePathForKey(_directory, key) error:NULL];
  170. });
  171. [self setCacheTimeoutInterval:timeoutInterval forKey:key];
  172. }
  173. #pragma mark -
  174. #pragma mark Data methods
  175. - (void)setData:(NSData*)data forKey:(NSString*)key {
  176. [self setData:data forKey:key withTimeoutInterval:self.defaultTimeoutInterval];
  177. }
  178. - (void)setData:(NSData*)data forKey:(NSString*)key withTimeoutInterval:(NSTimeInterval)timeoutInterval {
  179. CHECK_FOR_EGOCACHE_PLIST();
  180. NSString* cachePath = cachePathForKey(_directory, key);
  181. dispatch_async(_diskQueue, ^{
  182. [data writeToFile:cachePath atomically:YES];
  183. });
  184. [self setCacheTimeoutInterval:timeoutInterval forKey:key];
  185. }
  186. - (void)setNeedsSave {
  187. dispatch_async(_cacheInfoQueue, ^{
  188. if(_needsSave) return;
  189. _needsSave = YES;
  190. double delayInSeconds = 0.5;
  191. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
  192. dispatch_after(popTime, _cacheInfoQueue, ^(void){
  193. if(!_needsSave) return;
  194. [_cacheInfo writeToFile:cachePathForKey(_directory, @"EGOCache.plist") atomically:YES];
  195. _needsSave = NO;
  196. });
  197. });
  198. }
  199. - (NSData*)dataForKey:(NSString*)key {
  200. if([self hasCacheForKey:key]) {
  201. return [NSData dataWithContentsOfFile:cachePathForKey(_directory, key) options:0 error:NULL];
  202. } else {
  203. return nil;
  204. }
  205. }
  206. #pragma mark -
  207. #pragma mark String methods
  208. - (NSString*)stringForKey:(NSString*)key {
  209. return [[NSString alloc] initWithData:[self dataForKey:key] encoding:NSUTF8StringEncoding];
  210. }
  211. - (void)setString:(NSString*)aString forKey:(NSString*)key {
  212. [self setString:aString forKey:key withTimeoutInterval:self.defaultTimeoutInterval];
  213. }
  214. - (void)setString:(NSString*)aString forKey:(NSString*)key withTimeoutInterval:(NSTimeInterval)timeoutInterval {
  215. [self setData:[aString dataUsingEncoding:NSUTF8StringEncoding] forKey:key withTimeoutInterval:timeoutInterval];
  216. }
  217. #pragma mark -
  218. #pragma mark Image methds
  219. #if TARGET_OS_IPHONE
  220. - (UIImage*)imageForKey:(NSString*)key {
  221. UIImage* image = nil;
  222. @try {
  223. image = [NSKeyedUnarchiver unarchiveObjectWithFile:cachePathForKey(_directory, key)];
  224. } @catch (NSException* e) {
  225. // Surpress any unarchiving exceptions and continue with nil
  226. }
  227. return image;
  228. }
  229. - (void)setImage:(UIImage*)anImage forKey:(NSString*)key {
  230. [self setImage:anImage forKey:key withTimeoutInterval:self.defaultTimeoutInterval];
  231. }
  232. - (void)setImage:(UIImage*)anImage forKey:(NSString*)key withTimeoutInterval:(NSTimeInterval)timeoutInterval {
  233. @try {
  234. // Using NSKeyedArchiver preserves all information such as scale, orientation, and the proper image format instead of saving everything as pngs
  235. [self setData:[NSKeyedArchiver archivedDataWithRootObject:anImage] forKey:key withTimeoutInterval:timeoutInterval];
  236. } @catch (NSException* e) {
  237. // Something went wrong, but we'll fail silently.
  238. }
  239. }
  240. #else
  241. - (NSImage*)imageForKey:(NSString*)key {
  242. return [[NSImage alloc] initWithData:[self dataForKey:key]];
  243. }
  244. - (void)setImage:(NSImage*)anImage forKey:(NSString*)key {
  245. [self setImage:anImage forKey:key withTimeoutInterval:self.defaultTimeoutInterval];
  246. }
  247. - (void)setImage:(NSImage*)anImage forKey:(NSString*)key withTimeoutInterval:(NSTimeInterval)timeoutInterval {
  248. [self setData:[[NSBitmapImageRep imageRepWithData:anImage.TIFFRepresentation] representationUsingType:NSPNGFileType properties:@{ }] forKey:key withTimeoutInterval:timeoutInterval];
  249. }
  250. #endif
  251. #pragma mark -
  252. #pragma mark Property List methods
  253. - (NSData*)plistForKey:(NSString*)key; {
  254. NSData* plistData = [self dataForKey:key];
  255. return [NSPropertyListSerialization propertyListWithData:plistData options:NSPropertyListImmutable format:nil error:nil];
  256. }
  257. - (void)setPlist:(id)plistObject forKey:(NSString*)key; {
  258. [self setPlist:plistObject forKey:key withTimeoutInterval:self.defaultTimeoutInterval];
  259. }
  260. - (void)setPlist:(id)plistObject forKey:(NSString*)key withTimeoutInterval:(NSTimeInterval)timeoutInterval; {
  261. // Binary plists are used over XML for better performance
  262. NSData* plistData = [NSPropertyListSerialization dataWithPropertyList:plistObject format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
  263. if(plistData != nil) {
  264. [self setData:plistData forKey:key withTimeoutInterval:timeoutInterval];
  265. }
  266. }
  267. #pragma mark -
  268. #pragma mark Object methods
  269. - (id<NSCoding>)objectForKey:(NSString*)key {
  270. if([self hasCacheForKey:key]) {
  271. return [NSKeyedUnarchiver unarchiveObjectWithData:[self dataForKey:key]];
  272. } else {
  273. return nil;
  274. }
  275. }
  276. - (void)setObject:(id<NSCoding>)anObject forKey:(NSString*)key {
  277. [self setObject:anObject forKey:key withTimeoutInterval:self.defaultTimeoutInterval];
  278. }
  279. - (void)setObject:(id<NSCoding>)anObject forKey:(NSString*)key withTimeoutInterval:(NSTimeInterval)timeoutInterval {
  280. [self setData:[NSKeyedArchiver archivedDataWithRootObject:anObject] forKey:key withTimeoutInterval:timeoutInterval];
  281. }
  282. @end