YYCache理解

前言

本篇文章將帶來YYCache的解讀,YYCache支持內存和本地兩種方式的數據存儲。咱們先拋出兩個問題:node

  • YYCache是如何把數據寫入內存之中的?又是如何實現的高效讀取?
  • YYCache採用了何種方式把數據寫入磁盤?

先來看看YYCache總體框架設計圖算法


**分析下這個框架 **
從上圖能夠很清晰的看出這個框架的基礎框架爲:
交互層-邏輯層-處理層。
不管是最上層的YYCache或下面兩個獨立體YYMemoryCache和YYDiskCache都是這樣設計的。邏輯清晰。 來看看YYMemoryCache,邏輯層是負責對交付層的一個轉接和對處理層發佈一些命令,好比增刪改查。而處理層則是對邏輯層發佈的命令進行具體對應的處理。組件層則是對處理層的一種補充,也是元數據,擴展性很好。這兩個類咱們都是能夠單獨拿出來使用的。而多添加一層YYCache,則可讓咱們使用的更加方便,自動爲咱們同時進行內存緩存和磁盤緩存,讀取的時候得到內存緩存的高速讀取。
YYMemoryCache
咱們使用YYMemoryCache能夠把數據緩存進內存之中,它內部會建立了一個YYMemoryCache對象,而後把數據保存進這個對象之中。
但凡涉及到相似這樣的操做,代碼都須要設計成線程安全的。所謂的線程安全就是指充分考慮多線程條件下的增刪改查操做。
要想高效的查詢數據,使用字典是一個很好的方法。字典的原理跟哈希有關,總之就是把key直接映射成內存地址,而後處理衝突和和擴容的問題。對這方面有興趣的能夠自行搜索資料
YYMemoryCache內部封裝了一個對象_YYLinkedMap,包含了下邊這些屬性:

@interface _YYLinkedMap : NSObject { 
@package 
CFMutableDictionaryRef _dic; //哈希字典,存放緩存數據 
NSUInteger _totalCost; //緩存總大小 
NSUInteger _totalCount; //緩存節點總個數 
_YYLinkedMapNode *_head; //頭結點 
_YYLinkedMapNode *_tail; //尾結點 
BOOL _releaseOnMainThread; //在主線程釋放 
BOOL _releaseAsynchronously;//在異步線程釋放 
} 
@end 
複製代碼

_dic是哈希字典,負責存放緩存數據,_head和_tail分別是雙鏈表中指向頭節點和尾節點的指針,鏈表中的節點單元是_YYLinkedMapNode對象,該對象封裝了緩存數據的信息。sql

能夠看出來,CFMutableDictionaryRef _dic將被用來保存數據。這裏使用了CoreFoundation的字典,性能更好。字典裏邊保存着的是_YYLinkedMapNode對象。數據庫

@interface _YYLinkedMapNode : NSObject { 
@package 
__unsafe_unretained _YYLinkedMapNode *_prev; //前向前一個節點的指針 
__unsafe_unretained _YYLinkedMapNode *_next; //指向下一個節點的指針 
id _key; //緩存數據key 
id _value; //緩存數據value 
NSUInteger _cost; //節點佔用大小 
NSTimeInterval _time; //節點操做時間戳 
} 
@end 
複製代碼

上邊的代碼,就能知道使用了鏈表的知識。可是有一個疑問,單用字典咱們就能很快的查詢出數據,爲何還要實現鏈表這一數據結構呢?
答案就是淘汰算法,YYMemoryCache使用了LRU淘汰算法,也就是當數據超過某個限制條件後,咱們會從鏈表的尾部開始刪除數據,直到達到要求爲止。
LRU
LRU全稱是Least recently used,基於最近最少使用的原則,屬於一種緩存淘汰算法。實現思路是維護一個雙向鏈表數據結構,每次有新數據要緩存時,將緩存數據包裝成一個節點,插入雙向鏈表的頭部,若是訪問鏈表中的緩存數據,一樣將該數據對應的節點移動至鏈表的頭部。這樣的作法保證了被使用的數據(存儲/訪問)始終位於鏈表的前部。當緩存的數據總量超出容量時,先刪除末尾的緩存數據節點,由於末尾的數據最少被使用過。以下圖:編程

經過這種方式,就實現了相似數組的功能,是本來無序的字典成了有序的集合。
若是有一列數據已經按順序排好了,我使用了中間的某個數據,那麼就要把這個數據插入到最開始的位置,這就是一條規則,越是最近使用的越靠前。
在設計上,YYMemoryCache還提供了是否異步釋放數據這一選項,在這裏就不提了,咱們在來看看在YYMemoryCache中用到的鎖的知識。
pthread_mutex_lock是一種互斥所:

