緩存框架學(xué)習(xí)(一 MemoryCache)

做開發(fā)時,合理的利用緩存是非常重要的,一方面可以為用戶減少訪問流量,另一方面也能加快應(yīng)用的訪問速度, 這部分的緩存學(xué)習(xí)內(nèi)容是基于 PINCache的, PINCache項目是在Tumblr 宣布不在維護 TMCache 后箩帚,由 Pinterest 維護和改進的基于TMCache的一個內(nèi)存緩存剃盾,修復(fù)了TMCache存在的性能和死鎖問題狮惜,可以說是有了一個較大的提升与涡。

PINCache 是多線程安全的, 使用鍵值隊來保存數(shù)據(jù)。PINCache中包含兩個類魏蔗, 一個是PINMemoryCache負責內(nèi)存緩存砍的,一個是PINDiskCache負責磁盤緩存,PINCache屬于它們的上層封裝莺治,將具體的緩存操作交給它的兩個對象屬性(PINMemoryCache屬性廓鞠,PINDiskCache屬性)當App接收到內(nèi)存警告時,PINCache會清理掉所有的內(nèi)存緩存谣旁。關(guān)于緩存部分我想用三節(jié)來說床佳,分別對應(yīng)PINMemoryCache,PINDiskCache榄审, 最后通過PINCache總結(jié)整個流程砌们。我是將PINCache的源碼敲了一遍,基本都了解了,在這一遍下來也頗有心得浪感,于是決定寫著系列關(guān)于緩存的文章昔头,我想以后還會有關(guān)于多線程,網(wǎng)絡(luò)部分的吧影兽,學(xué)習(xí)框架揭斧,多學(xué)習(xí),多進步

我覺得從.m文件開始講起峻堰,因為這是整個框架的核心部分而.h是方法調(diào)用讹开。

