YYCache 閱讀總結(jié)

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

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;
  1. 方法一 實現(xiàn):先去內(nèi)存緩存中查找,如果沒有查找就去磁盤緩存中找,找到就返回 YES
  2. 方法二 實現(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;
  1. 方法一 實現(xiàn):先去內(nèi)存緩存中查找,如果沒有查找就去磁盤緩存中找,找到就返回結(jié)果。如果是在磁盤緩存中找到的緩存,先把緩存存入內(nèi)存緩存中,然后再返回結(jié)果
  2. 方法二 實現(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;
  1. 方法一 實現(xiàn)邏輯:首先調(diào)用內(nèi)存緩存設(shè)置緩存API(setObject:forKey:),然后調(diào)用磁盤緩存設(shè)置API(setObject:forKey:)
  2. 方法二 實現(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;
  1. 方法一 實現(xiàn)邏輯:首先調(diào)用內(nèi)存緩存刪除 API(removeObjectForKey:),然后調(diào)用磁盤緩存刪除API(removeObjectForKey:)
  2. 方法二 實現(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;
  1. 方法一 實現(xiàn):首先調(diào)用內(nèi)存緩存清空 API(removeAllObjects),然后調(diào)用磁盤緩存清空 API(removeAllObjects)
  2. 方法二 實現(xiàn):首先調(diào)用內(nèi)存緩存清空 API(removeAllObjects),然后調(diào)用磁盤緩存異步清空 API(removeAllObjects)
  3. 方法三 實現(xiàn):首先調(diào)用內(nèi)存緩存清空 API(removeAllObjects),然后調(diào)用磁盤緩存帶刪除進(jìn)度的清空緩存 API (removeAllObjectsWithProgressBlock:endBlock:)

YYMemoryCache

YYMemoryCache 內(nèi)部有一個儲存對象,實現(xiàn)分為兩部分:

  1. 第一部分,淘汰算法,這里使用一個雙向鏈表,每個節(jié)點為 _YYLinkedMapNode 類對象,通過訪問最后訪問時間來對鏈表進(jìn)行排列,最新訪問的緩存節(jié)點放在鏈表的頭部,淘汰算法只需要將鏈表未尾節(jié)點移除即可
  2. 第二部分,查找算法,這里使用的是 CFMutableDictionaryRef 散列表進(jìn)行存儲

YYMemoryCache 的多線程安全是 使用 pthread_mutex_t(互斥鎖) 來完成

YYDiskCache

  1. YYDiskCache 是對 YYKVStorage 封裝了異步訪問 API,多線程安全使用 dispatch_semaphore_t(二元信號量) 來完成
  2. YYDiskCache 中同一功能方法,同步和異步的區(qū)別:異步方法的實現(xiàn)其實質(zhì)上就是只是異步的調(diào)用了同步方法,所以我在下面方法的介紹的時候只介紹同步方法的實現(xiàn)
  3. YYDiskCache 自動清理緩存機(jī)制,內(nèi)部有一個這樣的 方法“_trimRecursively” 在類對象實例化的時候會被調(diào)用,仔細(xì)看一下源碼的實現(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:),這個異步方法的實現(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 寫入緩存

同步寫入方法的實現(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:),實現(xiàn)的套路異步調(diào)用同步寫入方法(setObject: forKey:)

YYDiskCache 刪除記錄的邏輯
  1. 刪除一條記錄(removeObjectForKey:),內(nèi)部實現(xiàn)是調(diào)用 YYKVStorage 的 removeObjectForKey:方法,并在調(diào)用的前后加上了鎖。而對應(yīng)的異步方法(removeObjectForKey: withBlock:),使用異步線程調(diào)用了同步方法(removeObjectForKey:)
  2. 刪除所有記錄
    1. 不帶進(jìn)度刪除所有記錄(removeAllObjects),內(nèi)部實現(xiàn)是調(diào)用 YYKVStorage 的 removeAllItems方法,并在調(diào)用的前后加上了鎖。而對應(yīng)的異步方法(removeAllObjectsWithBlock:),實質(zhì)上使用異步線程調(diào)用了同步方法(removeAllObjects)
    2. 帶刪除進(jìn)度地刪除所有記錄(removeAllObjectsWithProgressBlock:endBlock:),內(nèi)部實現(xiàn)是異步調(diào)用 YYKVStorage 的 removeAllItemsWithProgressBlock:endBlock: 方法,并在這個方法的調(diào)用前后加了鎖。
YYDiskCache 磁盤清理邏輯
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;

在幾種磁盤清理實現(xiàn)方式基本一致,都是調(diào)用 YYKVStorage 對應(yīng)的清理方法

YYKVStorage

YYKVStorage 為磁盤緩存的核心類,提供給了外部數(shù)據(jù)庫(sqlite3)存儲以及系統(tǒng)文件(系統(tǒng)文件管理類 NSFileManager)存儲方式,在使用文件存儲時是配合數(shù)據(jù)庫存儲的,文件的描述信息存在數(shù)據(jù)庫中。

  • 磁盤的淘汰算法實現(xiàn),淘汰算法使用的是最后訪問時間來進(jìn)行淘汰的,每當(dāng)數(shù)據(jù)庫中某條記錄被訪問到后,這條記錄就會更新最后訪問時間為當(dāng)前時間,而在刪除過期過期緩存時,只需要根據(jù)確定過期時間即可。
  • 磁盤的查找算法實現(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ù)據(jù)存在系統(tǒng)文件中,通過 filename 去系統(tǒng)文件中找到對應(yīng)文件,并賦值給這個對象的 value 字段。

總結(jié)

閱讀 YYCache 源碼,從開始到讀完一共花了三天的樣子,真心感嘆 大神寫得代碼 跟我這個凡人的差距。在閱讀源碼的過程中有很多只是點是自己沒有了解過的,得去問度娘查資料,所以閱讀得比較慢,比如“pthread_mutex_t 特性”、“sqlite3 特性”、“sql 語句”、“數(shù)據(jù)庫的 wal 模式”、“數(shù)據(jù)庫 synchronous 的模式選擇與區(qū)別”、“CFMutableDictionaryRef”等等。在源碼很多的實現(xiàn)簡直不要太精妙,希望下次能抽出時間去看看 YYText 的源碼,如果我能有這兩把刷子,此生足以????。

源碼筆記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容