pthread_mutex_init(&_lock, NULL); // 初始化
pthread_mutex_lock(&_lock); // 加鎖
pthread_mutex_unlock(&_lock); // 解鎖
pthread_mutex_trylock(&_lock) == 0 // 是否加鎖,0:未鎖住,其餘值:鎖住
複製代碼

在OC中有不少種鎖能夠用,pthread_mutex_lock就是其中的一種。YYMemoryCache有這樣一種設置,每隔一個固定的時間就要處理數據,及邊界檢測,代碼以下:swift

邊界檢測數組

- (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];//遞歸調用本方法
    });
}
複製代碼

上邊的代碼中,每隔_autoTrimInterval時間就會在後臺嘗試處理數據,而後再次調用自身,這樣就實現了一個相似定時器的功能。這一個小技巧能夠學習一下。緩存

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

_trimInBackground分別調用_trimToCost、_trimToCount和_trimToAge方法檢測。
_trimToCost方法判斷鏈表中全部節點佔用大小之和totalCost是否大於costLimit,若是超過,則從鏈表末尾開始刪除節點,直到totalCost小於等於costLimit爲止。代碼註釋以下:安全

- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCost > costLimit) {
                _YYLinkedMapNode *node = [_lru removeTailNode];//刪除末尾節點
                if (node) [holder addObject:node];
            } else {
                finish = YES; //totalCost<=costLimit,檢測完成 
            }
            pthread_mutex_unlock(&_lock);
        } else {
            usleep(10 * 1000); //10 ms
        }
    }
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue 釋放了資源
        });
    }
}
複製代碼

其中每一個節點的cost是人爲指定的,默認是0,且costLimit默認是NSUIntegerMax,因此在默認狀況下,_trimToCost方法不會刪除末尾的節點。bash

_trimToCount方法判斷鏈表中的全部節點個數之和是否大於countLimit,若是超過,則從鏈表末尾開始刪除節點,直到個數之和小於等於countLimit爲止。代碼註釋以下:

- (void)_trimToCount:(NSUInteger)countLimit { 
    BOOL finish = NO; 
    ... 
    NSMutableArray *holder = [NSMutableArray new]; 
    while (!finish) { 
        if (pthread_mutex_trylock(&;_lock) == 0) { 
            if (_lru->_totalCount > countLimit) { 
                _YYLinkedMapNode *node = [_lru removeTailNode]; //刪除末尾節點 
                if (node) [holder addObject:node]; 
            } else { 
                finish = YES; //totalCount<=countLimit,檢測完成 
            } 
            pthread_mutex_unlock(&;_lock); 
        } else { 
            usleep(10 * 1000); //10 ms等待一小段時間
        } 
    } 
... 
} 
複製代碼

初始化時countLimit默認是NSUIntegerMax,若是不指定countLimit,節點的總個數永遠不會超過這個限制,因此_trimToCount方法不會刪除末尾節點。

_trimToAge方法遍歷鏈表中的節點,刪除那些和now時刻的時間間隔大於ageLimit的節點,代碼以下:

- (void)_trimToAge:(NSTimeInterval)ageLimit { 
    BOOL finish = NO; 
    ... 
    NSMutableArray *holder = [NSMutableArray new]; 
    while (!finish) { 
        if (pthread_mutex_trylock(&;_lock) == 0) { 
            if (_lru->_tail &;&; (now - _lru->_tail->_time) > ageLimit) { //間隔大於ageLimit 
                _YYLinkedMapNode *node = [_lru removeTailNode]; //刪除末尾節點 
                if (node) [holder addObject:node]; 
            } else { 
                finish = YES; 
            } 
            pthread_mutex_unlock(&;_lock); 
        } else { 
            usleep(10 * 1000); //10 ms 
        } 
    } 
... 
} 
複製代碼

因爲鏈表中從頭部至尾部的節點,訪問時間由晚至早,因此尾部節點和now時刻的時間間隔較大,從尾節點開始刪除。ageLimit的默認值是DBL_MAX,若是不人爲指定ageLimit,則鏈表中節點不會被刪除。

當某個變量在出了本身的做用域以後,正常狀況下就會被自動釋放。

