做開發(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)用讹开。
先鋪一下需要了解的知識:
內(nèi)存緩存:一般使用字典來作為數(shù)據(jù)的緩存池,配合一個保存每個內(nèi)存緩存數(shù)據(jù)的緩存時間的字典捐名,一個保存每個內(nèi)存緩存數(shù)據(jù)的緩存容量的字典萧吠,一個保存內(nèi)存緩存總?cè)萘康淖兞俊τ谠鰟h改查操作桐筏,基本也都是圍繞著字典來的,需要重點注意的就是在這些個操作過程的多線程安全問題拇砰,還有同步和異步訪問方法梅忌,以及異步方法中的Block參數(shù)的循環(huán)引用問題。
-
線程安全:現(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ā)實在主線程中進行格郁,很少涉及多線程問題。
-
鎖:在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理张,效率能比信號量加鎖稍微高一點點
緩存策略:有優(yōu)先刪除緩存最久赫蛇,最少使用的策略,也有優(yōu)先刪除雾叭,容量最大悟耘,最少使用的策略。
臨界區(qū): 當訪問一個公共資源時,而這些公共資源無法被多個線程同時訪問,當一條線程進入臨界區(qū)時, 其他線程必須等待,公用資源是互斥的
共享資源: 一個類中的屬性, 成員變量全局變量就是這個對象的共享資源, 無論有多少個線程訪問該對象, 訪問的屬性全局變量成員變量都是同一塊內(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;
}
- 指定并發(fā)隊列的名稱
- 默認進入后臺旺嬉,收到內(nèi)存警告時清理所有的內(nèi)存緩存這個時候需要監(jiān)聽內(nèi)存警告 name:UIApplicationDidReceiveMemoryWarningNotification和進入后臺 name:UIApplicationDidEnterBackgroundNotification的通知 第6步
- 將緩存數(shù)據(jù),時間厨埋,消耗的字典進行初始化邪媳,直接訪問屬性能夠避免通過self調(diào)用get方法時消息發(fā)送時間的花費
- 定義的回調(diào)函數(shù),并初始化為空
- _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);
}
);
}
- 如果指定了當收到內(nèi)存警告時清理緩存,執(zhí)行 removeAllObject 方法
- 加鎖獲得當前線程指定的_didReceiveMemoryWaringBlock 回調(diào)
- 執(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), 小的在前,大的在后
- 對_dates根據(jù) key 排序, 排序結(jié)果是時間大的在前面, 比如20150101 在 20141230前面; 之后執(zhí)行數(shù)組倒轉(zhuǎn), 小的在前, 大的在后
- 二分搜索算法, 搜索第一個大于等于指定 date 的位置
- 創(chuàng)建區(qū)間[0, index)
- 變量區(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;
}
}];
}
- 設(shè)置局部變量, 負責記錄在移除的過程中總花費的變化
- 加鎖獲取公共資源
- 如果當前的總花費小于限制值,直接返回
- 執(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ū)別