先鋪一下需要了解的知識:

  1. 內(nèi)存緩存:一般使用字典來作為數(shù)據(jù)的緩存池,配合一個保存每個內(nèi)存緩存數(shù)據(jù)的緩存時間的字典捐名,一個保存每個內(nèi)存緩存數(shù)據(jù)的緩存容量的字典萧吠,一個保存內(nèi)存緩存總?cè)萘康淖兞俊τ谠鰟h改查操作桐筏,基本也都是圍繞著字典來的,需要重點注意的就是在這些個操作過程的多線程安全問題拇砰,還有同步和異步訪問方法梅忌,以及異步方法中的Block參數(shù)的循環(huán)引用問題。

  2. 線程安全:現(xiàn)在iPhone早已步入多核時代除破,多核就會產(chǎn)生并發(fā)操作牧氮,并發(fā)操作會遇到讀寫問題,比如去銀行取款瑰枫,取款時卡內(nèi)余額顯示1000踱葛,你決定取1000,當你進行取款操作的時候光坝,你的家人往你卡上打了2000尸诽,假設(shè)取款操作先結(jié)束那么保存卡內(nèi)余額的值會變成3000,如果存款操作先完成盯另,那么取完款之后卡內(nèi)余額變成了0性含, 所以會產(chǎn)生問題,這個時候我們就需要加鎖操作鸳惯,當執(zhí)行讀寫商蕴,寫寫操作不能同時進行,必須要加同步鎖芝发,確保線程安全绪商,同一時間只能有一條線程執(zhí)行相應(yīng)的操作。具體看框架中的代碼:

     // 代碼加鎖
     - (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost
     {
         [self lock];
             // 緩存數(shù)據(jù)
             _dictionary[key] = object;
         [self unlock];
     }
     
     // 代碼不加鎖
     - (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost
     {
         // 緩存數(shù)據(jù)
         _dictionary[key] = object;
     }
    

    因為函數(shù)體在內(nèi)存中是一片固定的內(nèi)存區(qū)域,任何時間可以被任意線程訪問,假設(shè)t1 線程 Thread1訪問, 需要保存的值為 object1, key1,此時 Thread2訪問值為 object2, key2,因為 Thread1未執(zhí)行完函數(shù),所以此時在函數(shù)內(nèi)就有四個參數(shù)值 key1, object1, key2, object2,然后同時執(zhí)行_dictionary[key] = object(_dictionary 為NSMutableDictionary不是線程安全)這條語句, 所以可能會出現(xiàn)_dictionary[key1] = object2 的問題; 如果進行加鎖操作,當 Thread1未執(zhí)行結(jié)束時, Thread2是無法執(zhí)行_dictionary[key] = object 這條語句的辅鲸。注意我們?nèi)粘i_發(fā)實在主線程中進行格郁,很少涉及多線程問題。

  3. 鎖:在PINCache中使用的是信號量來實現(xiàn)同步鎖,具體代碼如下:

     @property (strong, nonatomic) dispatch_semaphore_t lockSemaphore;
     
     - (void)lock
     {
         dispatch_semaphore_wait(_lockSemaphore, DISPATCH_TIME_FOREVER);
     }
     
     - (void)unlock
     {
         dispatch_semaphore_signal(_lockSemaphore);
     }
    

    我自己的代碼中用的是pthread理张,效率能比信號量加鎖稍微高一點點

  4. 緩存策略:有優(yōu)先刪除緩存最久赫蛇,最少使用的策略,也有優(yōu)先刪除雾叭,容量最大悟耘,最少使用的策略。

  5. 臨界區(qū): 當訪問一個公共資源時,而這些公共資源無法被多個線程同時訪問,當一條線程進入臨界區(qū)時, 其他線程必須等待,公用資源是互斥的

  6. 共享資源: 一個類中的屬性, 成員變量全局變量就是這個對象的共享資源, 無論有多少個線程訪問該對象, 訪問的屬性全局變量成員變量都是同一塊內(nèi)存區(qū)域, 不會因為線程不同創(chuàng)建不同的內(nèi)存區(qū)域. 所以對于多線程操作的問題要將共享區(qū)域的取值, 設(shè)置值操作加鎖

內(nèi)存緩存我們要用個字典來存放數(shù)據(jù)织狐,用個字典存放條數(shù)據(jù)的容量暂幼,用個字典來存放每條數(shù)據(jù)的最后的修改時間

/**
 *  緩存數(shù)據(jù), key可以為 URL, value 為網(wǎng)絡(luò)數(shù)據(jù)
 */
@property (nonatomic, strong) NSMutableDictionary *dictionary;
/**
 *  每個緩存數(shù)據(jù)的最后訪問時間
 */
@property (nonatomic, strong) NSMutableDictionary *dates;
/**
 *  記錄每個緩存的花費
 */
@property (nonatomic, strong) NSMutableDictionary *costs;

同樣我們還希望當通過GCD異步操作時為我們的緩存過程單獨有個線程名

#if OS_OBJECT_USE_OBJC  // iOS 6 之后 SDK 支持 GCD ARC, 不需要再 Dealloc 中 release
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
#else
@property (nonatomic, assign) dispatch_queue_t concurrentQueue;
#endif

鎖,鎖移迫,鎖重要的事說3遍

@implementation WNMemoryCache {
    pthread_mutex_t _lock;
}

#define Lock(_lock) (pthread_mutex_lock(&_lock))
#define Unlock(_lock) (pthread_mutex_unlock(&_lock))

初始化方法

- (instancetype)init {
    if (self = [super init]) {
        1.
        NSString *queueName = [NSString stringWithFormat:@"%@.%p",WannaMemoryCachePrefix,self];
        // 以指定的名稱, 創(chuàng)建并發(fā)隊列, 用于異步緩存數(shù)據(jù)
        _concurrentQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_CONCURRENT);
        
        2.
        _removeAllObjectOnMemoryWoring = YES;
        _removeAllObjectOnEnteringBackground = YES;
        
        3.
        _dictionary = [NSMutableDictionary dictionary];
        _dates = [NSMutableDictionary dictionary];
        _costs = [NSMutableDictionary dictionary];
        
        4. 
        _willAddObjectBlock = nil;
        _willRemoveObjectBlock = nil;
        _willRemoveAllObjectsBlock = nil;
        
        _didAddObjectBlock = nil;
        _didRemoveObjectBlock = nil;
        _didRemoveAllObjectsBlock = nil;
        
        _didReceiveMemoryWarningBlock = nil;
        _didEnterBackgroundBlock = nil;
        
        5. 
        _ageLimit = 0.0;
        _costLimit = 0;
        _totalCost = 0;

        6.
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_4_0 && !TARGET_OS_WATCH
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveEnterBackgroundNotification:)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveMemoryWarningNotification:)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];