存儲數據
調用setObject: forKey:方法存儲緩存數據,代碼以下:

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost { 
    if (!key) return; 
    if (!object) { 
        [self removeObjectForKey:key]; 
        return; 
    } 
    pthread_mutex_lock(&;_lock); //上鎖 
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //從字典中取節點 
    NSTimeInterval now = CACurrentMediaTime(); 
    if (node) { //若是能取到,說明鏈表中以前存在key對應的緩存數據 
        //更新totalCost 
        _lru->_totalCost -= node->_cost; 
        _lru->_totalCost += cost; 
        node->_cost = cost; 
        node->_time = now; //更新節點的訪問時間 
        node->_value = object; //更新節點中存放的緩存數據 
        [_lru bringNodeToHead:node]; //將節點移至鏈表頭部 
    } else { //若是不能取到,說明鏈表中以前不存在key對應的緩存數據 
        node = [_YYLinkedMapNode new]; //建立新的節點 
        node->_cost = cost; 
        node->_time = now; //設置節點的時間 
        node->_key = key; //設置節點的key 
        node->_value = object; //設置節點中存放的緩存數據 
        [_lru insertNodeAtHead:node]; //將新的節點加入鏈表頭部 
    } 
    if (_lru->_totalCost > _costLimit) { 
        dispatch_async(_queue, ^{ 
            [self trimToCost:_costLimit]; 
        }); 
    } 
    if (_lru->_totalCount > _countLimit) { 
        _YYLinkedMapNode *node = [_lru removeTailNode]; 
    ... 
    } 
    pthread_mutex_unlock(&;_lock); //解鎖 
} 
複製代碼

首先判斷key和object是否爲空,object若是爲空,刪除緩存中key對應的數據。而後從字典中查找key對應的緩存數據,分爲兩種狀況,若是訪問到節點,說明緩存數據存在,則根據最近最少使用原則,將本次操做的節點移動至鏈表的頭部,同時更新節點的訪問時間。若是訪問不到節點,說明是第一次添加key和數據,須要建立一個新的節點,把節點存入字典中,而且加入鏈表頭部。cost是指定的,默認是0。

訪問數據
調用objectForKey:方法訪問緩存數據,代碼註釋以下:

- (id)objectForKey:(id)key { 
    if (!key) return nil; 
    pthread_mutex_lock(&;_lock); 
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //從字典中讀取key相應的節點 
    if (node) { 
        node->_time = CACurrentMediaTime(); //更新節點訪問時間 
        [_lru bringNodeToHead:node]; //將節點移動至鏈表頭部 
    } 
    pthread_mutex_unlock(&;_lock); 
    return node ? node->_value : nil; 
} 
複製代碼

該方法從字典中獲取緩存數據,若是key對應的數據存在,則更新訪問時間,根據最近最少使用原則,將本次操做的節點移動至鏈表的頭部。若是不存在,則直接返回nil。
線程同步

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost { 
    pthread_mutex_lock(&;_lock); 
    //操做鏈表,寫緩存數據 
    pthread_mutex_unlock(&;_lock); 
} 
- (id)objectForKey:(id)key { 
    pthread_mutex_lock(&;_lock); 
    //訪問緩存數據 
    pthread_mutex_unlock(&;_lock); 
} 
複製代碼

若是存在線程A和B,線程A在寫緩存的時候,上鎖,線程B讀取緩存數據時,被阻塞,須要等到線程A執行完寫緩存的操做,調用pthread_mutex_unlock後,線程B才能讀緩存數據,這個時候新的緩存數據已經寫完,保證了操做的數據的同步。

總結
YYMemoryCache操做了內存緩存,相較於硬盤緩存須要進行I/O操做,在性能上快不少,所以YYCache訪問緩存時,優先用的是YYMemoryCache。
YYMemoryCache實際上就是建立了一個對象實例,該對象內部使用字典和雙向鏈表實現

YYDiskCache

YYDiskCache經過文件和SQLite數據庫兩種方式存儲緩存數據.YYKVStorage核心功能類,實現了文件讀寫和數據庫讀寫的功能。YYKVStorageYYKVStorage定義了讀寫緩存數據的三種枚舉類型,即typedefNS_ENUM(NSUInteger,YYKVStorageType)

YYKVStorage
YYKVStorage最核心的思想是KV這兩個字母,表示key-value的意思,目的是讓使用者像使用字典同樣操做數據
YYKVStorage讓咱們只關心3件事:

  • 數據保存的路徑
  • 保存數據,併爲該數據關聯一個key
  • 根據key取出數據或刪除數據

同理,YYKVStorage在設計接口的時候,也從這3個方面進行了考慮。這數據功能設計層面的思想。
YYKVStorage定義了讀寫緩存數據的三種枚舉類型,即

typedef NS_ENUM(NSUInteger, YYKVStorageType) { 
//文件讀取 
YYKVStorageTypeFile = 0, 
//數據庫讀寫 
YYKVStorageTypeSQLite = 1, 
//根據策略決定使用文件仍是數據庫讀寫數據 
YYKVStorageTypeMixed = 2, 
}; 
複製代碼

初始化
調用initWithPath: type:方法進行初始化,指定了存儲方式,建立了緩存文件夾和SQLite數據庫用於存放緩存,打開並初始化數據庫。下面是部分代碼註釋:

- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type { 
    ... 
    self = [super init]; 
    _path = path.copy; 
    _type = type; //指定存儲方式,是數據庫仍是文件存儲 
    _dataPath = [path stringByAppendingPathComponent:kDataDirectoryName]; //緩存數據的文件路徑 
    _trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName]; //存放垃圾緩存數據的文件路徑 
    _trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL); 
    _dbPath = [path stringByAppendingPathComponent:kDBFileName]; //數據庫路徑 
    _errorLogsEnabled = YES; 
    NSError *error = nil; 
    //建立緩存數據的文件夾和垃圾緩存數據的文件夾 
    if (![[NSFileManager defaultManager] createDirectoryAtPath:path 
    withIntermediateDirectories:YES 
    attributes:nil 
    error:&;error] || 
    ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kDataDirectoryName] 
    withIntermediateDirectories:YES 
    attributes:nil 
    error:&;error] || 
    ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kTrashDirectoryName] 
    withIntermediateDirectories:YES 
    attributes:nil 
    error:&;error]) { 
        NSLog(@"YYKVStorage init error:%@", error); 
        return nil; 
    } 
    //建立並打開數據庫、在數據庫中建表 
    //_dbOpen方法建立和打開數據庫manifest.sqlite
    //調用_dbInitialize方法建立數據庫中的表
    if (![self _dbOpen] || ![self _dbInitialize]) { 
        // db file may broken... 
        [self _dbClose]; 
        [self _reset]; // rebuild 
        if (![self _dbOpen] || ![self _dbInitialize]) { 
            [self _dbClose]; 
            NSLog(@"YYKVStorage init error: fail to open sqlite db."); 
            return nil; 
        } 
    } 
    //調用_fileEmptyTrashInBackground方法將trash目錄中的緩存數據刪除
    [self _fileEmptyTrashInBackground]; // empty the trash if failed at last time 
    return self; 
} 
複製代碼

_dbInitialize方法調用sql語句在數據庫中建立一張表,代碼以下:

- (BOOL)_dbInitialize { 
    NSString *sql = @"pragma journal_mode = wal; pragma synchronous = normal; create table if not exists manifest (key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key)); create index if not exists last_access_time_idx on manifest(last_access_time);"; 
    return [self _dbExecute:sql]; 
} 
複製代碼

"pragma journal_mode = wal"表示使用WAL模式進行數據庫操做,若是不指定,默認DELETE模式,是"journal_mode=DELETE"。使用WAL模式時,改寫操做數據庫的操做會先寫入WAL文件,而暫時不改動數據庫文件,當執行checkPoint方法時,WAL文件的內容被批量寫入數據庫。checkPoint操做會自動執行,也能夠改成手動。WAL模式的優勢是支持讀寫併發,性能更高,可是當wal文件很大時,須要調用checkPoint方法清空wal文件中的內容
dataPath和trashPath用於文件的方式讀寫緩存數據,當dataPath中的部分緩存數據須要被清除時,先將其移至trashPath中,而後統一清空trashPath中的數據,相似回收站的思路。_dbPath是數據庫文件,須要建立並初始化,下面是路徑: 在真實的編程中,每每須要把數據封裝成一個對象:


調用_dbOpen方法建立和打開數據庫manifest.sqlite,調用_dbInitialize方法建立數據庫中的表。調用_fileEmptyTrashInBackground方法將trash目錄中的緩存數據刪除

/**
 YYKVStorageItem is used by `YYKVStorage` to store key-value pair and meta data.
 Typically, you should not use this class directly.
 */
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key;                 //緩存數據的key 
@property (nonatomic, strong) NSData *value;                 //緩存數據的value 
@property (nullable, nonatomic, strong) NSString *filename;  //緩存文件名(文件緩存時有用) 
@property (nonatomic) int size;                              //數據大小 
@property (nonatomic) int modTime;                           //數據修改時間(用於更新相同key的緩存) 
@property (nonatomic) int accessTime;                        //數據訪問時間 
@property (nullable, nonatomic, strong) NSData *extendedData; //附加數據 
@end
複製代碼

緩存數據是按一條記錄的格式存入數據庫的,這條SQL記錄包含的字段以下:
key(鍵)、fileName(文件名)、size(大小)、inline_data(value/二進制數據)、modification_time(修改時間)、last_access_time(最後訪問時間)、extended_data(附加數據)
**寫入緩存數據 **

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData { 
    if (key.length == 0 || value.length == 0) return NO; 
    if (_type == YYKVStorageTypeFile && filename.length == 0) { 
        return NO; 
    } 
    //若是有文件名,說明須要寫入文件中 
    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]; 
    } 
} 
複製代碼

