【原】iOS學習之PINCache第三方緩存框架

  在項目中總是需要緩存一些網絡請求數據以減輕服務器壓力,業內也有許多優秀的開源的解決方案。通常的緩存方案都是由內存緩存和磁盤緩存組成的,內存緩存速度快容量小,磁盤緩存容量大速度慢可持久化。

1、PINCache概述

  PINCache 是 Pinterest 的程序員在 Tumblr 的 TMCache 基礎上發展而來的,主要的改進是修復了 dealock 的bug,TMCache 已經不再維護了,而 PINCache 最新版本是v3.0.1。

  PINCache是多線程安全的,使用鍵值對來保存數據。PINCache內部包含了2個類似的對象屬性,一個是內存緩存 PINMemoryCache,另一個是磁盤緩存 PINDiskCache,具體的操作包括:getsetremovetrim,都是通過這兩個內部對象來完成。

  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 <NSCoding>)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
 <concurrentQueue>.
 
 @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 <concurrentQueue>.
 
 @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 <concurrentQueue>.
 
 @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 <NSCoding>)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 <concurrentQueue>.
 
 @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 <concurrentQueue>.
 
 @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 <concurrentQueue>.
 
 @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 <removeAllObjects:>.
 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 <removeAllObjects:>.
 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進行業務層的封裝時,要保證更新操作的串行化,避免並行更新操作的情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章