在项目中总是需要缓存一些网络请求数据以减轻服务器压力,业内也有许多优秀的开源的解决方案。通常的缓存方案都是由内存缓存和磁盘缓存组成的,内存缓存速度快容量小,磁盘缓存容量大速度慢可持久化。
1、PINCache概述
PINCache 是 Pinterest 的程序员在 Tumblr 的 TMCache 基础上发展而来的,主要的改进是修复了 dealock 的bug,TMCache 已经不再维护了,而 PINCache 最新版本是v3.0.1。
PINCache是多线程安全的,使用键值对来保存数据。PINCache内部包含了2个类似的对象属性,一个是内存缓存 PINMemoryCache,另一个是磁盘缓存 PINDiskCache,具体的操作包括:get,set,remove,trim,都是通过这两个内部对象来完成。
PINCache本身并没有过多的做处理缓存的具体工作,而是全部交给它内部的2个对象属性来实现,它只是对外提供了一些同步或者异步接口。在iOS中,当App收到内存警告或者进入后台的时候,PINCache能够清理掉所有的内存缓存。
2、PINCache的实现方式
原理
采用 PINCache 项目的 Demo 来说明,PINCache 是从服务器加载数据,再缓存下来,继而做业务逻辑处理,如果下次还需要同样的数据,要是缓存里面还有这个数据的话,那么就不需要再次发起网络请求了,而是直接使用这个数据。
PINCache 采用 Disk(文件) + Memory(其实就是NSDictionary) 的双存储方式,在cache数据的管理上,都是采用键值对的方式进行管理,其中 Disk 文件的存储路径形式为:APP/Library/Caches/com.pinterest.PINDiskCache.(name),Memory 内存对象的存储为键值存储。
PINCache 除了可以按键取值、按键存值、按键删值之外,还可以移除某个日期之前的缓存数据、删除所有缓存、限制缓存大小等。在执行 set 操作的同时会记录文件/对象的更新date 和 成本cost,对于 date 和 cost 两个属性,有对应的API允许开发者按照 date 和 cost 清除 PINCache 管理的文件和内存,如清除某个日期之前的cache数据,清除cost大于X的cache数据等。
在Cache的操作实现上,PINCache采用dispatch_queue+dispatch_semaphore 的方式,dispatch_queue 是并发队列,为了保证线程安全采用 dispatch_semaphore 作锁,从bireme的这篇文章中了解到,dispatch_semaphore 的优势在于不会轮询状态的改变,适用于低频率的Disk操作,而像Memory这种高频率的操作,反而会降低性能。
同步操作Cache
同步方式阻塞访问线程,直到操作成功:
/// @name Synchronous Methods
/** This method determines whether an object is present for the given key in the cache.
@see containsObjectForKey:block:
@param key The key associated with the object.
@result YES if an object is present for the given key in the cache, otherwise NO.*/
- (BOOL)containsObjectForKey:(NSString *)key;
/** Retrieves the object for the specified key. This method blocks the calling thread until the object is available. Uses a lock to achieve synchronicity on the disk cache.
@see objectForKey:block:
@param key The key associated with the object.
@result The object for the specified key. */
- (__nullable id)objectForKey:(NSString *)key;
/** Stores an object in the cache for the specified key. This method blocks the calling thread until the object has been set. Uses a lock to achieve synchronicity on the disk cache.
@see setObject:forKey:block:
@param object An object to store in the cache.
@param key A key to associate with the object. This string will be copied. */
- (void)setObject:(id)object forKey:(NSString *)key;
/** Removes the object for the specified key. This method blocks the calling thread until the object has been removed. Uses a lock to achieve synchronicity on the disk cache.
@param key The key associated with the object to be removed. */
- (void)removeObjectForKey:(NSString *)key;
/** Removes all objects from the cache that have not been used since the specified date. This method blocks the calling thread until the cache has been trimmed. Uses a lock to achieve synchronicity on the disk cache.
@param date Objects that haven't been accessed since this date are removed from the cache.*/
- (void)trimToDate:(NSDate *)date;
/** Removes all objects from the cache. This method blocks the calling thread until the cache has been cleared. Uses a lock to achieve synchronicity on the disk cache. */
- (void)removeAllObjects;
异步操作Cache
异步方式具体操作在并发队列上完成后会根据传入的block把结果返回出来:
/// @name Asynchronous Methods
/** This method determines whether an object is present for the given key in the cache. This method returns immediately and executes the passed block after the object is available, potentially in parallel with other blocks on the.
@see containsObjectForKey:
@param key The key associated with the object.
@param block A block to be executed concurrently after the containment check happened */
- (void)containsObjectForKey:(NSString *)key block:(PINCacheObjectContainmentBlock)block;
/** Retrieves the object for the specified key. This method returns immediately and executes the passed block after the object is available, potentially in parallel with other blocks on the.
@param key The key associated with the requested object.
@param block A block to be executed concurrently when the object is available. */
- (void)objectForKey:(NSString *)key block:(PINCacheObjectBlock)block;
/** Stores an object in the cache for the specified key. This method returns immediately and executes the passed block after the object has been stored, potentially in parallel with other blocks on the.
@param object An object to store in the cache.
@param key A key to associate with the object. This string will be copied.
@param block A block to be executed concurrently after the object has been stored, or nil. */
- (void)setObject:(id)object forKey:(NSString *)key block:(nullable PINCacheObjectBlock)block;
/** Removes the object for the specified key. This method returns immediately and executes the passed block after the object has been removed, potentially in parallel with other blocks on the.
@param key The key associated with the object to be removed.
@param block A block to be executed concurrently after the object has been removed, or nil. */
- (void)removeObjectForKey:(NSString *)key block:(nullable PINCacheObjectBlock)block;
/** Removes all objects from the cache that have not been used since the specified date. This method returns immediately and executes the passed block after the cache has been trimmed, potentially in parallel with other blocks on the.
@param date Objects that haven't been accessed since this date are removed from the cache.
@param block A block to be executed concurrently after the cache has been trimmed, or nil. */
- (void)trimToDate:(NSDate *)date block:(nullable PINCacheBlock)block;
/** Removes all objects from the cache.This method returns immediately and executes the passed block after the cache has been cleared, potentially in parallel with other blocks on the.
@param block A block to be executed concurrently after the cache has been cleared, or nil. */
- (void)removeAllObjects:(nullable PINCacheBlock)block;
3、PINDiskCache
DiskCache有以下属性:
@property (readonly) NSString *name; //指定的cache名称,如MyPINCacheName,在Library/Caches/目录下
@property (readonly) NSURL *cacheURL; //cache目录URL,如Library/Caches/com.pinterest.PINDiskCache.MyPINCacheName,这个才是真实的存储路径
@property (readonly) NSUInteger byteCount; //disk存储的文件大小
@property (assign) NSUInteger byteLimit; //disk上允许存储的最大字节
@property (assign) NSTimeInterval ageLimit; //存储文件的最大生命周期
@property (nonatomic, assign, getter=isTTLCache) BOOL ttlCache; //TTL强制存储,如果为YES,访问操作不会延长该cache对象的生命周期,如果试图访问一个生命超出self.ageLimit的cache对象时,会当做该对象不存在。
为了遵循Cocoa的设计哲学,PINCache还允许用户自定义block用以监听add,remove操作事件,不是KVO,却似KVO:
/// @name Event Blocks
/** A block to be executed just before an object is added to the cache. The queue waits during execution. */
@property (copy) PINDiskCacheObjectBlock __nullable willAddObjectBlock;
/** A block to be executed just before an object is removed from the cache. The queue waits during execution. */
@property (copy) PINDiskCacheObjectBlock __nullable willRemoveObjectBlock;
/** A block to be executed just before all objects are removed from the cache as a result of. The queue waits during execution. */
@property (copy) PINDiskCacheBlock __nullable willRemoveAllObjectsBlock;
/** A block to be executed just after an object is added to the cache. The queue waits during execution. */
@property (copy) PINDiskCacheObjectBlock __nullable didAddObjectBlock;
/** A block to be executed just after an object is removed from the cache. The queue waits during execution. */
@property (copy) PINDiskCacheObjectBlock __nullable didRemoveObjectBlock;
/** A block to be executed just after all objects are removed from the cache as a result of.
The queue waits during execution.*/
@property (copy) PINDiskCacheBlock __nullable didRemoveAllObjectsBlock;
对应 PINCache 的同步异步两套API,PINDiskCache 也有两套实现,不同之处在于同步操作会在函数开始加锁,函数结尾释放锁,而异步操作只在对关键数据操作时才加锁,执行完后立即释放,这样在一个函数内部可能要完成多次加锁解锁的操作,这样提高了PINCache的并发操作效率,但对性能也是一个考验。
4、PINMemoryCache
PINMemoryCache的属性:
@property (readonly) NSUInteger totalCost; //开销总数
@property (assign) NSUInteger costLimit; //允许的内存最大开销
@property (assign) NSTimeInterval ageLimit; //same as PINDiskCache
@property (nonatomic, assign, getter=isTTLCache) BOOL ttlCache; //same as PINDiskCache
@property (assign) BOOL removeAllObjectsOnMemoryWarning; //内存警告时是否清除memory cache
@property (assign) BOOL removeAllObjectsOnEnteringBackground; //App进入后台时是否清除memory cache
5、操作安全性
PINDiskCache的同步API
- (void)setObject:(id)object forKey:(NSString *)key fileURL:(NSURL **)outFileURL {
...
[self lock];
//1.将对象 archive,存入 fileURL 中
//2.修改对象的访问日期为当前的日期
//3.更新PINDiskCache成员变量
[self unlock];
}
整个操作都是在lock状态下完成的,保证了对disk文件操作的互斥
其他的objectForKey,removeObjectForKey操作也是这种实现方式。
PINDiskCache的异步API
- (void)setObject:(id)object forKey:(NSString *)key block:(PINDiskCacheObjectBlock)block {
__weak PINDiskCache *weakSelf = self;
dispatch_async(_asyncQueue, ^{//向并发队列加入一个task,该task同样是同步执行PINDiskCache的同步API
PINDiskCache *strongSelf = weakSelf;
[strongSelf setObject:object forKey:key fileURL:&fileURL];
if (block) {
[strongSelf lock];
NSURL *fileURL = nil;
block(strongSelf, key, object, fileURL);
[strongSelf unlock];
}});
}
PINMemoryCache的同步API
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost {
[self lock];
PINMemoryCacheObjectBlock willAddObjectBlock = _willAddObjectBlock;
PINMemoryCacheObjectBlock didAddObjectBlock = _didAddObjectBlock;
NSUInteger costLimit = _costLimit;
[self unlock];
if (willAddObjectBlock)
willAddObjectBlock(self, key, object);
[self lock];
_dictionary[key] = object;//更新key对应的object
_dates[key] = [[NSDate alloc] init];
_costs[key] = @(cost);
_totalCost += cost;
[self unlock];//释放lock,此时在并发队列上的别的操作如objectForKey可以获取同一个key对应的object,但是拿到的都是同一个对象
...
}
PINMemoryCache 的并发安全性依赖于 PINMemoryCache 维护了一个NSMutableDictionary,每一个 key-value 的 读取和设置 都是互斥的,即信号量保证了这个 NSMutableDictionary 的操作是线程安全的,其实Cocoa的容器类如NSArray,NSDictionary,NSSet都是线程安全的,而NSMutableArray,NSMutableDictionary则不是线程安全的,所以这里在对PINMemoryCache的NSMutableDictionary进行操作时需要加锁互斥。
那么假如从 PINMemoryCache 中根据一个 key 取到的是一个 mutable 的Collection对象,就会出现如下情况:
1)线程A和B都读到了一份value,NSMutableDictionary,它们是同一个对象
2)线程A对读出的NSMutableDictionary进行更新操作
3)线程B对读出的NSMutableDictionary进行更新操作
这就有可能导致执行出错,因为NSMutableDictionary不是线程安全的,所以在对PINCache进行业务层的封装时,要保证更新操作的串行化,避免并行更新操作的情况。