TMCache源碼分析(一)---TMMemoryCache內存緩存

原文在這裏git

緩存是咱們移動端開發必不可少的功能, 目前說起的緩存按照存儲形式來分主要分爲:github

  • 內存緩存: 快速, 讀寫數據量小
  • 磁盤緩存: 慢速, 讀寫數據量大(慢速是相對於內存緩存而言)

那緩存的目的是什麼呢? 大概分爲如下幾點:編程

  • 複用數據,避免重複計算.
  • 緩解服務端壓力.
  • 提升用戶體驗,好比離線瀏覽, 節省流量等等.

簡言之,緩存的目的就是:數組

以空間換時間.緩存

目前 gitHub 上開源了不少緩存框架, 著名的 TMCache, PINCache, YYCache等, 接下來我會逐一分析他們的源碼實現, 對比它們的優缺點.安全

TMCache, PINCache, YYCache基本框架結構都相同, 接口 API 相似, 因此只要會使用其中一個框架, 另外兩個上手起來很是容易, 可是三個框架的內部實現原理略有不一樣.bash

TMMemoryCache

TMMemoryCacheTMCache 框架中針對內存緩存的實現, 在系統 NSCache 緩存的基礎上增長了不少方法和屬性, 好比數量限制、內存總容量限制、緩存存活時間限制、內存警告或應用退到後臺時清空緩存等功能. 而且TMMemoryCache可以同步和異步的對內存數據進行操做,最重要的一點是TMMemoryCache是線程安全的, 可以確保在多線程狀況下數據的安全性.多線程

首先來看一下 TMMemoryCache 提供什麼功能, 按照功能來分析它的實現原理:併發

  1. 同步/異步的存儲對象到內存中.
  2. 同步/異步的從內存中獲取對象.
  3. 同步/異步的從內存中刪除指定 key 的對象,或者所有對象.
  4. 增長/刪除數據, 內存警告, 退回後臺的異步回調事件.
  5. 設置內存緩存使用上限.
  6. 設置內存緩存過時時間.
  7. 內存警告或退到後臺清空緩存.
  8. 根據時間或緩存大小來清空指定時間段或緩存範圍的數據.

同步/異步的存儲對象到內存中

相關 API:框架

// 同步
- (void)setObject:(id)object forKey:(NSString *)key;
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost;

// 異步
- (void)setObject:(id)object forKey:(NSString *)key block:(TMMemoryCacheObjectBlock)block;
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost block:(TMMemoryCacheObjectBlock)block;
複製代碼

異步存儲

首先看一下異步存儲對象, 由於同步存儲裏面會調用異步存儲操做, 採用 dispatch_semaphore 信號量的方式強制把異步操做轉換成同步操做.
內存緩存的核心是建立字典把須要存儲的對象按照 key, value的形式存進字典中, 這是一條主線, 而後在主線上分發出許多分支, 好比:緩存時間, 緩存大小, 線程安全等, 都是圍繞着這條主線來的. TMMemoryCache 也不例外, 在調用+ (instancetype)sharedCache方法建立並初始化的時候會建立三個可變字典_dictionary, _dates, _costs,這三個字典分別保存三種鍵值對:

- Key value
_dictionary 存儲對象的 key 存儲對象的值
_dates 存儲對象的 key 存儲對象時的時間
_costs 存儲對象的 key 存儲對象所佔內存大小

實現數據存儲的核心方法:

- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost block:(TMMemoryCacheObjectBlock)block {
    NSDate *now = [[NSDate alloc] init];

    if (!key || !object)
        return;

    __weak TMMemoryCache *weakSelf = self;

    // 0.競態條件下, 在併發隊列中保護寫操做
    dispatch_barrier_async(_queue, ^{
        TMMemoryCache *strongSelf = weakSelf;
        if (!strongSelf)
            return;

        // 1.調用 will add block
        if (strongSelf->_willAddObjectBlock)
            strongSelf->_willAddObjectBlock(strongSelf, key, object);

        // 2.存儲 key 對應的數據,時間,緩存大小到相應的字典中
        [strongSelf->_dictionary setObject:object forKey:key];
        [strongSelf->_dates setObject:now forKey:key];
        [strongSelf->_costs setObject:@(cost) forKey:key];

        _totalCost += cost;

        // 3.調用 did add block
        if (strongSelf->_didAddObjectBlock)
            strongSelf->_didAddObjectBlock(strongSelf, key, object);

        // 4.根據時間排序來清空指定緩存大小的內存
        if (strongSelf->_costLimit > 0)
            [strongSelf trimToCostByDate:strongSelf->_costLimit block:nil];

        // 5.異步回調
        if (block) {
            __weak TMMemoryCache *weakSelf = strongSelf;
            dispatch_async(strongSelf->_queue, ^{
                TMMemoryCache *strongSelf = weakSelf;
                if (strongSelf)
                    block(strongSelf, key, object);
            });
        }
    });
}
複製代碼

在上面的代碼中我標出了核心存儲方法作了幾件事, 其中最爲核心的是保證線程安全的dispatch_barrier_async方法, 在 GCD 中稱之爲柵欄方法, 通常跟併發隊列一塊兒用, 在多線程中對同一資源的競爭條件下保護共享資源, 確保在同一時間片斷只有一個線程資源, 這是不擴展講 GCD 的相關知識.

dispatch_barrier_async 方法通常都是跟併發隊列搭配使用,下面的圖解很清晰(侵刪), 在併發隊列中有不少任務(block), 這些block都是按照 FIFO 的順序執行的, 當要執行用 dispatch_barrier_async 方法提交到併發隊列queue的 block 的時候, 該併發隊列暫時會'卡住', 等待以前的任務 block 執行完畢, 再執行dispatch_barrier_async 提交的 block, 在此 block 以後提交到併發隊列queue的 block 不會被執行,會一直等待 dispatch_barrier_async block 執行完畢後纔開始併發執行, 咱們能夠看出, 在併發隊列遇到 dispatch_barrier_async block 時就處於一直串行隊列狀態, 等待執行完畢後又開始併發執行.
因爲TMMemoryCache中全部的讀寫操做都是在一個 concurrent queue(併發隊列)中, 因此使用 dispatch_barrier_async 可以保證寫操做的線程安全, 在同一時間只有一個寫任務在執行, 其它讀寫操做都處於等待狀態, 這是 TMMemoryCache 保證線程安全的核心, 但也是它最大的毛病, 容易形成性能降低和死鎖.

Barrier_on_queue

從上面代碼中能夠看出, 在該方法中把須要存儲的數據按照 key-value 的形式存儲進了_dictionary字典中, 其它操做無非就是增長功能的配料,後面會抽絲剝繭的捋清楚, 到此處咱們的任務完成, 知道是怎麼存儲數據的, 很是簡單:

  1. 使用 GCD 的 dispatch_barrier_async 方法保證寫操做線程安全.
  2. 把須要存儲的數據存進可變字典中.

同步存儲

根據上文所說, 同步存儲中會調用異步存儲操做, 來看一下代碼:

- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost {
    if (!object || !key)
        return;

    // 1.建立信號量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    // 2.異步存數據
    [self setObject:object forKey:key withCost:cost block:^(TMMemoryCache *cache, NSString *key, id object) {
        
        // 3.異步存儲完畢發送 signal 信號
        dispatch_semaphore_signal(semaphore);
    }];

    // 4.等待異步存儲完畢
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

}
複製代碼

從上面代碼能夠看出,同步的存儲數據使用了 GCD 的 dispatch_semaphore_t 信號量, 這是一個很是古老又複雜的線程概念, 有興趣的話能夠看看 <<UNIX 環境高級編程>> 這本經典之做, 由於它的複雜是創建在操做系統的複雜性上的.可是這並不影響咱們使用 dispatch_semaphore_t 信號量. 怎麼使用 GCD 的信號量以及原理下面大概描述一下:

信號量在競態條件下可以保證線程安全,在建立信號量 dispatch_semaphore_create 的時候設置信號量的值, 這個值表示容許多少個線程可同時訪問公共資源, 就比如咱們的車位同樣, 線程就是咱們的車子,這個信號量就是停車場的管理者, 他知道何時有多少車位, 是否是該把車子放進停車場, 當沒有車位或者車位不足時, 這個管理員就會把司機卡在停車場外不許進, 那麼被攔住的司機按照 FIFO 的隊列排着隊, 有足夠位置的時候,管理員就方法閘門, 大吼一聲: 孩子們去吧. 那麼確定有司機等不耐煩了, 就想着等十分鐘沒有車位就不等了,就能夠在 dispatch_semaphore_wait 方法中設置等待時間, 等待超過設置時間就不等待.
那麼把上面的場景應用在 dispatch_semaphore_create 信號量中就很容易理解了, 建立信號量並設置最大併發線程數, dispatch_semaphore_wait 設置等待時間,在等待時間未到達或者信號量值沒有達到初始值時會一直等待, 調用 dispatch_semaphore_wait 方法會使信號量的值+1, 表示增長一個線程等待處理共用資源, 當 dispatch_semaphore_signal 時會使信號量的值-1, 表示該線程再也不佔用共用資源.

根據上面對 dispatch_semaphore_t 信號量的描述可知, 信號量的初始值爲0,當前線程執行 dispatch_semaphore_wait 方法就會一直等待, 此時就至關於同步操做, 當在併發隊列中異步存儲完數據調用dispatch_semaphore_signal 方法, 此時信號量的值變成0,跟初始值同樣,當前線程當即結束等待, 同步設置方法執行完畢.

其實同步實現存儲數據的方式不少, 主要就是要串行執行寫操做採用 dispatch_sync的方式, 可是基於 TMMemoryCache 全部的操做都是在併發隊列上的, 因此才採用信號量的方式.

其實只要知道dispatch_barrier_async, dispatch_semaphore_t 的用法,後面的均可以不用看了, 本身去找源碼看看就明白了.


休息一下吧,後面的簡單了


同步/異步的從內存中獲取對象.

有了上面的同步/異步存儲的理論, 那麼同步/異步獲取對象簡直易如反掌, 不就是從_dictionary字典中根據 key 取出對應的 value 值, 在取的過程當中加以線程安全, will/did 之類輔助處理的 block 操做.

異步獲取數據

- (void)objectForKey:(NSString *)key block:(TMMemoryCacheObjectBlock)block {
    NSDate *now = [[NSDate alloc] init];
    
    if (!key || !block)
        return;

    __weak TMMemoryCache *weakSelf = self;

    // 1.異步加載存儲數據
    dispatch_async(_queue, ^{
        TMMemoryCache *strongSelf = weakSelf;
        if (!strongSelf)
            return;
        
        // 2.根據 key 找到value
        id object = [strongSelf->_dictionary objectForKey:key];

        if (object) {
            __weak TMMemoryCache *weakSelf = strongSelf;
            // 3.也用柵欄保護寫操做, 保證在寫的時候沒有線程在訪問共享資源
            dispatch_barrier_async(strongSelf->_queue, ^{
                TMMemoryCache *strongSelf = weakSelf;
                if (strongSelf)
                    // 4.更新數據的最後操做時間(當前時間)
                    [strongSelf->_dates setObject:now forKey:key];
            });
        }

        // 5.回調
        block(strongSelf, key, object);
    });
}
複製代碼

根據代碼中註釋可知,除了拿到 key 值對應的 value, 還更新了此數據最後操做時間, 這有什麼用呢? 實際上是爲了記錄數據最後的操做時間, 後面會根據這個最後操做時間來刪除數據等一系列根據時間排序的操做.最後一步是回調, 咱們能夠看到, TMMemoryCache全部的讀寫和回調操做都放在同一個併發隊列中,這就爲之後性能降低和死鎖埋下伏筆.

同步獲取數據

