iOS源碼解析—YYCache(YYDiskCache)

概述

上一篇主要講解了YYMemoryCache的文件結(jié)構(gòu),分析了YYMemoryCache類的相關(guān)方法,本章主要分析硬盤緩存類YYDiskCache。YYDiskCache通過文件和SQLite數(shù)據(jù)庫兩種方式存儲緩存數(shù)據(jù)。YYKVStorage核心功能類,實現(xiàn)了文件讀寫和數(shù)據(jù)庫讀寫的功能。

YYKVStorage

YYKVStorage定義了讀寫緩存數(shù)據(jù)的三種枚舉類型,即

typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    //文件讀取
    YYKVStorageTypeFile = 0,
    //數(shù)據(jù)庫讀寫
    YYKVStorageTypeSQLite = 1,
    //根據(jù)策略決定使用文件還是數(shù)據(jù)庫讀寫數(shù)據(jù)
    YYKVStorageTypeMixed = 2,
};

由于讀寫數(shù)據(jù)的方式不同,YYKVStorage分別實現(xiàn)了數(shù)據(jù)庫和文件的讀寫方式,下面分析主要方法。

初始化

調(diào)用initWithPath: type:方法進行初始化,指定了存儲方式,創(chuàng)建了緩存文件夾和SQLite數(shù)據(jù)庫用于存放緩存,打開并初始化數(shù)據(jù)庫。下面是部分代碼注釋:

- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {
    ...
    self = [super init];
    _path = path.copy;
    _type = type; //指定存儲方式,是數(shù)據(jù)庫還是文件存儲
    _dataPath = [path stringByAppendingPathComponent:kDataDirectoryName]; //緩存數(shù)據(jù)的文件路徑
    _trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName]; //存放垃圾緩存數(shù)據(jù)的文件路徑
    _trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL);
    _dbPath = [path stringByAppendingPathComponent:kDBFileName]; //數(shù)據(jù)庫路徑
    _errorLogsEnabled = YES;
    NSError *error = nil;
    //創(chuàng)建緩存數(shù)據(jù)的文件夾和垃圾緩存數(shù)據(jù)的文件夾
    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;
    }
    //創(chuàng)建并打開數(shù)據(jù)庫、在數(shù)據(jù)庫中建表
    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;
        }
    }
    [self _fileEmptyTrashInBackground]; // empty the trash if failed at last time
    return self;
}

_dbInitialize方法調(diào)用sql語句在數(shù)據(jù)庫中創(chuàng)建一張表,代碼如下:

- (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模式進行數(shù)據(jù)庫操作,如果不指定,默認DELETE模式,是"journal_mode=DELETE"。使用WAL模式時,改寫操作數(shù)據(jù)庫的操作會先寫入WAL文件,而暫時不改動數(shù)據(jù)庫文件,當執(zhí)行checkPoint方法時,WAL文件的內(nèi)容被批量寫入數(shù)據(jù)庫。checkPoint操作會自動執(zhí)行,也可以改為手動。WAL模式的優(yōu)點是支持讀寫并發(fā),性能更高,但是當wal文件很大時,需要調(diào)用checkPoint方法清空wal文件中的內(nèi)容。關(guān)于WAL模式,可以參考這篇文章。

dataPath和trashPath用于文件的方式讀寫緩存數(shù)據(jù),當dataPath中的部分緩存數(shù)據(jù)需要被清除時,先將其移至trashPath中,然后統(tǒng)一清空trashPath中的數(shù)據(jù),類似回收站的思路。_dbPath是數(shù)據(jù)庫文件,需要創(chuàng)建并初始化,下面是路徑:

3-1.png

調(diào)用_dbOpen方法創(chuàng)建和打開數(shù)據(jù)庫manifest.sqlite,調(diào)用_dbInitialize方法創(chuàng)建數(shù)據(jù)庫中的表。調(diào)用_fileEmptyTrashInBackground方法將trash目錄中的緩存數(shù)據(jù)刪除。

YYKVStorageItem

YYKVStorageItem封裝了每次寫入硬盤的數(shù)據(jù),代碼如下:

@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; //緩存數(shù)據(jù)的key
@property (nonatomic, strong) NSData *value; //緩存數(shù)據(jù)的value
@property (nullable, nonatomic, strong) NSString *filename; //緩存文件名(文件緩存時有用)
@property (nonatomic) int size; //數(shù)據(jù)大小
@property (nonatomic) int modTime; //數(shù)據(jù)修改時間(用于更新相同key的緩存)
@property (nonatomic) int accessTime; //數(shù)據(jù)訪問時間
@property (nullable, nonatomic, strong) NSData *extendedData; //附加數(shù)據(jù)
@end

緩存數(shù)據(jù)是按一條記錄的格式存入數(shù)據(jù)庫的,這條SQL記錄包含的字段如下:

key(鍵)、fileName(文件名)、size(大?。nline_data(value/二進制數(shù)據(jù))、modification_time(修改時間)、last_access_time(最后訪問時間)、extended_data(附加數(shù)據(jù))

描述了這條緩存數(shù)據(jù)的相關(guān)信息,對應(yīng)YYKVStorageItem對象的各個屬性。

寫入緩存數(shù)據(jù)

通過saveItemWithKey: value: filename: extendedData:方法將緩存數(shù)據(jù)寫入硬盤,代碼注釋如下:

- (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]) { //寫數(shù)據(jù)進文件
            return NO;
        }
        //寫文件進數(shù)據(jù)庫
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename]; //寫失敗,同時刪除文件中的數(shù)據(jù)
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key]; //從文件中刪除緩存
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        //寫入數(shù)據(jù)庫
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