該方法首先判斷fileName即文件名是否爲空,若是存在,則調用_fileWriteWithName方法將緩存的數據寫入文件系統中,同時將數據寫入數據庫,須要注意的是,調用_dbSaveWithKey:value:fileName:extendedData:方法會建立一條SQL記錄寫入表中
代碼註釋以下:

- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData { 
    //構建sql語句,將一條記錄添加進manifest表 
    NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);"; 
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; //準備sql語句,返回stmt指針 
    if (!stmt) return NO; 
    int timestamp = (int)time(NULL); 
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //綁定參數值對應"?1" 
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); //綁定參數值對應"?2" 
    sqlite3_bind_int(stmt, 3, (int)value.length); 
    if (fileName.length == 0) { //若是fileName不存在,綁定參數值value.bytes對應"?4" 
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0); 
    } else { //若是fileName存在,不綁定,"?4"對應的參數值爲null 
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0); 
    } 
    sqlite3_bind_int(stmt, 5, timestamp); //綁定參數值對應"?5" 
    sqlite3_bind_int(stmt, 6, timestamp); //綁定參數值對應"?6" 
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); //綁定參數值對應"?7" 
    int result = sqlite3_step(stmt); //開始執行sql語句 
    if (result != SQLITE_DONE) { 
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); 
        return NO; 
    } 
    return YES; 
} 
複製代碼

該方法首先建立sql語句,value括號中的參數"?"表示參數須要經過變量綁定,"?"後面的數字表示綁定變量對應的索引號,若是VALUES (?1, ?1, ?2),則能夠用同一個值綁定多個變量

而後調用_dbPrepareStmt方法構建數據位置指針stmt,標記查詢到的數據位置,sqlite3_prepare_v2()方法進行數據庫操做的準備工做,第一個參數爲成功打開的數據庫指針db,第二個參數爲要執行的sql語句,第三個參數爲stmt指針的地址,這個方法也會返回一個int值,做爲標記狀態是否成功

接着調用sqlite3_bind_text()方法將實際值做爲變量綁定sql中的"?"參數,序號對應"?"後面對應的數字。不一樣類型的變量調用不一樣的方法,例如二進制數據是sqlite3_bind_blob方法

同時判斷若是fileName存在,則生成的sql語句只綁定數據的相關描述,不綁定inline_data,即實際存儲的二進制數據,由於該緩存以前已經將二進制數據寫進文件。這樣作能夠防止緩存數據同時寫入文件和數據庫,形成緩存空間的浪費。若是fileName不存在,則只寫入數據庫中,這時sql語句綁定inline_data,不綁定fileName

最後執行sqlite3_step方法執行sql語句,對stmt指針進行移動,並返回一個int值。

刪除緩存數據

removeItemForKey:方法
該方法刪除指定key對應的緩存數據,區分type,若是是YYKVStorageTypeSQLite,調用_dbDeleteItemWithKey:從數據庫中刪除對應key的緩存記錄,以下:

- (BOOL)_dbDeleteItemWithKey:(NSString *)key { 
    NSString *sql = @"delete from manifest where key = ?1;"; //sql語句 
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; //準備stmt 
    if (!stmt) return NO; 
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //綁定參數 
    int result = sqlite3_step(stmt); //執行sql語句 
    ... 
    return YES; 
} 
複製代碼

若是是YYKVStorageTypeFile或者YYKVStorageTypeMixed,說明可能緩存數據以前可能被寫入文件中,判斷方法是調用_dbGetFilenameWithKey:方法從數據庫中查找key對應的SQL記錄的fileName字段。該方法的流程和上面的方法差很少,只是sql語句換成了select查詢語句。若是查詢到fileName,說明數據以前寫入過文件中,調用_fileDeleteWithName方法刪除數據,同時刪除數據庫中的記錄。不然只從數據庫中刪除SQL記錄

removeItemForKeys:方法

該方法和上一個方法相似,刪除一組key對應的緩存數據,一樣區分type,對於YYKVStorageTypeSQLite,調用_dbDeleteItemWithKeys:方法指定sql語句刪除一組記錄,以下:

- (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys { 
    if (![self _dbCheck]) return NO; 
    //構建sql語句 
    NSString *sql = [NSString stringWithFormat:@"delete from manifest where key in (%@);", [self _dbJoinedKeys:keys]]; 
    sqlite3_stmt *stmt = NULL; 
    int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &;stmt, NULL); 
    ... 
    //綁定變量 
    [self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1]; 
    result = sqlite3_step(stmt); //執行參數 
    sqlite3_finalize(stmt); //對stmt指針進行關閉 
    ... 
    return YES; 
}
複製代碼