#endif
    }
    return self;
}
  1. 指定并發(fā)隊列的名稱
  2. 默認進入后臺旺嬉,收到內(nèi)存警告時清理所有的內(nèi)存緩存這個時候需要監(jiān)聽內(nèi)存警告 name:UIApplicationDidReceiveMemoryWarningNotification和進入后臺 name:UIApplicationDidEnterBackgroundNotification的通知 第6步
  3. 將緩存數(shù)據(jù),時間厨埋,消耗的字典進行初始化邪媳,直接訪問屬性能夠避免通過self調(diào)用get方法時消息發(fā)送時間的花費
  4. 定義的回調(diào)函數(shù),并初始化為空
  5. _ageLimit 緩存存活時間, 如果設(shè)置為一個大于0的值, 就被開啟為 TTL 緩存(指定存活期的緩存),即如果 ageLimit > 0 => ttlCache = YES;
    _constLimit 內(nèi)存花費限制荡陷,_totalConst 總的內(nèi)存緩存消耗

這里要說一下TTLCache雨效,當一個Ceche被設(shè)置為TTLCache,那么它的存活時間只有指定的時長ageLimit废赞,當它存活的時間超過ageLimit時會被清理徽龟。在PINCache中設(shè)置ageLimit并未將TTLCache設(shè)置稱為YES,但是通過閱讀PINCache源碼發(fā)現(xiàn)唉地,只有設(shè)置好ageLimit据悔,TTLCache才能在一定的時間限制內(nèi)清空過期緩存,而設(shè)置ageLimit時就說明緩存有了存活周期耘沼,所以此時一定是TTLCache极颓;(如理解有誤歡迎指正)

@property (strong, readonly) __nonnull dispatch_queue_t concurrentQueue;
/** 內(nèi)存緩存所占的總?cè)萘?/
@property (assign, readonly) NSUInteger totalCost;
@property (assign) NSUInteger costLimit;
/**
 *  緩存存活時間, 如果設(shè)置為一個大于0的值, 就被開啟為 TTL 緩存(指定存活期的緩存),即如果 ageLimit > 0 => ttlCache = YES;
 */
@property (assign) NSTimeInterval ageLimit;
/**
 *  如果指定為 YES, 緩存行為就像 TTL 緩存, 緩存只在指定的存活期(ageLimit)內(nèi)存活
 * Accessing an object in the cache does not extend that object's lifetime in the cache
 * When attempting to access an object in the cache that has lived longer than self.ageLimit,
 * the cache will behave as if the object does not exist
 */
@property (assign, getter=isTTLCache) BOOL ttlCache;
/** 是否當內(nèi)存警告時移除緩存, 默認 YES*/
@property (assign) BOOL removeAllObjectOnMemoryWoring;
/** 是否當進入到后臺時移除緩存, 默認 YES*/
@property (assign) BOOL removeAllObjectOnEnteringBackground;

@property (copy) WNMemoryCacheObjectBlock __nullable willAddObjectBlock;

@property (copy) WNMemoryCacheObjectBlock __nullable willRemoveObjectBlock;

