iOS PINCache學習

最近突發奇想,想對比下幾個不同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線程安全總結

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 在項目中總是需要緩存一些網絡請求數據以減輕服務器壓力,業內也有許多優秀的開源的解決方案。通常的緩存方案都是...
    墨隱于非閱讀 1,036評論 0 1
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,774評論 0 9
  • 技術無極限,從菜鳥開始,從源碼開始。 由于公司目前項目還是用OC寫的項目,沒有升級swift 所以暫時SDWebI...
    充滿活力的早晨閱讀 12,694評論 0 2
  • 第一篇第二篇大概是把下載圖片緩存圖片的這個邏輯走完了,里面涉及好多類。 羅列一下 UIView+WebCache ...
    充滿活力的早晨閱讀 769評論 0 1
  • 目 錄|雙生鎖 上一章|尋藥未果,反遭綁架 白色的空間里,丁雨涵望著機械呆滯的“爸爸”“媽媽”,內心翻涌出一陣酸...
    安曉暖閱讀 522評論 2 4