- (id)objectForKey:(NSString *)key {
    if (!key)
        return nil;

    __block id objectForKey = nil;

    // 採用信號量強制轉化成同步操做
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    [self objectForKey:key block:^(TMMemoryCache *cache, NSString *key, id object) {
        objectForKey = object;
        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return objectForKey;
}
複製代碼

同步獲取數據也是經過 dispatch_semaphore_t 信號量的方式,把異步獲取數據的操做強制轉成同步獲取, 跟同步存儲數據的原理相同.

同步/異步的從內存中刪除指定 key 的對象,或者所有對象.

刪除操做也不例外:

- (void)removeObjectForKey:(NSString *)key block:(TMMemoryCacheObjectBlock)block {
    if (!key)
        return;

    __weak TMMemoryCache *weakSelf = self;

    // 1."柵欄"方法,保證線程安全
    dispatch_barrier_async(_queue, ^{
        TMMemoryCache *strongSelf = weakSelf;
        if (!strongSelf)
            return;

        // 2.根據 key 刪除 value
        [strongSelf removeObjectAndExecuteBlocksForKey:key];

        if (block) {
            __weak TMMemoryCache *weakSelf = strongSelf;
            
            // 3.完成後回調
            dispatch_async(strongSelf->_queue, ^{
                TMMemoryCache *strongSelf = weakSelf;
                if (strongSelf)
                    block(strongSelf, key, nil);
            });
        }
    });
}

// private API
- (void)removeObjectAndExecuteBlocksForKey:(NSString *)key {
    id object = [_dictionary objectForKey:key];
    NSNumber *cost = [_costs objectForKey:key];

    if (_willRemoveObjectBlock)
        _willRemoveObjectBlock(self, key, object);

    if (cost)
        _totalCost -= [cost unsignedIntegerValue];

    // 刪除全部跟此數據相關的緩存: value, date, cost
    [_dictionary removeObjectForKey:key];
    [_dates removeObjectForKey:key];
    [_costs removeObjectForKey:key];

    if (_didRemoveObjectBlock)
        _didRemoveObjectBlock(self, key, nil);
}
複製代碼

須要注意的是 - (void)removeObjectAndExecuteBlocksForKey 是共用私有方法, 刪除跟 key 相關的全部緩存, 後面的刪除操做還會用到此方法.

設置內存緩存使用上限

TMMemoryCache 提供costLimit屬性來設置內存緩存使用上限, 這個也是 NSCache 不具有的功能,來看一下跟此屬性相關的方法以及實現,代碼中有詳細解釋:

// getter
- (NSUInteger)costLimit {
    __block NSUInteger costLimit = 0;

    // 要想經過函數返回值傳遞回去,那麼必須同步執行,因此使用dispatch_sync同步獲取內存使用上限
    dispatch_sync(_queue, ^{
        costLimit = _costLimit;
    });

    return costLimit;
}

// setter
- (void)setCostLimit:(NSUInteger)costLimit {
    __weak TMMemoryCache *weakSelf = self;

    // "柵欄"方法保護寫操做
    dispatch_barrier_async(_queue, ^{
        TMMemoryCache *strongSelf = weakSelf;
        if (!strongSelf)
            return;

        // 設置內存上限
        strongSelf->_costLimit = costLimit;

        if (costLimit > 0)
            // 根據時間排序來削減內存緩存,以達到設置的內存緩存上限的目的
            [strongSelf trimToCostLimitByDate:costLimit];
    });
}

- (void)trimToCostLimitByDate:(NSUInteger)limit {
    if (_totalCost <= limit)
        return;

    // 按照時間的升序來排列 key
    NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)];

    // oldest objects first
    for (NSString *key in keysSortedByDate) {
        [self removeObjectAndExecuteBlocksForKey:key];

        if (_totalCost <= limit)
            break;
    }
}
複製代碼

- (void)trimToCostLimitByDate:(NSUInteger)limit 方法的做用:

  1. 若是目前已使用的內存大小小於須要設置的內存上線,則不刪除數據,不然刪除'最老'的數據,讓已使用的內存大小不超過設置的內存上限.
  2. 按照存儲的數據最近操做的最後時間進行升序排序,即最近操做的數據對應的 key 排最後.
  3. 若是已經超過內存上限, 則根據 key 值刪除數據, 先刪除操做時間較早的數據.

從這裏就會恍然大悟, 以前設置的 _date 數組終於派上用場了,若是須要刪除數據則按照時間的前後順序來刪除,也算是一種優先級策略吧.

設置內存緩存過時時間