該方法首先判斷fileName即文件名是否為空,如果存在,則調(diào)用_fileWriteWithName方法將緩存的數(shù)據(jù)寫入文件系統(tǒng)中,同時將數(shù)據(jù)寫入數(shù)據(jù)庫,需要注意的是,調(diào)用_dbSaveWithKey:value:fileName:extendedData:方法會創(chuàng)建一條SQL記錄寫入表中,

代碼注釋如下:

- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    //構(gòu)建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); //綁定參數(shù)值對應(yīng)"?1"
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); //綁定參數(shù)值對應(yīng)"?2"
    sqlite3_bind_int(stmt, 3, (int)value.length);
    if (fileName.length == 0) { //如果fileName不存在,綁定參數(shù)值value.bytes對應(yīng)"?4"
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else { //如果fileName存在,不綁定,"?4"對應(yīng)的參數(shù)值為null
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    sqlite3_bind_int(stmt, 5, timestamp); //綁定參數(shù)值對應(yīng)"?5"
    sqlite3_bind_int(stmt, 6, timestamp); //綁定參數(shù)值對應(yīng)"?6"
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); //綁定參數(shù)值對應(yīng)"?7"
    
    int result = sqlite3_step(stmt); //開始執(zhí)行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;
}

該方法首先創(chuàng)建sql語句,value括號中的參數(shù)"?"表示參數(shù)需要通過變量綁定,"?"后面的數(shù)字表示綁定變量對應(yīng)的索引號,如果VALUES (?1, ?1, ?2),則可以用同一個值綁定多個變量。

然后調(diào)用_dbPrepareStmt方法構(gòu)建數(shù)據(jù)位置指針stmt,標記查詢到的數(shù)據(jù)位置,sqlite3_prepare_v2()方法進行數(shù)據(jù)庫操作的準備工作,第一個參數(shù)為成功打開的數(shù)據(jù)庫指針db,第二個參數(shù)為要執(zhí)行的sql語句,第三個參數(shù)為stmt指針的地址,這個方法也會返回一個int值,作為標記狀態(tài)是否成功。

接著調(diào)用sqlite3_bind_text()方法將實際值作為變量綁定sql中的"?"參數(shù),序號對應(yīng)"?"后面對應(yīng)的數(shù)字。不同類型的變量調(diào)用不同的方法,例如二進制數(shù)據(jù)是sqlite3_bind_blob方法。

同時判斷如果fileName存在,則生成的sql語句只綁定數(shù)據(jù)的相關(guān)描述,不綁定inline_data,即實際存儲的二進制數(shù)據(jù),因為該緩存之前已經(jīng)將二進制數(shù)據(jù)寫進文件。這樣做可以防止緩存數(shù)據(jù)同時寫入文件和數(shù)據(jù)庫,造成緩存空間的浪費。如果fileName不存在,則只寫入數(shù)據(jù)庫中,這時sql語句綁定inline_data,不綁定fileName。

最后執(zhí)行sqlite3_step方法執(zhí)行sql語句,對stmt指針進行移動,并返回一個int值。