其中_dbJoinedKeys:方法是拼裝,?,?,?格式,_dbBindJoinedKeys:stmt:fromIndex:方法綁定變量和參數,若是?後面沒有參數,則sqlite3_bind_text方法的第二個參數,索引值依次對應sql後面的"?"

若是是YYKVStorageTypeFile或者YYKVStorageTypeMixed,經過_dbGetFilenameWithKeys:方法返回一組fileName,根據每個fileName刪除文件中的緩存數據,同時刪除數據庫中的記錄,不然只從數據庫中刪除SQL記錄

removeItemsLargerThanSize:方法刪除那些size大於指定size的緩存數據。一樣是區分type,刪除的邏輯也和上面的方法一致。_dbDeleteItemsWithSizeLargerThan方法除了sql語句不一樣,操做數據庫的步驟相同。_dbCheckpoint方法調用sqlite3_wal_checkpoint方法進行checkpoint操做,將數據同步到數據庫中

讀取緩存數據

getItemValueForKey:方法

該方法經過key訪問緩存數據value,區分type,若是是YYKVStorageTypeSQLite,調用_dbGetValueWithKey:方法從數據庫中查詢key對應的記錄中的inline_data。若是是YYKVStorageTypeFile,首先調用_dbGetFilenameWithKey:方法從數據庫中查詢key對應的記錄中的filename,根據filename從文件中刪除對應緩存數據。若是是YYKVStorageTypeMixed,一樣先獲取filename,根據filename是否存在選擇用相應的方式訪問。代碼註釋以下:

- (NSData *)getItemValueForKey:(NSString *)key { 
    if (key.length == 0) return nil; 
    NSData *value = nil; 
    switch (_type) { 
        case YYKVStorageTypeFile: 
        { 
            NSString *filename = [self _dbGetFilenameWithKey:key]; //從數據庫中查找filename 
            if (filename) { 
                value = [self _fileReadWithName:filename]; //根據filename讀取數據 
                if (!value) { 
                    [self _dbDeleteItemWithKey:key]; //若是沒有讀取到緩存數據,從數據庫中刪除記錄,保持數據同步 
                    value = nil; 
                } 
            } 
        } 
        break; 
        case YYKVStorageTypeSQLite: 
        { 
            value = [self _dbGetValueWithKey:key]; //直接從數據中取inline_data 
        } 
        break; 
        case YYKVStorageTypeMixed: { 
            NSString *filename = [self _dbGetFilenameWithKey:key]; //從數據庫中查找filename 
            if (filename) { 
                value = [self _fileReadWithName:filename]; //根據filename讀取數據 
                if (!value) { 
                    [self _dbDeleteItemWithKey:key]; //保持數據同步 
                    value = nil; 
                } 
            } else { 
                value = [self _dbGetValueWithKey:key]; //直接從數據中取inline_data 
            } 
        } 
        break; 
    } 
    if (value) { 
        [self _dbUpdateAccessTimeWithKey:key]; //更新訪問時間 
    } 
    return value; 
} 
複製代碼

調用方法用於更新該數據的訪問時間,即sql記錄中的last_access_time字段。

getItemForKey:方法
該方法經過key訪問數據,返回YYKVStorageItem封裝的緩存數據。首先調用_dbGetItemWithKey:excludeInlineData:從數據庫中查詢,下面是代碼註釋:

- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData { 
    //查詢sql語句,是否排除inline_data 
    NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;"; 
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; //準備工做,構建stmt 
    if (!stmt) return nil; 
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //綁定參數 
    YYKVStorageItem *item = nil; 
    int result = sqlite3_step(stmt); //執行sql語句 
    if (result == SQLITE_ROW) { 
        item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData]; //取出查詢記錄中的各個字段,用YYKVStorageItem封裝並返回 
    } else { 
        if (result != SQLITE_DONE) { 
            if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); 
        } 
    } 
    return item; 
} 
複製代碼

sql語句是查詢符合key值的記錄中的各個字段,例如緩存的key、大小、二進制數據、訪問時間等信息, excludeInlineData表示查詢數據時,是否要排除inline_data字段,便是否查詢二進制數據,執行sql語句後,經過stmt指針和_dbGetItemFromStmt:excludeInlineData:方法取出各個字段,並建立YYKVStorageItem對象,將記錄的各個字段賦值給各個屬性,代碼註釋以下:

- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData { 
    int i = 0; 
    char *key = (char *)sqlite3_column_text(stmt, i++); //key 
    char *filename = (char *)sqlite3_column_text(stmt, i++); //filename 
    int size = sqlite3_column_int(stmt, i++); //數據大小 
    const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i); //二進制數據 
    int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++); 
    int modification_time = sqlite3_column_int(stmt, i++); //修改時間 
    int last_access_time = sqlite3_column_int(stmt, i++); //訪問時間 
    const void *extended_data = sqlite3_column_blob(stmt, i); //附加數據 
    int extended_data_bytes = sqlite3_column_bytes(stmt, i++); 
    //用YYKVStorageItem對象封裝 
    YYKVStorageItem *item = [YYKVStorageItem new]; 
    if (key) item.key = [NSString stringWithUTF8String:key]; 
    if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename]; 
    item.size = size; 
    if (inline_data_bytes > 0 &;&; inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes]; 
    item.modTime = modification_time; 
    item.accessTime = last_access_time; 
    if (extended_data_bytes > 0 &;&; extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes]; 
    return item; //返回YYKVStorageItem對象 
} 
複製代碼

最後取出YYKVStorageItem對象後,判斷filename屬性是否存在,若是存在說明緩存的二進制數據寫進了文件中,此時返回的YYKVStorageItem對象的value屬性是nil,須要調用_fileReadWithName:方法從文件中讀取數據,並賦值給YYKVStorageItem的value屬性。代碼註釋以下:

- (YYKVStorageItem *)getItemForKey:(NSString *)key { 
    if (key.length == 0) return nil; 
    //從數據庫中查詢記錄,返回YYKVStorageItem對象,封裝了緩存數據的信息 
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO]; 
    if (item) { 
        [self _dbUpdateAccessTimeWithKey:key]; //更新訪問時間 
        if (item.filename) { //filename存在,按照item.value從文件中讀取 
            item.value = [self _fileReadWithName:item.filename]; 
        ... 
        } 
    } 
    return item; 
} 
複製代碼

getItemForKeys:方法

返回一組YYKVStorageItem對象信息,調用_dbGetItemWithKeys:excludeInlineData:方法獲取一組YYKVStorageItem對象。訪問邏輯和getItemForKey:方法相似,sql語句的查詢條件改成多個key匹配。

getItemValueForKeys:方法

返回一組緩存數據,調用getItemForKeys:方法獲取一組YYKVStorageItem對象後,取出其中的value,存入一個臨時字典對象後返回。

YYDiskCache
YYDiskCache是上層調用YYKVStorage的類,對外提供了存、刪、查、邊界控制的方法。內部維護了三個變量,以下:

@implementation YYDiskCache { 
    YYKVStorage *_kv; 
    dispatch_semaphore_t _lock; 
    dispatch_queue_t _queue; 
} 
複製代碼

_kv用於緩存數據,_lock是信號量變量,用於多線程訪問數據時的同步操做。

初始化方法
nitWithPath:inlineThreshold:方法用於初始化,下面是代碼註釋:

- (instancetype)initWithPath:(NSString *)path 
inlineThreshold:(NSUInteger)threshold { 
    ... 
    YYKVStorageType type; 
    if (threshold == 0) { 
        type = YYKVStorageTypeFile; 
    } else if (threshold == NSUIntegerMax) { 
        type = YYKVStorageTypeSQLite; 
    } else { 
        type = YYKVStorageTypeMixed; 
    } 
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type]; 
    if (!kv) return nil; 
    _kv = kv; 
    _path = path; 
    _lock = dispatch_semaphore_create(1); 
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT); 
    _inlineThreshold = threshold; 
    _countLimit = NSUIntegerMax; 
    _costLimit = NSUIntegerMax; 
    _ageLimit = DBL_MAX; 
    _freeDiskSpaceLimit = 0; 
    _autoTrimInterval = 60; 
    [self _trimRecursively]; 
    ... 
    return self; 
}
複製代碼

根據threshold參數決定緩存的type,默認threshold是20KB,會選擇YYKVStorageTypeMixed方式,即根據緩存數據的size進一步決定。而後初始化YYKVStorage對象,信號量、各類limit參數。

寫緩存
setObject:forKey:方法存儲數據,首先判斷type,若是是YYKVStorageTypeSQLite,則直接將數據存入數據庫中,filename傳nil,若是是YYKVStorageTypeFile或者YYKVStorageTypeMixed,則判斷要存儲的數據的大小,若是超過threshold(默認20KB),則須要將數據寫入文件,並經過key生成filename。YYCache的做者認爲當數據代銷超過20KB時,寫入文件速度更快。代碼註釋以下:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key { 
... 
    value = [NSKeyedArchiver archivedDataWithRootObject:object]; //序列化 
    ... 
    NSString *filename = nil; 
    if (_kv.type != YYKVStorageTypeSQLite) { 
        if (value.length > _inlineThreshold) { //value大於閾值,用文件方式存儲value 
            filename = [self _filenameForKey:key]; 
        } 
    } 
    Lock(); 
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData]; //filename存在,數據庫中不寫入value,即inline_data字段爲空 
    Unlock(); 
} 
//讀緩存 
objectForKey:方法調用YYKVStorage對象的getItemForKey:方法讀取數據,返回YYKVStorageItem對象,取出value屬性,進行反序列化。
//刪除緩存 
removeObjectForKey:方法調用YYKVStorage對象的removeItemForKey:方法刪除緩存數據
複製代碼

