最近突發奇想,想對比下幾個不同Cache框架的實現,于是就從項目中在用的PINCache著手分析。PINCache是Pinterest的程序員在Tumblr的TMCache基礎上發展而來的,主要的改進是修復了dealock的bug,TMCache已經不再維護了,而PINCache最新版本是v2.2。
PINCache從對象上來劃分:
PINCache只是PINDiskCache+PINMemoryCache的封裝,具體的操作包括:get,set,remove,trim,都是通過這兩個內部對象來完成。
1.PINCache的實現方式
采用Disk(文件) + Memory(其實就是NSDictionary)的雙存儲方式,在cache數據的管理上,都是采用鍵值對的方式進行管理,其中Disk文件的存儲路徑形式為:APP/Library/Caches/com.pinterest.PINDiskCache.(name),Memory內存對象的存儲為鍵值存儲。在執行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這種高頻率的操作,反而會降低性能,所以ibireme 實現的YYCache對MemoryCache的同步機制選用OSSpinLock,而不是dispatch_semaphore,當然OSSpinLock和dispatch_semaphore正好相反,當條件不滿足時會輪詢,導致CPU占用率升高。
PINCache實現了同步和異步兩套操作Cache的API
同步方式阻塞訪問線程,直到操作成功:
- (__nullable id)objectForKey:(NSString *)key;
- (void)setObject:(id)object forKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key;
異步方式具體操作在并發隊列上完成后會根據傳入的block把結果返回出來:
- (void)objectForKey:(NSString *)key block:(PINCacheObjectBlock)block;
- (void)setObject:(id)object forKey:(NSString *)key block:(nullable PINCacheObjectBlock)block;
- (void)removeObjectForKey:(NSString *)key block:(nullable PINCacheObjectBlock)block;
2.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:
@property (copy) PINDiskCacheObjectBlock __nullable willAddObjectBlock;
@property (copy) PINDiskCacheObjectBlock __nullable didAddObjectBlock;
@property (copy) PINDiskCacheObjectBlock __nullable willRemoveObjectBlock;
@property (copy) PINDiskCacheObjectBlock __nullable didRemoveObjectBlock;
對應PINCache的同步異步兩套API,PINDiskCache也有兩套實現,不同之處在于同步操作會在函數開始加鎖,函數結尾釋放鎖,而異步操作只在對關鍵數據操作時才加鎖,執行完后立即釋放,這樣在一個函數內部可能要完成多次加鎖解鎖的操作,這樣提高了PINCache的并發操作效率,但對性能也是一個考驗。
3.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
4.操作安全性
(1)PINDiskCache的同步API
- (void)setObject:(id)object forKey:(NSString *)key fileURL:(NSURL **)outFileURL {
...
[self lock];
//1.archive對象
//2.修改對象的訪問日期
//3.更新PINDiskCache成員變量
[self unlock];
}
整個操作都是在lock狀態下完成的,保證了對disk文件操作的互斥
其他的objectForKey,removeObjectForKey操作也是這種實現方式。
(1)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];
? ? ? ? ? ? block(strongSelf, key, object, fileURL);
? ? ? ? ? ? [strongSelf unlock];
? ? ? ? }
? ? });
}
(3)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進行業務層的封裝時,要保證更新操作的串行化,避免并行更新操作的情況。
參考:Apple線程安全總結