@property (copy) WNMemoryCacheObjectBlock __nullable didAddObjectBlock;

@property (copy) WNMemoryCacheObjectBlock __nullable didRemoveObjectBlock;

@property (copy) WNMemoryCacheBlcok __nullable willRemoveAllObjectsBlock;
@property (copy) WNMemoryCacheBlcok __nullable didRemoveAllObjectsBlock;
@property (copy) WNMemoryCacheBlcok __nullable didReceiveMemoryWarningBlock;
@property (copy) WNMemoryCacheBlcok __nullable didEnterBackgroundBlock;

這里并沒有指定為nonatomic,所以就是默認的atomic耕拷,atomic是原子屬性讼昆,線程安全。atomic和nonatomic用來決定編譯器生成的getter和setter是否為原子操作骚烧。在多線程環(huán)境下浸赫,原子操作是必要的,否則有可能引起錯誤的結(jié)果赃绊。

這樣的話setter/getter會變成下面的樣式既峡,添加線程安全

- (BOOL)isTTLCache {
    BOOL isTTLCache;
    
    [self lock];
        isTTLCache = _ttlCache;
    [self unlock];
    
    return isTTLCache;
}

- (void)setTtlCache:(BOOL)ttlCache {
    [self lock];
        _ttlCache = ttlCache;
    [self unlock];
}

getter實現(xiàn)創(chuàng)建一個局部變量用于在臨界區(qū)內(nèi)獲得對象內(nèi)部的屬性值,setter在臨界區(qū)內(nèi)設(shè)置屬性

/**
 *  收到內(nèi)存警告操作
 */
- (void)didReceiveMemoryWarningNotification:(NSNotification *)notify {
    1.
    if (self.removeAllObjectOnMemoryWoring) {
        [self removeAllObject:nil];
    }
    
    __weak typeof(self)weakSelf = self;
    AsyncOption(
                __strong typeof(weakSelf)strongSelf = weakSelf;
                if (!strongSelf) return ;
                2.
                Lock(_lock);
                WNMemoryCacheBlcok didReceiveMemoryWarningBlock = strongSelf->_didReceiveMemoryWarningBlock;
                Unlock(_lock);
                3.
                if (didReceiveMemoryWarningBlock) {
                    didReceiveMemoryWarningBlock(strongSelf);
                }
    );
}
  1. 如果指定了當收到內(nèi)存警告時清理緩存,執(zhí)行 removeAllObject 方法
  2. 加鎖獲得當前線程指定的_didReceiveMemoryWaringBlock 回調(diào)
  3. 執(zhí)行回調(diào)

這里要說一下, 為什么只在第二步中加鎖,第三步?jīng)]有加鎖;

先指定個假設(shè)前提回調(diào)是個超級耗時操作, 并且現(xiàn)在函數(shù)被兩條線程訪問,

(1). 如果沒有鎖當線程1獲得didReceiveMemoryWarningBlock,這個時候 CPU 調(diào)度到線程2,由于didReceiveMemoryWarningBlock在線程1中已經(jīng)獲得,所以在線程2中執(zhí)行的起始是線程1中的回調(diào),導(dǎo)致回調(diào)不正確;

(2). 如果將第三步也加鎖,線程1執(zhí)行到第二步,加鎖,獲得回調(diào)并執(zhí)行,依舊是當線程1執(zhí)行到第二步時, CPU 調(diào)度到線程2,此時線程2執(zhí)行發(fā)現(xiàn)線程1加鎖操作,導(dǎo)致線程2等待,線程1安全執(zhí)行線程1的回調(diào),而回調(diào)是一個假設(shè)耗時10000s 的操作,導(dǎo)致線程2需要等待10000s, 效率低下;

(3).上述加鎖方式執(zhí)行的話,獲得回調(diào)函數(shù)是線程安全, 線程1獲得線程1中的回調(diào), 線程2獲得線程2中的回調(diào), 所以即使在執(zhí)行回調(diào)時進行 CPU 調(diào)度,那么線程1依舊執(zhí)行的是線程1的回調(diào),線程2執(zhí)行線程2的回調(diào),提高了效率,又避免安全性