邊界控制

在前一篇文章中,YYMemoryCache實現了內存緩存的LRU算法,YYDiskCache也試了LRU算法,在初始化的時候調用_trimRecursively方法每一個必定時間檢測一下緩存數據大小是否超過容量。

數據同步

YYMemoryCache使用了互斥鎖來實現多線程訪問數據的同步性,YYDiskCache使用了信號量來實現,下面是兩個宏:

#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER) 
#define Unlock() dispatch_semaphore_signal(self->_lock) 
複製代碼

讀寫緩存數據的方法中都調用了宏:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key 
{ 
... 
    Lock(); 
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData]; 
    Unlock(); 
} 

- (id<NSCoding>)objectForKey:(NSString *)key { 
    Lock(); 
    YYKVStorageItem *item = [_kv getItemForKey:key]; 
    Unlock(); 
    ... 
} 
複製代碼

初始化方法建立信號量,dispatch_semaphore_create(1),值是1。當線程調用寫緩存的方法時,調用dispatch_semaphore_wait方法使信號量-1。同時線程B在讀緩存時,因爲信號量爲0,遇到dispatch_semaphore_wait方法時會被阻塞。直到線程A寫完數據時,調用dispatch_semaphore_signal方法時,信號量+1,線程B繼續執行,讀取數據。關於iOS中各類互斥鎖性能的對比。
咱們看一些接口設計方面的內容:

#pragma mark - Attribute
///=============================================================================
/// @name Attribute
///=============================================================================

@property (nonatomic, readonly) NSString *path;        ///< The path of this storage.
@property (nonatomic, readonly) YYKVStorageType type;  ///< The type of this storage.
@property (nonatomic) BOOL errorLogsEnabled;           ///< Set `YES` to enable error logs for debug.

#pragma mark - Initializer
///=============================================================================
/// @name Initializer
///=============================================================================
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;

/**
 The designated initializer. 
 
 @param path  Full path of a directory in which the storage will write data. If
    the directory is not exists, it will try to create one, otherwise it will 
    read the data in this directory.
 @param type  The storage type. After first initialized you should not change the 
    type of the specified path.
 @return  A new storage object, or nil if an error occurs.
 @warning Multiple instances with the same path will make the storage unstable.
 */
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
複製代碼

接口中的屬性都是很重要的信息,咱們應該儘可能利用好它的讀寫屬性,儘可能設計成只讀屬性。默認狀況下,不是隻讀的,都很容易讓其餘開發者認爲,該屬性是能夠設置的。

對於初始化方法而言,若是某個類須要提供一個指定的初始化方法,那麼就要使用NS_DESIGNATED_INITIALIZER給予提示。同時使用UNAVAILABLE_ATTRIBUTE禁用掉默認的方法。接下來要重寫禁用的初始化方法,在其內部拋出異常:

- (instancetype)init {
    @throw [NSException exceptionWithName:@"YYKVStorage init error" reason:@"Please use the designated initializer and pass the 'path' and 'type'." userInfo:nil];
    return [self initWithPath:@"" type:YYKVStorageTypeFile];
}
複製代碼

上邊的代碼你們能夠直接拿來用,千萬不要怕程序拋出異常,在發佈以前,可以發現潛在的問題是一件好事。使用了上邊的一個小技巧後呢,編碼水平是否是有所提高?
再給你們簡單分析分析下邊同樣代碼:

- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
複製代碼

上邊咱們關心的是nullable關鍵字,表示可能爲空,與之對應的是nonnull,表示不爲空。能夠說,他們都跟swift有關係,swift中屬性或參數是否爲空都有嚴格的要求。所以咱們在設計屬性,參數,返回值等等的時候,要考慮這些可能爲空的狀況。

// 設置中間的內容默認都是nonnull
NS_ASSUME_NONNULL_BEGIN
NS_ASSUME_NONNULL_END
複製代碼

總結 YYCache庫的分析到此爲止,其中有許多代碼值得學習。例如二級緩存的思想,LRU的實現,SQLite的WAL機制。文中許多地方的分析和思路,表達的不是很準確和清楚,但願經過從此的學習和練習,提高本身的水平,總之路漫漫其修遠兮...

相關文章
相關標籤/搜索