The iOS framework that grows only as fast as its documentation
NIInMemoryCache.m
1 //
2 // Copyright 2011-2014 NimbusKit
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16 
17 #import "NIInMemoryCache.h"
18 
19 #import "NIDebuggingTools.h"
20 #import "NIPreprocessorMacros.h"
21 
22 #import <UIKit/UIKit.h>
23 
24 #if !defined(__has_feature) || !__has_feature(objc_arc)
25 #error "Nimbus requires ARC support."
26 #endif
27 
28 @interface NIMemoryCache()
29 // Mapping from a name (usually a URL) to an internal object.
30 @property (nonatomic, strong) NSMutableDictionary* cacheMap;
31 // A linked list of least recently used cache objects. Most recently used is the tail.
32 @property (nonatomic, strong) NSMutableOrderedSet* lruCacheObjects;
33 @end
34 
40 @interface NIMemoryCacheInfo : NSObject
41 
45 @property (nonatomic, copy) NSString* name;
46 
50 @property (nonatomic, strong) id object;
51 
55 @property (nonatomic, strong) NSDate* expirationDate;
56 
65 @property (nonatomic, strong) NSDate* lastAccessTime;
66 
74 - (BOOL)hasExpired;
75 
76 @end
77 
78 @implementation NIMemoryCache
79 
80 - (void)dealloc {
81  [[NSNotificationCenter defaultCenter] removeObserver:self];
82 }
83 
84 - (id)init {
85  return [self initWithCapacity:0];
86 }
87 
88 - (id)initWithCapacity:(NSUInteger)capacity {
89  if ((self = [super init])) {
90  _cacheMap = [[NSMutableDictionary alloc] initWithCapacity:capacity];
91  _lruCacheObjects = [NSMutableOrderedSet orderedSet];
92 
93  // Automatically reduce memory usage when we get a memory warning.
94  [[NSNotificationCenter defaultCenter] addObserver:self
95  selector:@selector(reduceMemoryUsage)
96  name:UIApplicationDidReceiveMemoryWarningNotification
97  object:nil];
98  }
99  return self;
100 }
101 
102 - (NSString *)description {
103  return [NSString stringWithFormat:
104  @"<%@"
105  @" lruObjects: %@"
106  @" cache map: %@"
107  @">",
108  [super description],
109  self.lruCacheObjects,
110  self.cacheMap];
111 }
112 
113 #pragma mark - Internal
114 
115 - (void)updateAccessTimeForInfo:(NIMemoryCacheInfo *)info {
116  @synchronized(self) {
117  NIDASSERT(nil != info);
118  if (nil == info) {
119  return; // COV_NF_LINE
120  }
121  info.lastAccessTime = [NSDate date];
122 
123  [self.lruCacheObjects removeObject:info];
124  [self.lruCacheObjects addObject:info];
125  }
126 }
127 
128 - (NIMemoryCacheInfo *)cacheInfoForName:(NSString *)name {
129  NIMemoryCacheInfo* info;
130  @synchronized(self) {
131  info = self.cacheMap[name];
132  }
133  return info;
134 }
135 
136 - (void)setCacheInfo:(NIMemoryCacheInfo *)info forName:(NSString *)name {
137  @synchronized(self) {
138  NIDASSERT(nil != name);
139  if (nil == name) {
140  return;
141  }
142 
143  // Storing in the cache counts as an access of the object, so we update the access time.
144  [self updateAccessTimeForInfo:info];
145 
146  id previousObject = [self cacheInfoForName:name].object;
147  if ([self shouldSetObject:info.object withName:name previousObject:previousObject]) {
148  self.cacheMap[name] = info;
149  [self didSetObject:info.object withName:name];
150  }
151  }
152 }
153 
154 - (void)removeCacheInfoForName:(NSString *)name {
155  @synchronized(self) {
156  NIDASSERT(nil != name);
157  if (nil == name) {
158  return;
159  }
160 
161  NIMemoryCacheInfo* cacheInfo = [self cacheInfoForName:name];
162  [self willRemoveObject:cacheInfo.object withName:name];
163 
164  [self.lruCacheObjects removeObject:cacheInfo];
165  [self.cacheMap removeObjectForKey:name];
166  }
167 }
168 
169 #pragma mark - Subclassing
170 
171 // Deprecated method.
172 - (BOOL)willSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject {
173  return [self shouldSetObject:object withName:name previousObject:previousObject];
174 }
175 
176 - (BOOL)shouldSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject {
177  // Allow anything to be stored.
178  return YES;
179 }
180 
181 - (void)didSetObject:(id)object withName:(NSString *)name {
182  // No-op
183 }
184 
185 - (void)willRemoveObject:(id)object withName:(NSString *)name {
186  // No-op
187 }
188 
189 #pragma mark - Public
190 
191 - (void)storeObject:(id)object withName:(NSString *)name {
192  @synchronized(self) {
193  [self storeObject:object withName:name expiresAfter:nil];
194  }
195 }
196 
197 - (void)storeObject:(id)object withName:(NSString *)name expiresAfter:(NSDate *)expirationDate {
198  @synchronized(self) {
199  // Don't store nil objects in the cache.
200  if (nil == object) {
201  return;
202  }
203 
204  if (nil != expirationDate && [[NSDate date] timeIntervalSinceDate:expirationDate] >= 0) {
205  // The object being stored is already expired so remove the object from the cache altogether.
206  [self removeObjectWithName:name];
207 
208  // We're done here.
209  return;
210  }
211 
212  NIMemoryCacheInfo* info = [self cacheInfoForName:name];
213 
214  // Create a new cache entry.
215  if (nil == info) {
216  info = [[NIMemoryCacheInfo alloc] init];
217  info.name = name;
218  }
219 
220  // Store the object in the cache item.
221  info.object = object;
222 
223  // Override any existing expiration date.
224  info.expirationDate = expirationDate;
225 
226  // Commit the changes to the cache.
227  [self setCacheInfo:info forName:name];
228  }
229 }
230 
231 - (id)objectWithName:(NSString *)name {
232  @synchronized(self) {
233  NIMemoryCacheInfo* info = [self cacheInfoForName:name];
234 
235  id object = nil;
236 
237  if (nil != info) {
238  if ([info hasExpired]) {
239  [self removeObjectWithName:name];
240 
241  } else {
242  // Update the access time whenever we fetch an object from the cache.
243  [self updateAccessTimeForInfo:info];
244 
245  object = info.object;
246  }
247  }
248 
249  return object;
250  }
251 }
252 
253 - (BOOL)containsObjectWithName:(NSString *)name {
254  @synchronized(self) {
255  NIMemoryCacheInfo* info = [self cacheInfoForName:name];
256 
257  if ([info hasExpired]) {
258  [self removeObjectWithName:name];
259  return NO;
260  }
261 
262  return (nil != info);
263  }
264 }
265 
266 - (NSDate *)dateOfLastAccessWithName:(NSString *)name {
267  @synchronized(self) {
268  NIMemoryCacheInfo* info = [self cacheInfoForName:name];
269 
270  if ([info hasExpired]) {
271  [self removeObjectWithName:name];
272  return nil;
273  }
274 
275  return [info lastAccessTime];
276  }
277 }
278 
280  @synchronized(self) {
281  NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject];
282 
283  if ([info hasExpired]) {
284  [self removeObjectWithName:info.name];
285  return nil;
286  }
287 
288  return info.name;
289  }
290 }
291 
293  @synchronized(self) {
294  NIMemoryCacheInfo* info = [self.lruCacheObjects lastObject];
295 
296  if ([info hasExpired]) {
297  [self removeObjectWithName:info.name];
298  return nil;
299  }
300 
301  return info.name;
302  }
303 }
304 
305 - (void)removeObjectWithName:(NSString *)name {
306  @synchronized(self) {
307  [self removeCacheInfoForName:name];
308  }
309 }
310 
311 - (void)removeAllObjectsWithPrefix:(NSString *)prefix {
312  @synchronized(self) {
313  // Assertions fire if you try to modify the object you're iterating over, so we make a copy.
314  for (NSString* name in [self.cacheMap copy]) {
315  if ([name hasPrefix:prefix]) {
316  [self removeObjectWithName:name];
317  }
318  }
319  }
320 }
321 
323  @synchronized(self) {
324  [self.cacheMap removeAllObjects];
325  [self.lruCacheObjects removeAllObjects];
326  }
327 }
328 
330  @synchronized(self) {
331  // Assertions fire if you try to modify the object you're iterating over, so we make a copy.
332  for (id name in [self.cacheMap copy]) {
333  NIMemoryCacheInfo* info = [self cacheInfoForName:name];
334 
335  if ([info hasExpired]) {
336  [self removeCacheInfoForName:name];
337  }
338  }
339  }
340 }
341 
342 - (NSUInteger)count {
343  @synchronized(self) {
344  return self.cacheMap.count;
345  }
346 }
347 
348 @end
349 
350 @implementation NIMemoryCacheInfo
351 
352 - (BOOL)hasExpired {
353  return (nil != _expirationDate
354  && [[NSDate date] timeIntervalSinceDate:_expirationDate] >= 0);
355 }
356 
357 - (NSString *)description {
358  return [NSString stringWithFormat:
359  @"<%@"
360  @" name: %@"
361  @" object: %@"
362  @" expiration date: %@"
363  @" last access time: %@"
364  @">",
365  [super description],
366  self.name,
367  self.object,
368  self.expirationDate,
369  self.lastAccessTime];
370 }
371 
372 @end
373 
374 @interface NIImageMemoryCache()
375 @property (nonatomic, assign) unsigned long long numberOfPixels;
376 @end
377 
378 @implementation NIImageMemoryCache
379 
380 - (unsigned long long)numberOfPixelsUsedByImage:(UIImage *)image {
381  @synchronized(self) {
382  if (nil == image) {
383  return 0;
384  }
385 
386  return (unsigned long long)(image.size.width * image.size.height * [image scale] * [image scale]);
387  }
388 }
389 
390 - (void)removeAllObjects {
391  @synchronized(self) {
392  [super removeAllObjects];
393 
394  self.numberOfPixels = 0;
395  }
396 }
397 
398 - (void)reduceMemoryUsage {
399  @synchronized(self) {
400  // Remove all expired images first.
401  [super reduceMemoryUsage];
402 
403  if (self.maxNumberOfPixelsUnderStress > 0) {
404  // Remove the least recently used images by iterating over the linked list.
405  while (self.numberOfPixels > self.maxNumberOfPixelsUnderStress) {
406  NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject];
407  [self removeCacheInfoForName:info.name];
408  }
409  }
410  }
411 }
412 
413 - (BOOL)shouldSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject {
414  @synchronized(self) {
415  NIDASSERT(nil == object || [object isKindOfClass:[UIImage class]]);
416  if (![object isKindOfClass:[UIImage class]]) {
417  return NO;
418  }
419 
420  _numberOfPixels -= [self numberOfPixelsUsedByImage:previousObject];
421  _numberOfPixels += [self numberOfPixelsUsedByImage:object];
422 
423  return YES;
424  }
425 }
426 
427 - (void)didSetObject:(id)object withName:(NSString *)name {
428  @synchronized(self) {
429  // Reduce the cache size after the object has been set in case the cache size is smaller
430  // than the object that's being added and we need to remove this object right away.
431  if (self.maxNumberOfPixels > 0) {
432  // Remove least recently used images until we satisfy our memory constraints.
433  while (self.numberOfPixels > self.maxNumberOfPixels) {
434  NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject];
435  [self removeCacheInfoForName:info.name];
436  }
437  }
438  }
439 }
440 
441 - (void)willRemoveObject:(id)object withName:(NSString *)name {
442  @synchronized(self) {
443  NIDASSERT(nil == object || [object isKindOfClass:[UIImage class]]);
444  if (nil == object || ![object isKindOfClass:[UIImage class]]) {
445  return;
446  }
447 
448  self.numberOfPixels -= [self numberOfPixelsUsedByImage:object];
449  }
450 }
451 
452 @end
453 
void removeAllObjects()
Removes all objects from the cache, regardless of expiration dates.
An in-memory cache for storing objects with expiration support.
void reduceMemoryUsage()
Removes all expired objects from the cache.
void storeObject:withName:expiresAfter:(id object,[withName] NSString *name,[expiresAfter] NSDate *expirationDate)
Stores an object in the cache with an expiration date.
BOOL shouldSetObject:withName:previousObject:(id object,[withName] NSString *name,[previousObject] id previousObject)
An object is about to be stored in the cache.
An in-memory cache for storing images with caps on the total number of pixels.
unsigned long long numberOfPixels
Returns the total number of pixels being stored in the cache.
NSString * nameOfMostRecentlyUsedObject()
Retrieve the key with the most fresh access.
unsigned long long maxNumberOfPixelsUnderStress
The maximum number of pixels this cache may store after a call to reduceMemoryUsage.
void removeObjectWithName:(NSString *name)
Removes an object from the cache with the given name.
NSUInteger count()
Returns the number of objects currently in the cache.
unsigned long long maxNumberOfPixels
The maximum number of pixels this cache may ever store.
NSString * nameOfLeastRecentlyUsedObject()
Retrieve the name of the object that was least recently used.