所以,加鎖可以避免線程問題,但盲目加鎖會造成效率執(zhí)行低下

/**
 *  程序進入后臺操作
 */
- (void)didReceiveEnterBackgroundNotification:(NSNotification *)notify {
    if (self.removeAllObjectOnEnteringBackground) {
        [self removeAllObject:nil];
    }
    __weak typeof(self)weakSelf = self;
    AsyncOption(
               __strong typeof(weakSelf)strongSelf = weakSelf;
               if (!strongSelf) return ;
               Lock(_lock);
               WNMemoryCacheBlcok didEnterBackgroundBlock = strongSelf->_didEnterBackgroundBlock;
               Unlock(_lock);
               if (didEnterBackgroundBlock) {
                   didEnterBackgroundBlock(strongSelf);
               }
    );

}

函數(shù)體與執(zhí)行內(nèi)存警告的函數(shù)體相同, 就是回調(diào)方法不同碧查。

繼續(xù)往下看:

/**
 *  線程安全, 移除指定 key 的緩存, 并執(zhí)行回調(diào)
 *
 *  @param key 指定的緩存 key
 */
- (void)removeObjectAndExectureBlockForKey:(NSString *)key {
    1.
    Lock(_lock);
    id object = _dictionary[key];
    NSNumber *cost = _costs[key];
    WNMemoryCacheObjectBlock willRemoveObjectBlock = _willRemoveObjectBlock;
    WNMemoryCacheObjectBlock didRemoveObjectBlcok = _didRemoveObjectBlock;
    Unlock(_lock);
    
    2.
    if (willRemoveObjectBlock) {
        willRemoveObjectBlock(self, key, object);
    }
    
    3.
    Lock(_lock);
    if (cost) {
        _totalCost -= [cost unsignedIntegerValue];
    }
    [_dictionary removeObjectForKey:key];
    [_costs removeObjectForKey:key];
    [_dates removeObjectForKey:key];
    Unlock(_lock);
    
    4.
    if (didRemoveObjectBlcok) {
        didRemoveObjectBlcok(self, key, object);
    }
}

1 . 加鎖獲得對應(yīng) key 存儲的對象, 消耗, 及制定的回調(diào)

2 . 執(zhí)行將要移除的回調(diào), 與第四步形成呼應(yīng)

3 . 如果存儲該緩存存在花費, 從總花費中減去該該緩存的花費, 移除 key 對應(yīng)的緩存對象, 花費以及最后修改的時間,而這些操作是要放在一片臨界區(qū)內(nèi)的

       /**
     *  使所有的緩存時間 <= date
     *
     *  @param date 指定的緩存時間
     */
    - (void)trimMemoryToDate:(NSDate *)date {
        1.
        Lock(_lock);
        NSArray *sortKeyByDate = (NSArray *)[[_dates keysSortedByValueUsingSelector:@selector(compare:)] reverseObjectEnumerator];
        Unlock(_lock);
        
        2.
        NSUInteger index = [self binarySearchEqualOrMoreDate:date fromKeys:sortKeyByDate];
        3.
            NSIndexSet *indexSets = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, index)];
        4.
        [sortKeyByDate enumerateObjectsAtIndexes:indexSets
                                         options:NSEnumerationConcurrent
                                      usingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {       
                                            5.
                                          if (key) {
                                              [self removeObjectAndExectureBlockForKey:key];
                                          }
                                      }];
    }