TMMemoryCache 提供ageLimit屬性來設置緩存過時時間,根據上面costLimit屬性能夠猜測一下ageLimit是怎麼實現的,既然是要設置緩存過時時間, 那麼我設置緩存過時時間 ageLimit = 10 10秒鐘,說明距離當前時間以前的10秒的數據已通過期, 須要刪除掉; 再過10秒又要當前時間刪除以前10秒存的數據,咱們知道刪除只須要找到 key 就行,因此就必須經過_date字典找到過時的 key, 再刪除數據.由此可知須要一個定時器,每過10秒刪除一次,完成一個定時任務. 上面只是咱們的猜測,來看看代碼是否是這麼實現的呢?咱們只需看核心的操做方法

- (void)trimToAgeLimitRecursively {
    if (_ageLimit == 0.0)
        return;

    // 說明距離如今 ageLimit 秒的緩存應該被清除掉了
    NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:-_ageLimit];
    [self trimMemoryToDate:date];
    
    __weak TMMemoryCache *weakSelf = self;
    
    // 延遲 ageLimit 秒, 又異步的清除緩存
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_ageLimit * NSEC_PER_SEC));
    dispatch_after(time, _queue, ^(void){
        TMMemoryCache *strongSelf = weakSelf;
        if (!strongSelf)
            return;
        
        __weak TMMemoryCache *weakSelf = strongSelf;
        
        dispatch_barrier_async(strongSelf->_queue, ^{
            TMMemoryCache *strongSelf = weakSelf;
            [strongSelf trimToAgeLimitRecursively];
        });
    });
}
複製代碼

上面的代碼驗證了咱們的猜測,可是在不斷的建立定時器,不斷的在並行隊列中使用dispatch_barrier_async柵欄方法提交遞歸 block, 天啦嚕...若是設置的 ageLimit 很小,可想而知性能消耗會很是大!

內存警告或退到後臺清空緩存

內存警告和退到後臺須要監聽系統通知,UIApplicationDidReceiveMemoryWarningNotificationUIApplicationDidEnterBackgroundNotification, 而後執行清除操做方法removeAllObjects,只不過在相應的位置執行對應的 will/did 之類的 block 操做.

根據時間或緩存大小來清空指定時間段或緩存範圍的數據

這兩類方法主要是爲了更加靈活的使用 TMMemoryCache,指定一個時間或者內存大小,會自動刪除時間點以前和大於指定內存大小的數據. 相關 API:

// 清空 date 以前的數據
- (void)trimToDate:(NSDate *)date block:(TMMemoryCacheBlock)block;
// 清空數據,讓已使用內存大小爲cost 
- (void)trimToCost:(NSUInteger)cost block:(TMMemoryCacheBlock)block;
複製代碼

刪除指定時間點有兩點注意:

  • 若是指定的時間點爲 [NSDate distantPast] 表示最先能表示的時間,說明清空所有數據.
  • 若是不是最先時間,把_date中的 key 按照升序排序,再遍歷排序後的 key 數組,判斷跟指定時間的關係,若是比指定時間更早則刪除, 即刪除指定時間節點以前的數據.
- (void)trimMemoryToDate:(NSDate *)trimDate {
    // 字典中存放的順序不是按照順序存放的, 因此按照必定格式排序, 根據 value 升序的排 key 值順序, 也就是說根據時間的升序來排 key, 數組中第一個值是最先的時間的值.
    NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)];
    
    for (NSString *key in keysSortedByDate) { // oldest objects first
        NSDate *accessDate = [_dates objectForKey:key];
        if (!accessDate)
            continue;
        
        // 找出每一個時間的而後跟要刪除的時間點進行比較, 若是比刪除時間早則刪除
        if ([accessDate compare:trimDate] == NSOrderedAscending) { // older than trim date
            [self removeObjectAndExecuteBlocksForKey:key];
        } else {
            break;
        }
    }
}
複製代碼

總結

內存緩存是很簡單的, 核心就是 key-value 的形式存儲數據進字典,再輔助設置內存上限,緩存時間,各種 will/did block 操做, 最重要的是要實現線程安全.

歡迎你們斧正!

相關文章
相關標籤/搜索