刪除緩存數(shù)據(jù)
  1. removeItemForKey:方法

    該方法刪除指定key對應(yīng)的緩存數(shù)據(jù),區(qū)分type,如果是YYKVStorageTypeSQLite,調(diào)用_dbDeleteItemWithKey:從數(shù)據(jù)庫中刪除對應(yīng)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); //綁定參數(shù)
        int result = sqlite3_step(stmt); //執(zhí)行sql語句
        ...
        return YES;
    }
    

    如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,說明可能緩存數(shù)據(jù)之前可能被寫入文件中,判斷方法是調(diào)用_dbGetFilenameWithKey:方法從數(shù)據(jù)庫中查找key對應(yīng)的SQL記錄的fileName字段。該方法的流程和上面的方法差不多,只是sql語句換成了select查詢語句。如果查詢到fileName,說明數(shù)據(jù)之前寫入過文件中,調(diào)用_fileDeleteWithName方法刪除數(shù)據(jù),同時刪除數(shù)據(jù)庫中的記錄。否則只從數(shù)據(jù)庫中刪除SQL記錄。

  2. removeItemForKeys:方法

    該方法和上一個方法類似,刪除一組key對應(yīng)的緩存數(shù)據(jù),同樣區(qū)分type,對于YYKVStorageTypeSQLite,調(diào)用_dbDeleteItemWithKeys:方法指定sql語句刪除一組記錄,如下:

    - (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys {
        if (![self _dbCheck]) return NO;
         //構(gòu)建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); //執(zhí)行參數(shù)
        sqlite3_finalize(stmt); //對stmt指針進行關(guān)閉
        ...
        return YES;
    }
    

    其中_dbJoinedKeys:方法是拼裝,?,?,?格式,_dbBindJoinedKeys:stmt:fromIndex:方法綁定變量和參數(shù),如果?后面沒有參數(shù),則sqlite3_bind_text方法的第二個參數(shù),索引值依次對應(yīng)sql后面的"?"。

    如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,通過_dbGetFilenameWithKeys:方法返回一組fileName,根據(jù)每一個fileName刪除文件中的緩存數(shù)據(jù),同時刪除數(shù)據(jù)庫中的記錄,否則只從數(shù)據(jù)庫中刪除SQL記錄。

  3. removeItemsLargerThanSize:方法刪除那些size大于指定size的緩存數(shù)據(jù)。同樣是區(qū)分type,刪除的邏輯也和上面的方法一致。_dbDeleteItemsWithSizeLargerThan方法除了sql語句不同,操作數(shù)據(jù)庫的步驟相同。_dbCheckpoint方法調(diào)用sqlite3_wal_checkpoint方法進行checkpoint操作,將數(shù)據(jù)同步到數(shù)據(jù)庫中。

  4. 其余的remove方法也都是根據(jù)一些篩選條件,刪除不符合條件的數(shù)據(jù),調(diào)用不同的sql語句實現(xiàn)這些數(shù)據(jù)庫的操作,不詳細分析了。

讀取緩存數(shù)據(jù)
  1. getItemValueForKey:方法

    該方法通過key訪問緩存數(shù)據(jù)value,區(qū)分type,如果是YYKVStorageTypeFile,調(diào)用_dbGetValueWithKey:方法從數(shù)據(jù)庫中查詢key對應(yīng)的記錄中的inline_data。如果是YYKVStorageTypeFile,首先調(diào)用_dbGetFilenameWithKey:方法從數(shù)據(jù)庫中查詢key對應(yīng)的記錄中的filename,根據(jù)filename從文件中刪除對應(yīng)緩存數(shù)據(jù)。如果是YYKVStorageTypeMixed,同樣先獲取filename,根據(jù)filename是否存在選擇用相應(yīng)的方式訪問。代碼注釋如下:

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

    調(diào)用方法用于更新該數(shù)據(jù)的訪問時間,即sql記錄中的last_access_time字段。

  2. getItemForKey:方法

    該方法通過key訪問數(shù)據(jù),返回YYKVStorageItem封裝的緩存數(shù)據(jù)。首先調(diào)用_dbGetItemWithKey:excludeInlineData:從數(shù)據(jù)庫中查詢,下面是代碼注釋:

    - (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]; //準備工作,構(gòu)建stmt
        if (!stmt) return nil;
        sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //綁定參數(shù)
        
        YYKVStorageItem *item = nil;
        int result = sqlite3_step(stmt); //執(zhí)行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、大小、二進制數(shù)據(jù)、訪問時間等信息, excludeInlineData表示查詢數(shù)據(jù)時,是否要排除inline_data字段,即是否查詢二進制數(shù)據(jù),執(zhí)行sql語句后,通過stmt指針和_dbGetItemFromStmt:excludeInlineData:方法取出各個字段,并創(chuàng)建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++); //數(shù)據(jù)大小
        const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i); //二進制數(shù)據(jù)
        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); //附加數(shù)據(jù)
        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屬性是否存在,如果存在說明緩存的二進制數(shù)據(jù)寫進了文件中,此時返回的YYKVStorageItem對象的value屬性是nil,需要調(diào)用_fileReadWithName:方法從文件中讀取數(shù)據(jù),并賦值給YYKVStorageItem的value屬性。代碼注釋如下:

    - (YYKVStorageItem *)getItemForKey:(NSString *)key {
        if (key.length == 0) return nil;
         //從數(shù)據(jù)庫中查詢記錄,返回YYKVStorageItem對象,封裝了緩存數(shù)據(jù)的信息
        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;
    }
    
  3. getItemForKeys:方法

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

  4. getItemValueForKeys:方法

    返回一組緩存數(shù)據(jù),調(diào)用getItemForKeys:方法獲取一組YYKVStorageItem對象后,取出其中的value,存入一個臨時字典對象后返回。

