YYCache初探

一直想研究研究YYKit的源碼,最近剛好抽出了些時間開始看,希望這是一個系列的文章。說句題外話YYKit真的一個龐大的工具庫,涵蓋了我們日常絕大多數需要的工具,不僅僅是常用的YYModel、YYCache、YYImage、YYText還提供了NSString、NSObject、NSArray、NSNumber等的Category,提供了使用平率很高的工具方法,甚至提供了KVO的block形式的封裝,接入一個YYKit很多其他的第三方庫都不再需要接入了。

好了直入主題,下圖是YYCache的架構圖:

YYCache架構圖.jpg

下面是YYCache的使用方法:

YYCache *cache = [YYCache cacheWithName:@"courseCache"];
[cache setObject:obj1 forKey:key1];
[cache containsObjectForKey:key1];
[cache objectForKey:key1];
...

默認情況下YYCache會在內存和文件/DB中分別緩存一份數據,取數據時先向內存取,若沒有名中則向文件/DB取,同時更新內存中的緩存,這一點和SDWebImage中對圖片的緩存機制是一致的。YYCache內部實現了LRU算法,然后通過總數量、總大小、存活時間這些指標配合LRU實現了淘汰緩存文件的機制。本文主要是介紹YYCache內部的LRU實現機制。

YYMemoryCache

YYMemoryCache內部用一個雙向鏈表實現了LRU算法。

鏈表節(jié)點_YYLinkedMapNode

/**
 A node in linked map.
 */
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // 指向前一個節(jié)點 retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // 指向后一個節(jié)點 retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

@implementation _YYLinkedMapNode
@end

雙向鏈表結構_YYLinkedMap

/**
 A linked map used by YYMemoryCache.
 It's not thread-safe and does not validate the parameters.
 */
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // 實際持有節(jié)點的字典
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // 尾節(jié)點
    _YYLinkedMapNode *_tail; // 頭結點
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

下面是一些雙向鏈表的操作,就是一個典型的雙向鏈表的操作,相信讀者都能看明白,不做過多注釋 :)

頭部插入操作:

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    _totalCost += node->_cost;// cost增加
    _totalCount++; //數量增加
    if (_head) {
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    } else {
        _head = _tail = node;
    }
}

刪除操作:

- (void)removeNode:(_YYLinkedMapNode *)node {
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
    _totalCost -= node->_cost;
    _totalCount--;
    if (node->_next) node->_next->_prev = node->_prev;
    if (node->_prev) node->_prev->_next = node->_next;
    if (_head == node) _head = node->_next;
    if (_tail == node) _tail = node->_prev;
}

將一個節(jié)點移至頭結點:

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    if (_head == node) return;
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    } else {
        node->_next->_prev = node->_prev;
        node->_prev->_next = node->_next;
    }
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}

同時YYMemory內部有一個計時器,默認每隔5秒鐘檢查一次緩存的狀態(tài)(主要是總數量、總大小、存活時間),若超出則通過雙向鏈表刪除尾節(jié)點。

- (void)_trimRecursively {
    __weak typeof(self) _self = self;
    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];
        [self _trimRecursively];
    });
}

- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}

至于這里為什么沒有用NSTimer,我也沒弄明白(攤手。還請知道的讀者賜教。
update: 2017.10.26
NSTimer必須放在runloop中才能生效,使用dispatch_after的方式可以避開這個限制
。

YYDiskCache

YYDiskCache用文件和SQLite作為存儲介質。至于這兩者的規(guī)則,YYDiskCache.h有這樣一個參數:

@property (readonly) NSUInteger inlineThreshold;

簡而言之超過這個閾值會使用文件存儲,低于這個閾值會使用SQLite存儲,這個值默認是20KB。

但是注意,這里的文件存儲和SQLite存儲并不是指的數據完全用文件存儲或者SQLite存儲;具體的規(guī)則還是代碼比較好說明:

YYKVStorage.h
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    YYKVStorageTypeFile = 0,
    YYKVStorageTypeSQLite = 1,
    YYKVStorageTypeMixed = 2,
};

YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold {
    ...
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    ...
}

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    ...
    NSString *filename = nil;
    if (_kv.type != YYKVStorageTypeSQLite) {
        if (value.length > _inlineThreshold) {
            filename = [self _filenameForKey:key];//計算key的MD5值
        }
    }
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}

YYKVStorage.m

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    ...    
    if (filename.length) {
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

oh no,我真的不想貼這么多源碼,也難為ibreme設計出這么一套規(guī)則,反正就一個目的,在SQLite是一定能找到緩存數據對應的key->filename的。至于真正的數據data(value)只有在type是YYKVStorageTypeFile才會在文件中存儲一份備份。至于為什么這么做?因為SQLite能用時間戳取排序取出數據來實現LRU淘汰算法。所以所有的緩存數據必須在SQLite中找到索引。

一些其他的技巧

作者ibreme在源碼中使用了很多宏,比如NS_DESIGNATED_INITIALIZER UNAVAILABLE_ATTRIBUTE等等,UNAVAILABLE_ATTRIBUTE這個宏在封裝組件的時候尤其有用,當某各類的初始化強依賴于幾個必要的屬性的時候,可以禁用init方法和new方法 or whatever。下面是這個宏的使用姿勢:

YYCache.h

- (nullable instancetype)initWithName:(NSString *)name;

- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;

效果如下:

禁用宏.jpg

NSMapTable

日常開發(fā)中使用的NSDictionaryNSArray、NSSet對key和value都是強引用,但是在遇到需要對key或者value的指針是個弱引用的時候這個類就派上用場了:

[[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

YYDiskCache中由于想通過一個哈希表記錄path -> cache(YYDiskCache) 但是卻不想對cache形成強引用而使用了這個類。

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

相關閱讀更多精彩內容

  • YYCache簡介 YYCache由YYMemoryCache(高速內存緩存)和YYDiskCache(低速磁盤緩...
    簡書lu閱讀 1,530評論 0 5
  • YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來講解該框架的實現細節(jié),性能分析、設計...
    JonesCxy閱讀 689評論 0 2
  • 發(fā)現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 15,406評論 4 61
  • 作者設計思路 1.YYMemoryCache YYMemoryCache負責管理內存緩存。這個類是線程安全的。 L...
    WeiHing閱讀 719評論 0 7
  • 0、前提"安裝CocoaPods 因為最近兩天我更換了ssd固態(tài)硬盤和重裝了 macOS Sierra 10.12...
    朝雨晚風閱讀 22,340評論 0 2

友情鏈接更多精彩內容