這個方法我做了些修改, 在 PINCache 中,第一步按時間排序, 第二部從頭開始遍歷, 將時間 < 指定 date 的值移除, 我覺得當數(shù)據(jù)量很大時, 遍歷的效率低下, 于是我寫了個二分搜索, 搜索第一個大于等于 date 的位置, 所以我在第一步將排序結(jié)果進行倒轉(zhuǎn), 小的在前,大的在后

  1. 對_dates根據(jù) key 排序, 排序結(jié)果是時間大的在前面, 比如20150101 在 20141230前面; 之后執(zhí)行數(shù)組倒轉(zhuǎn), 小的在前, 大的在后
  1. 二分搜索算法, 搜索第一個大于等于指定 date 的位置
  1. 創(chuàng)建區(qū)間[0, index)
  1. 變量區(qū)間, 如果有 key, 就將其從緩存中移除, 并執(zhí)行指定的"移除數(shù)據(jù)的回調(diào)"
    /**
     *   根據(jù)緩存大小移除緩存到臨界值, 緩存大的先被移除
     *
     *  @param limit 緩存臨界值
     */
    - (void)trimToCostLimit:(NSUInteger)limit {
        // 1.
        __block NSUInteger totalCost = 0;
        // 2.
        Lock(_lock);
        totalCost = _totalCost;
        NSArray *keysSortByCost = [_costs keysSortedByValueUsingSelector:@selector(compare:)];
        Unlock(_lock);
        // 3.
        if (totalCost <= limit) {
            return ;
        }
        // 4.
        [keysSortByCost enumerateObjectsWithOptions:NSEnumerationReverse
                                         usingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {
                                             [self removeObjectAndExectureBlockForKey:key];
                                             Lock(_lock);
                                             totalCost = _totalCost;
                                             Unlock(_lock);
                                             if (totalCost <= limit) {
                                                 *stop = YES;
                                             }
                                         }];
    }       
  1. 設(shè)置局部變量, 負責記錄在移除的過程中總花費的變化
  1. 加鎖獲取公共資源
  1. 如果當前的總花費小于限制值,直接返回
  1. 執(zhí)行移除緩存操作, 從大到小逐個移除, 同時加鎖修改總花費, 當總花費小于限制時, 停止移除操作
/**
 *  遞歸檢查并清除超過規(guī)定時間的緩存對象, TTL緩存操作
 */
- (void)trimToAgeLimitRecursively {

    Lock(_lock);
    NSTimeInterval ageLimit = _ageLimit;
    BOOL ttlCache = _ttlCache;
    Unlock(_lock);
    
    if (ageLimit == 0.0 || !ttlCache) {
        return ;
    }
    // 從當前時間開始, 往前推移 ageLimit(內(nèi)存緩存對象允許存在的最大時間)
    NSDate *trimDate = [NSDate dateWithTimeIntervalSinceNow:-ageLimit];
    // 將計算得來的時間點之前的數(shù)據(jù)清除, 確保每個對象最大存在 ageLimit 時間
    [self trimMemoryToDate:trimDate];
    
    // ageLimit 之后在遞歸執(zhí)行
    __weak typeof(self)weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ageLimit * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf)strongSelf = weakSelf;
        [strongSelf trimToAgeLimitRecursively];
    });
}

這個方法我寫了注釋, 主要思路就是從當前時間為起點,往前推移一個設(shè)置的緩存存活時間, 這段時間段內(nèi)的緩存應(yīng)當被清理,然后 ageLimit 之后繼續(xù)執(zhí)行該方法, 同樣清理這段時間里的緩存, 這是個遞歸調(diào)用, 每隔 ageLimit 時間請一次緩存

/**
 *  移除所有的數(shù)據(jù)
 *
 *  @param callBack 回調(diào)
 */
- (void)removeAllObject:(WNMemoryCacheBlcok)callBack {
    __weak typeof(self)weakSelf = self;
    // 異步移除所有數(shù)據(jù)
    AsyncOption(
                __strong typeof(weakSelf)strongSelf = weakSelf;
                [strongSelf removeAllObjects];
                if (callBack) {
                    callBack(strongSelf);
                });
}

異步指定移除操作,將移除緩存方法放入到 GCD 的異步線程中