YYDiskCache

YYDiskCache是上層調(diào)用YYKVStorage的類,對外提供了存、刪、查、邊界控制的方法。內(nèi)部維護了三個變量,如下:

@implementation YYDiskCache {
    YYKVStorage *_kv;
    dispatch_semaphore_t _lock;
    dispatch_queue_t _queue;
}

_kv用于緩存數(shù)據(jù),_lock是信號量變量,用于多線程訪問數(shù)據(jù)時的同步操作。

初始化方法

initWithPath: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;

根據(jù)threshold參數(shù)決定緩存的type,默認threshold是20KB,會選擇YYKVStorageTypeMixed方式,即根據(jù)緩存數(shù)據(jù)的size進一步?jīng)Q定。然后初始化YYKVStorage對象,信號量、各種limit參數(shù)。

寫緩存

setObject:forKey:方法存儲數(shù)據(jù),首先判斷type,如果是YYKVStorageTypeSQLite,則直接將數(shù)據(jù)存入數(shù)據(jù)庫中,filename傳nil,如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,則判斷要存儲的數(shù)據(jù)的大小,如果超過threshold(默認20KB),則需要將數(shù)據(jù)寫入文件,并通過key生成filename。YYCache的作者認為當數(shù)據(jù)代銷超過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存在,數(shù)據(jù)庫中不寫入value,即inline_data字段為空
    Unlock();
}
讀緩存

objectForKey:方法調(diào)用YYKVStorage對象的getItemForKey:方法讀取數(shù)據(jù),返回YYKVStorageItem對象,取出value屬性,進行反序列化。

刪除緩存

removeObjectForKey:方法調(diào)用YYKVStorage對象的removeItemForKey:方法刪除緩存數(shù)據(jù)。

邊界控制

前一篇文章中,YYMemoryCache實現(xiàn)了內(nèi)存緩存的LRU算法,YYDiskCache也試了LRU算法,在初始化的時候調(diào)用_trimRecursively方法每個一定時間檢測一下緩存數(shù)據(jù)大小是否超過容量。

數(shù)據(jù)同步

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

#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

讀寫緩存數(shù)據(jù)的?方法中都調(diào)用了宏:

- (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();
    ...
}

初始化方法創(chuàng)建信號量,dispatch_semaphore_create(1),值是1。當線程調(diào)用寫緩存的方法時,調(diào)用dispatch_semaphore_wait方法使信號量-1。同時線程B在讀緩存時,由于信號量為0,遇到dispatch_semaphore_wait方法時會被阻塞。直到線程A寫完數(shù)據(jù)時,調(diào)用dispatch_semaphore_signal方法時,信號量+1,線程B繼續(xù)執(zhí)行,讀取數(shù)據(jù)。關(guān)于iOS中各種互斥鎖性能的對比,可以參考作者的文章。

總結(jié)

YYCache庫的分析到此為止,其中有許多代碼值得學(xué)習(xí)。例如二級緩存的思想,LRU的實現(xiàn),SQLite的WAL機制。文中許多地方的分析和思路,表達的不是很準確和清楚,希望通過今后的學(xué)習(xí)和練習(xí),提升自己的水平,總之路漫漫其修遠兮...

最后編輯于
?著作權(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)容