YYCache 的基本使用
- (void)synchronizingStorageForCache {
NSString *value1 = @"朽木自雕";
NSString *key1 = @"key1";
NSArray *value2 = @[@"1", @"2", @"3", @"4", @"5"];
NSString *key2 = @"key2";
// 創(chuàng)建緩存對象
YYCache *cache = [[YYCache alloc]initWithName:@"cacheTest"];
[cache setObject:value1 forKey:key1];
[cache setObject:value2 forKey:key2];
// 判斷對象是否存在
if ([cache containsObjectForKey:key1]) {
// 取出緩存
id value = [cache objectForKey:key1];
NSLog(@"%@", value);
}
// 判斷對象是否存在
if ([cache containsObjectForKey:key2]) {
// 取出緩存
id value = [cache objectForKey:key2];
NSLog(@"%@", value);
}
}
其他的 API 的使用很簡單蘸秘,不在這里逼逼叨逼逼叨的
類圖
類說明
YYCache
YYCache 是提供用用戶使用的對象恨搓,內(nèi)部對 YYMemoryCache 和 YYDiskCache 功能的整合封裝。為 YYMemoryCache 提供了多線程功能好爬,而 YYDiskCache 對象本身內(nèi)部封裝了異步讀寫功能局雄。封裝的功能包括:
初始化緩存對象
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path;
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
這幾個方法功能是一直的,最終方法都是會掉“initWithPath:”
判斷是否存在某條緩存
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;
- 方法一 實(shí)現(xiàn):先去內(nèi)存緩存中查找存炮,如果沒有查找就去磁盤緩存中找炬搭,找到就返回 YES
- 方法二 實(shí)現(xiàn):同步在在內(nèi)存中查找緩存,如果找到了穆桂,使用異步返回宫盔,如果沒有找到,就調(diào)用磁盤緩存的異步查找 API享完,并把查找結(jié)果異步回調(diào)
通過 key 取出緩存
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;
- 方法一 實(shí)現(xiàn):先去內(nèi)存緩存中查找灼芭,如果沒有查找就去磁盤緩存中找,找到就返回結(jié)果般又。如果是在磁盤緩存中找到的緩存彼绷,先把緩存存入內(nèi)存緩存中,然后再返回結(jié)果
- 方法二 實(shí)現(xiàn):同步在在內(nèi)存中查找緩存茴迁,如果找到了寄悯,使用異步返回,如果沒有找到堕义,就調(diào)用磁盤緩存的異步查找 API猜旬,如果是在磁盤緩存中找到的緩存,先把緩存存入內(nèi)存緩存中倦卖,然后再返回結(jié)果
增洒擦、改--緩存對象
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;
- 方法一 實(shí)現(xiàn)邏輯:首先調(diào)用內(nèi)存緩存設(shè)置緩存API(setObject:forKey:),然后調(diào)用磁盤緩存設(shè)置API(setObject:forKey:)
- 方法二 實(shí)現(xiàn)邏輯:首先調(diào)用內(nèi)存緩存設(shè)置緩存API(setObject:forKey:)怕膛,然后調(diào)用磁盤緩存異步設(shè)置API(setObject:forKey:)
刪除 key 對應(yīng)的緩存記錄
- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;
- 方法一 實(shí)現(xiàn)邏輯:首先調(diào)用內(nèi)存緩存刪除 API(removeObjectForKey:)熟嫩,然后調(diào)用磁盤緩存刪除API(removeObjectForKey:)
- 方法二 實(shí)現(xiàn)邏輯:首先調(diào)用內(nèi)存緩存刪除 API(removeObjectForKey:),然后調(diào)用磁盤緩存異步刪除API(removeObjectForKey:withBlock:)
清空所有緩存記錄
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
- 方法一 實(shí)現(xiàn):首先調(diào)用內(nèi)存緩存清空 API(removeAllObjects)褐捻,然后調(diào)用磁盤緩存清空 API(removeAllObjects)
- 方法二 實(shí)現(xiàn):首先調(diào)用內(nèi)存緩存清空 API(removeAllObjects)邦危,然后調(diào)用磁盤緩存異步清空 API(removeAllObjects)
- 方法三 實(shí)現(xiàn):首先調(diào)用內(nèi)存緩存清空 API(removeAllObjects)洋侨,然后調(diào)用磁盤緩存帶刪除進(jìn)度的清空緩存 API (removeAllObjectsWithProgressBlock:endBlock:)
YYMemoryCache
YYMemoryCache 內(nèi)部有一個儲存對象舍扰,實(shí)現(xiàn)分為兩部分:
- 第一部分倦蚪,淘汰算法,這里使用一個雙向鏈表边苹,每個節(jié)點(diǎn)為 _YYLinkedMapNode 類對象陵且,通過訪問最后訪問時間來對鏈表進(jìn)行排列,最新訪問的緩存節(jié)點(diǎn)放在鏈表的頭部个束,淘汰算法只需要將鏈表未尾節(jié)點(diǎn)移除即可
- 第二部分慕购,查找算法,這里使用的是 CFMutableDictionaryRef 散列表進(jìn)行存儲
YYMemoryCache 的多線程安全是 使用 pthread_mutex_t(互斥鎖) 來完成
YYDiskCache
- YYDiskCache 是對 YYKVStorage 封裝了異步訪問 API茬底,多線程安全使用 dispatch_semaphore_t(二元信號量) 來完成
- YYDiskCache 中同一功能方法沪悲,同步和異步的區(qū)別:異步方法的實(shí)現(xiàn)其實(shí)質(zhì)上就是只是異步的調(diào)用了同步方法,所以我在下面方法的介紹的時候只介紹同步方法的實(shí)現(xiàn)
- YYDiskCache 自動清理緩存機(jī)制阱表,內(nèi)部有一個這樣的 方法“_trimRecursively” 在類對象實(shí)例化的時候會被調(diào)用殿如,仔細(xì)看一下源碼的實(shí)現(xiàn):
// 自動檢查緩存
- (void)_trimRecursively {
__weak typeof(self) _self = self;
// 延遲執(zhí)行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
// 清理緩存
[self _trimInBackground];
// 遞歸的調(diào)用
[self _trimRecursively];
});
}
// 在后臺線程中清理磁盤緩存
- (void)_trimInBackground {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
if (!self) return;
// 沒什么好解釋,磁盤數(shù)據(jù)的讀寫必須加鎖最爬,保證多線程讀寫安全
Lock();
[self _trimToCost:self.costLimit];
[self _trimToCount:self.countLimit];
[self _trimToAge:self.ageLimit];
[self _trimToFreeDiskSpace:self.freeDiskSpaceLimit];
Unlock();
});
}
注釋都寫好了涉馁,不用再解釋吧。
YYDiskCache 判斷是否存在某條緩存
- (BOOL)containsObjectForKey:(NSString *)key {
// 判斷參數(shù)的合法性
if (!key) return NO;
//上鎖爱致,沒什么好說的烤送,為了安全起見
Lock();
// 從數(shù)據(jù)庫中查詢
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
//返回結(jié)果
return contains;
}
同時還存在一個功能相同,但是異步的方法(containsObjectForKey:withBlock:)糠悯,這個異步方法的實(shí)現(xiàn)就是異步調(diào)用了方法(containsObjectForKey:)帮坚,不妨看看源碼
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block {
// 判斷參數(shù)的合法性
if (!block) return;
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
BOOL contains = [self containsObjectForKey:key];
block(key, contains);
});
}
YYDiskCache 獲取某條緩存(objectForKey:)
這個方法的返回的是一個 遵守 NSCoding 協(xié)議的類對象,對象的解碼可以 customUnarchiveBlock 屬性互艾,自定義解碼试和。默認(rèn)內(nèi)部使用 NSCoding 歸檔工具解碼。
- (id<NSCoding>)objectForKey:(NSString *)key {
// 判斷參數(shù)的合法性
if (!key) return nil;
//上鎖忘朝,沒什么好說的灰署,為了安全起見
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
if (!item.value) return nil;
id object = nil;
if (_customUnarchiveBlock) {
// 外部解壓
object = _customUnarchiveBlock(item.value);
// 自己解壓
} else {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
// 擴(kuò)展參數(shù)。為了做到速度的極致局嘁,這里也是拼了
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
// 返回結(jié)果
return object;
}
YYDiskCache 寫入緩存
同步寫入方法的實(shí)現(xiàn)(setObject: forKey:)
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
// 判斷 key 的合法性
if (!key) return;
// 當(dāng) object 為 nil 時溉箕,改存入為刪除
if (!object) {
[self removeObjectForKey:key];
return;
}
// 獲取到擴(kuò)展數(shù)據(jù)
NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
// 外部壓縮
if (_customArchiveBlock) {
value = _customArchiveBlock(object);
} else {
@try {
// 內(nèi)部壓縮
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
// 如果壓縮的值為空,就不用存數(shù)據(jù)了
if (!value) return;
NSString *filename = nil;
// 判斷緩存存儲方式悦昵,如果不是 數(shù)據(jù)庫存儲肴茄,則進(jìn)入條件
if (_kv.type != YYKVStorageTypeSQLite) {
// 如果值的長度大于臨界值,則以文件的形式進(jìn)行存儲
if (value.length > _inlineThreshold) {
// 獲取文件名
filename = [self _filenameForKey:key];
}
}
// 數(shù)據(jù)寫入本地磁盤
// 上鎖但指,沒什么好說的寡痰,為了安全起見
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
對應(yīng)的異步寫入方法(setObject: forKey: withBlock:)抗楔,實(shí)現(xiàn)的套路異步調(diào)用同步寫入方法(setObject: forKey:)
YYDiskCache 刪除記錄的邏輯
- 刪除一條記錄(removeObjectForKey:),內(nèi)部實(shí)現(xiàn)是調(diào)用 YYKVStorage 的 removeObjectForKey:方法拦坠,并在調(diào)用的前后加上了鎖连躏。而對應(yīng)的異步方法(removeObjectForKey: withBlock:),使用異步線程調(diào)用了同步方法(removeObjectForKey:)
- 刪除所有記錄
- 不帶進(jìn)度刪除所有記錄(removeAllObjects)贞滨,內(nèi)部實(shí)現(xiàn)是調(diào)用 YYKVStorage 的 removeAllItems方法入热,并在調(diào)用的前后加上了鎖。而對應(yīng)的異步方法(removeAllObjectsWithBlock:)晓铆,實(shí)質(zhì)上使用異步線程調(diào)用了同步方法(removeAllObjects)
- 帶刪除進(jìn)度地刪除所有記錄(removeAllObjectsWithProgressBlock:endBlock:)勺良,內(nèi)部實(shí)現(xiàn)是異步調(diào)用 YYKVStorage 的 removeAllItemsWithProgressBlock:endBlock: 方法,并在這個方法的調(diào)用前后加了鎖骄噪。
YYDiskCache 磁盤清理邏輯
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
在幾種磁盤清理實(shí)現(xiàn)方式基本一致尚困,都是調(diào)用 YYKVStorage 對應(yīng)的清理方法
YYKVStorage
YYKVStorage 為磁盤緩存的核心類,提供給了外部數(shù)據(jù)庫(sqlite3)存儲以及系統(tǒng)文件(系統(tǒng)文件管理類 NSFileManager)存儲方式链蕊,在使用文件存儲時是配合數(shù)據(jù)庫存儲的事甜,文件的描述信息存在數(shù)據(jù)庫中。
- 磁盤的淘汰算法實(shí)現(xiàn)示弓,淘汰算法使用的是最后訪問時間來進(jìn)行淘汰的讳侨,每當(dāng)數(shù)據(jù)庫中某條記錄被訪問到后,這條記錄就會更新最后訪問時間為當(dāng)前時間奏属,而在刪除過期過期緩存時跨跨,只需要根據(jù)確定過期時間即可。
- 磁盤的查找算法實(shí)現(xiàn)囱皿,前面說過勇婴,磁盤存儲分為兩種,一種是數(shù)據(jù)庫嘱腥,每條緩存對應(yīng)數(shù)據(jù)庫表中的一條記錄耕渴,另一種是系統(tǒng)文件,每條緩存對應(yīng)系統(tǒng)文件中的一個文件齿兔。在通過 key 查找某個緩存時橱脸,首先去數(shù)據(jù)庫中找到 key 對應(yīng)的記錄,把去出來的數(shù)據(jù)轉(zhuǎn)換成 YYKVStorageItem 類對象分苇,判斷 filename 字段是否為空添诉,如果為空,說明數(shù)據(jù)存在數(shù)據(jù)空医寿,則直接使用這個對象的 value 字段栏赴,如果不為空,則說明緩存實(shí)體數(shù)據(jù)存在系統(tǒng)文件中靖秩,通過 filename 去系統(tǒng)文件中找到對應(yīng)文件须眷,并賦值給這個對象的 value 字段竖瘾。
總結(jié)
閱讀 YYCache 源碼,從開始到讀完一共花了三天的樣子花颗,真心感嘆 大神寫得代碼 跟我這個凡人的差距捕传。在閱讀源碼的過程中有很多只是點(diǎn)是自己沒有了解過的,得去問度娘查資料捎稚,所以閱讀得比較慢乐横,比如“pthread_mutex_t 特性”、“sqlite3 特性”今野、“sql 語句”、“數(shù)據(jù)庫的 wal 模式”罐农、“數(shù)據(jù)庫 synchronous 的模式選擇與區(qū)別”条霜、“CFMutableDictionaryRef”等等。在源碼很多的實(shí)現(xiàn)簡直不要太精妙涵亏,希望下次能抽出時間去看看 YYText 的源碼宰睡,如果我能有這兩把刷子,此生足以????气筋。