/**
 *  線程安全的緩存對象的讀取操作, 所有關(guān)于緩存讀取的操作都是調(diào)用該方法
 *
 *  @param key 要獲得的緩存對應(yīng)的 key
 *
 *  @return 緩存對象
 */
- (__nullable id)objectForKey:(NSString *)key {
    if (!key) {
        return nil;
    }
    
    NSDate *now = [NSDate date];
    Lock(_lock);
    id object = nil;
    /**
     *  如果指定了 TTL, 那么判斷是否指定存活期, 如果指定存活期, 要判斷對象是否在存活期內(nèi)
     *  如果沒有指定 TTL, 那么緩存對象一定存在, 直接獲得
     */
    if (!self->_ttlCache ||
        self->_ageLimit <= 0 ||
        fabs([_dates[key] timeIntervalSinceDate:now]) < self->_ageLimit) {
        object = _dictionary[key];
    }
    Unlock(_lock);
    if (object) {
        Lock(_lock);
        _dates[key] = now;
        Unlock(_lock);
    }
    return object;
}

/**
 *  線程安全的緩存存儲操作, 所有的緩存寫入都是調(diào)用該方法
 *
 *  @param object 要緩存的對象
 *  @param key    緩存對象對應(yīng)的 Key
 *  @param cost   緩存的代價
 */
- (void)setObject:(id)object forKey:(NSString *)key withCost:(NSUInteger)cost {
    if (!key || !object) {
        return ;
    }
    // 加鎖獲得回調(diào)
    Lock(_lock);
    WNMemoryCacheObjectBlock willAddObjectBlock = _willAddObjectBlock;
    WNMemoryCacheObjectBlock didAddObjectBlock = _didAddObjectBlock;
    NSUInteger coseLimit = _costLimit;
    Unlock(_lock);
    
    // 執(zhí)行回調(diào)
    if (willAddObjectBlock) {
        willAddObjectBlock(self, key, object);
    }
    
    // 加鎖設(shè)置緩存信息
    Lock(_lock);
    _dictionary[key] = object, _costs[key] = @(cost), _dates[key] = [NSDate date];
    _totalCost += cost;
    Unlock(_lock);
    
    // 執(zhí)行回調(diào)     
    if (didAddObjectBlock) {
        didAddObjectBlock(self, key, object);
    }
    
    // 如果設(shè)置花費限制, 判斷此時總花費是否大于花費限制
    if (coseLimit > 0) {
        [self trimCostByDateToCostLimit:coseLimit];
    }
}

/**
 *  根據(jù)時間, 先移除時間最久的緩存, 直到緩存容量小于等于指定的 limit
 *  LRU(Last Recently Used): 最久未使用算法, 使用時間距離當前最就的將被移除
 */
- (void)trimCostByDateToCostLimit:(NSUInteger)limit {
    __block NSUInteger totalCost = 0;
    Lock(_lock);
    totalCost = _totalCost;
    NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)];
    Unlock(_lock);
    if (totalCost <= limit) {
        return;
    }
    
    // 先移除時間最長的緩存, date 時間小的
    [keysSortedByDate enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {
        [self removeObjectAndExectureBlockForKey:key];
        Lock(_lock);
        totalCost = _totalCost;
        Unlock(_lock);
        if (totalCost <= limit) {
            *stop = YES;
        }
    }];
}

執(zhí)行緩存的設(shè)置和獲取操作,最核心的是線程安全設(shè)置和獲得, 異步也只是將線程安全的方法放入到異步線程, 在此不再贅述, 更多看源碼, 有詳細注釋

還有兩點要說:

PINCache 實現(xiàn)了下標腳本設(shè)置和獲取方法, 即通過 id obj = cache[@"key"] 獲得緩存值, cache[@"key"] = object設(shè)置緩存值.

具體步驟是兩個協(xié)議方法

@required
/**
 *  下標腳本的取值操作, 實現(xiàn)該方法, 可以通過下標腳本獲得存儲的緩存值
 *  就像這樣獲得緩存值 id obj = cache[@"key"]
 *  @param key 緩存對象關(guān)聯(lián)的 key
 *
 *  @return  指定 key 的緩存對象
 */
- (id)objectForKeyedSubscript:(NSString *)key;

/**
 *  下標腳本的設(shè)置值操作, 實現(xiàn)該方法可以通過下標腳本設(shè)置緩存
 *  像這樣 cache[@"key"] = object
 *  @param obj 要緩存的對象
 *  @param key 緩存對象關(guān)聯(lián)的 key
 */
- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key;

/**
 *  以上兩個方法應(yīng)該確保線程安全
 */

MemoryCache 中具體實現(xiàn)

#pragma mark - Protocol Method
- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key {
    [self setObject:obj forKey:key withCost:0];
}

- (id)objectForKeyedSubscript:(NSString *)key {
    return [self objectForKey:key];
}

設(shè)置和獲得緩存都應(yīng)該是線程安全的

還有一點就是由于我們設(shè)置屬性為 atomic, 所以我們的 setter/getter 要確保線程安全, 具體上代碼:

- (NSTimeInterval)ageLimit {
    Lock(_lock);
    NSTimeInterval age = _ageLimit;
    Unlock(_lock);
    return age;
}

- (void)setAgeLimit:(NSTimeInterval)ageLimit {
    Lock(_lock);
    _ageLimit = ageLimit;
    if (ageLimit > 0) {
        _ttlCache = YES;
    }
    Unlock(_lock);
    [self trimToAgeLimitRecursively];
}

- (NSUInteger)costLimit {
    Lock(_lock);
    NSUInteger limit = _costLimit;
    Unlock(_lock);
    return limit;
}

- (void)setCostLimit:(NSUInteger)costLimit {
    Lock(_lock);
    _costLimit = costLimit;
    Unlock(_lock);
    if (costLimit > 0) {
        [self trimCostByDateToCostLimit:costLimit];
    }
}

以上就是內(nèi)存緩存一些必要知識, 以上只是一部分代碼, 具體看項目源碼 WannaCache

感謝:

Amin706 PINCache

陽光飛鳥 atomic與nonatomic的區(qū)別

臨界區(qū)-百度百科

臨界區(qū)-維基百科

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末运敢,一起剝皮案震驚了整個濱河市校仑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌传惠,老刑警劉巖迄沫,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卦方,居然都是意外死亡羊瘩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門盼砍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尘吗,“玉大人,你說我怎么就攤上這事浇坐〔谴罚” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵近刘,是天一觀的道長擒贸。 經(jīng)常有香客問我,道長觉渴,這世上最難降的妖魔是什么酗宋? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮疆拘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寂曹。我一直安慰自己哎迄,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布隆圆。 她就那樣靜靜地躺著漱挚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渺氧。 梳的紋絲不亂的頭發(fā)上旨涝,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音侣背,去河邊找鬼白华。 笑死,一個胖子當著我的面吹牛贩耐,可吹牛的內(nèi)容都是我干的弧腥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼潮太,長吁一口氣:“原來是場噩夢啊……” “哼管搪!你這毒婦竟也來了虾攻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤更鲁,失蹤者是張志新(化名)和其女友劉穎霎箍,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澡为,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡漂坏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缀壤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡筋夏,死狀恐怖条篷,靈堂內(nèi)的尸體忽然破棺而出蛤织,到底是詐尸還是另有隱情,我是刑警寧澤乞巧,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布绽媒,位于F島的核電站免猾,受9級特大地震影響猎提,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疙教,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一松逊、第九天 我趴在偏房一處隱蔽的房頂上張望肯夏。 院中可真熱鬧犀暑,春花似錦烁兰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至槽奕,卻和暖如春粤攒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背焕济。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工晴弃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逊拍,地道東北人顺献。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓注整,卻偏偏與公主長得像度硝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子椒袍,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容