本文分析YYDiskCache
->YYKVStorage
實現過程:
YYDiskCache
對YYKVStorage
一層封裝,快取方式:資料庫+檔案,下面先分析主要實現類YYKVStorage
,再分析表層類YYDiskCache
.
YYKVStorage.h方法結構圖
YYKVStorage.h方法解釋
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN // 用YYKVStorageItem儲存快取相關引數 @interface YYKVStorageItem : NSObject // 快取鍵值 @property (nonatomic, strong) NSString *key; // 快取物件 @property (nonatomic, strong) NSData *value; // 快取檔名 @property (nullable, nonatomic, strong) NSString *filename; // 快取大小 @property (nonatomic) int size; // 修改時間 @property (nonatomic) int modTime; // 最後使用時間 @property (nonatomic) int accessTime; // 擴充資料 @property (nullable, nonatomic, strong) NSData *extendedData; @end // 可以指定快取型別 typedef NS_ENUM(NSUInteger, YYKVStorageType) { // 檔案快取(filename != null) YYKVStorageTypeFile = 0, // 資料庫快取 YYKVStorageTypeSQLite = 1, // 如果filename != null,則value用檔案快取,快取的其他引數用資料庫快取;如果filename == null,則用資料庫快取 YYKVStorageTypeMixed = 2, }; // 快取操作實現 @interface YYKVStorage : NSObject #pragma mark - Attribute // 快取路徑 @property (nonatomic, readonly) NSString *path; // 快取方式 @property (nonatomic, readonly) YYKVStorageType type; // 是否要開啟錯誤日誌 @property (nonatomic) BOOL errorLogsEnabled; #pragma mark - Initializer // 這兩個方法不能使用,因為例項化物件時要有初始化path、type - (instancetype)init UNAVAILABLE_ATTRIBUTE; + (instancetype)new UNAVAILABLE_ATTRIBUTE; /** * 例項化物件 * * @param path 快取路徑 * @param type 快取方式 */ - (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER; #pragma mark - Save Items /** * 新增快取 * * @param item 把快取資料封裝到YYKVStorageItem物件 */ - (BOOL)saveItem:(YYKVStorageItem *)item; /** * 新增快取 * * @param key 快取鍵值 * @param value 快取物件 */ - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value; /** * 新增快取 * * @param key 快取鍵值 * @param value 快取物件 * @param filename 快取檔名稱 * filename != null * 則用檔案快取value,並把`key`,`filename`,`extendedData`寫入資料庫 * filename == null * 快取方式type:YYKVStorageTypeFile 不進行快取 * 快取方式type:YYKVStorageTypeSQLite || YYKVStorageTypeMixed 資料庫快取 * @param extendedData 快取擴充資料 */ - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(nullable NSString *)filename extendedData:(nullable NSData *)extendedData; #pragma mark - Remove Items /** * 刪除快取 */ - (BOOL)removeItemForKey:(NSString *)key; - (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys; /** * 刪除所有記憶體開銷大於size的快取 */ - (BOOL)removeItemsLargerThanSize:(int)size; /** * 刪除所有時間比time小的快取 */ - (BOOL)removeItemsEarlierThanTime:(int)time; /** * 減小快取佔的容量開銷,使總快取的容量開銷值不大於maxSize(刪除原則:LRU 最久未使用的快取將先刪除) */ - (BOOL)removeItemsToFitSize:(int)maxSize; /** * 減小總快取數量,使總快取數量不大於maxCount(刪除原則:LRU 最久未使用的快取將先刪除) */ - (BOOL)removeItemsToFitCount:(int)maxCount; /** * 清空所有快取 */ - (BOOL)removeAllItems; - (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress endBlock:(nullable void(^)(BOOL error))end; #pragma mark - Get Items /** * 讀取快取 */ - (nullable YYKVStorageItem *)getItemForKey:(NSString *)key; - (nullable YYKVStorageItem *)getItemInfoForKey:(NSString *)key; - (nullable NSData *)getItemValueForKey:(NSString *)key; - (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys; - (nullable NSArray<YYKVStorageItem *> *)getItemInfoForKeys:(NSArray<NSString *> *)keys; - (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys; #pragma mark - Get Storage Status /** * 判斷當前key是否有對應的快取 */ - (BOOL)itemExistsForKey:(NSString *)key; /** * 獲取快取總數量 */ - (int)getItemsCount; /** * 獲取快取總記憶體開銷 */ - (int)getItemsSize; @end NS_ASSUME_NONNULL_END |
YYKVStorage.m方法結構圖
私有方法:
公開方法:
YYKVStorage.m方法實現
下面分別拎出新增、刪除、查詢各個主要的方法來講解
1.新增
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// 新增快取 - (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) { //** `快取方式為YYKVStorageTypeFile(檔案快取)`並且`未傳快取檔名`則不快取(忽略) ** return NO; } if (filename.length) { //** 存在檔名則用檔案快取,並把`key`,`filename`,`extendedData`寫入資料庫 ** // 快取資料寫入檔案 if (![self _fileWriteWithName:filename data:value]) { return NO; } // 把`key`,`filename`,`extendedData`寫入資料庫,存在filenam,則不把value快取進資料庫 if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) { // 如果資料庫操作失敗,刪除之前的檔案快取 [self _fileDeleteWithName:filename]; return NO; } return YES; } else { if (_type != YYKVStorageTypeSQLite) { // ** 快取方式:非資料庫 ** // 根據快取key查詢快取檔名 NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { // 刪除檔案快取 [self _fileDeleteWithName:filename]; } } // 把快取寫入資料庫 return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData]; } } |
1 2 3 4 5 6 7 |
// 把data寫入檔案 - (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data { // 拼接檔案路徑 NSString *path = [_dataPath stringByAppendingPathComponent:filename]; // 寫入檔案 return [data writeToFile:path atomically:NO]; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// 寫入資料庫 - (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData { // 執行sql語句 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);"; // 所有sql執行前,都必須能run sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; // 時間 int timestamp = (int)time(NULL); // 繫結引數值 sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); sqlite3_bind_int(stmt, 3, (int)value.length); if (fileName.length == 0) { // fileName為null時,快取value sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0); } else { // fileName不為null時,不快取value sqlite3_bind_blob(stmt, 4, NULL, 0, 0); } sqlite3_bind_int(stmt, 5, timestamp); sqlite3_bind_int(stmt, 6, timestamp); sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); // 執行操作 int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { //** 未完成執行資料庫 ** // 輸出錯誤logs if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 將sql編譯成stmt - (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql { if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL; // 從_dbStmtCache字典裡取之前編譯過sql的stmt(優化) sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql)); if (!stmt) { // 將sql編譯成stmt int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { //** 未完成執行資料庫 ** // 輸出錯誤logs if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NULL; } // 將新的stmt快取到字典 CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt); } else { // 重置stmt狀態 sqlite3_reset(stmt); } return stmt; } // 刪除檔案 - (BOOL)_fileDeleteWithName:(NSString *)filename { NSString *path = [_dataPath stringByAppendingPathComponent:filename]; return [[NSFileManager defaultManager] removeItemAtPath:path error:NULL]; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// 從資料庫查詢檔名 - (NSString *)_dbGetFilenameWithKey:(NSString *)key { // 準備執行sql NSString *sql = @"select filename from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; // 繫結引數 sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); // 執行操作 int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { //** 存在可讀的row ** // 取出stmt中的資料 char *filename = (char *)sqlite3_column_text(stmt, 0); if (filename && *filename != 0) { // 轉utf8 string return [NSString stringWithUTF8String:filename]; } } else { if (result != SQLITE_DONE) { //** 未完成執行資料庫 ** // 輸出錯誤logs if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); } } return nil; } |
2.刪除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 刪除快取 - (BOOL)removeItemForKey:(NSString *)key { if (key.length == 0) return NO; // 判斷快取方式 switch (_type) { case YYKVStorageTypeSQLite: { //** 資料庫快取 ** // 刪除資料庫記錄 return [self _dbDeleteItemWithKey:key]; } break; case YYKVStorageTypeFile: case YYKVStorageTypeMixed: { //** 資料庫快取 或 檔案快取 ** // 查詢快取檔名 NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { // 刪除檔案快取 [self _fileDeleteWithName:filename]; } // 刪除資料庫記錄 return [self _dbDeleteItemWithKey:key]; } break; default: return NO; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 刪除資料庫記錄 - (BOOL)_dbDeleteItemWithKey:(NSString *)key { // 準備執行sql NSString *sql = @"delete from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; // 繫結引數 sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); // 執行操作 int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { //** 未完成執行資料庫 ** // 輸出錯誤logs if (_errorLogsEnabled) NSLog(@"%s line:%d db delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } |
3.查詢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 查詢快取 - (YYKVStorageItem *)getItemForKey:(NSString *)key { if (key.length == 0) return nil; // 資料庫查詢 YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO]; if (item) { //** 資料庫存在記錄 ** // 更新操作時間 [self _dbUpdateAccessTimeWithKey:key]; if (item.filename) { //** 存在檔名 ** // 讀入檔案 item.value = [self _fileReadWithName:item.filename]; if (!item.value) { //** 未找到檔案 ** // 刪除資料庫記錄 [self _dbDeleteItemWithKey:key]; item = nil; } } } return item; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 資料庫查詢 - (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData { // 準備執行sql 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]; if (!stmt) return nil; // 繫結引數 sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); YYKVStorageItem *item = nil; // 執行操作 int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { //** 存在可讀的row ** // 獲取YYKVStorageItem item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData]; } else { if (result != SQLITE_DONE) { //** 未完成執行資料庫 ** // 輸出錯誤logs if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); } } return item; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 轉換模型YYKVStorageItem - (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData { int i = 0; // 取出資料 char *key = (char *)sqlite3_column_text(stmt, i++); char *filename = (char *)sqlite3_column_text(stmt, i++); 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 *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; } |
YYDiskCache
1.初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
- (instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold { self = [super init]; if (!self) return nil; // 1.根據path先從快取裡面找YYDiskCache(未找到再去重新建立例項) YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path); if (globalCache) return globalCache; // 2.重新建立例項 // 2.1快取方式 YYKVStorageType type; if (threshold == 0) { type = YYKVStorageTypeFile; } else if (threshold == NSUIntegerMax) { type = YYKVStorageTypeSQLite; } else { type = YYKVStorageTypeMixed; } // 2.2例項化YYKVStorage物件(YYKVStorage上面已分析,YYDiskCache的快取實現都在YKVStorage) YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type]; if (!kv) return nil; // 2.3初始化資料 _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]; // 2.4快取self _YYDiskCacheSetGlobal(self); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil]; return self; } |
1 2 3 4 5 6 7 8 9 10 11 12 |
// 獲取已經快取的YYDiskCache物件 static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) { if (path.length == 0) return nil; // 初始化字典(用來快取YYDiskCache物件)與建立鎖 _YYDiskCacheInitGlobal(); // 加鎖 dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER); id cache = [_globalInstances objectForKey:path]; // 解鎖 dispatch_semaphore_signal(_globalInstancesLock); return cache; } |
1 2 3 4 5 6 7 8 9 10 |
// 初始化字典(用來快取YYDiskCache物件)與建立鎖 static void _YYDiskCacheInitGlobal() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 建立一把鎖 _globalInstancesLock = dispatch_semaphore_create(1); // 初始化字典 _globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; }); } |
1 2 3 4 5 6 7 8 |
// 儲存新的YYDiskCache static void _YYDiskCacheSetGlobal(YYDiskCache *cache) { if (cache.path.length == 0) return; _YYDiskCacheInitGlobal(); dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER); [_globalInstances setObject:cache forKey:cache.path]; dispatch_semaphore_signal(_globalInstancesLock); } |
2.新增快取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// 新增快取 - (void)setObject:(id<NSCoding>)object forKey:(NSString *)key { if (!key) return; if (!object) { //** 快取物件為null ** // 刪除快取 [self removeObjectForKey:key]; return; } NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object]; NSData *value = nil; // 你可以customArchiveBlock外部歸檔資料 if (_customArchiveBlock) { value = _customArchiveBlock(object); } else { <a href="http://www.jobbole.com/members/xyz937134366">@try</a> { // 歸檔資料 value = [NSKeyedArchiver archivedDataWithRootObject:object]; } <a href="http://www.jobbole.com/members/wx895846013">@catch</a> (NSException *exception) { // nothing to do... } } if (!value) return; NSString *filename = nil; if (_kv.type != YYKVStorageTypeSQLite) { // ** 快取型別非YYKVStorageTypeSQLite ** if (value.length > _inlineThreshold) { // ** 快取物件大於_inlineThreshold值則用檔案快取 ** // 生成檔名 filename = [self _filenameForKey:key]; } } // 加鎖 Lock(); // 快取資料(此方法上面講過) [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData]; // 解鎖 Unlock(); } |
3.其他查詢,刪除都類似,就不一一分析了
YYCache原始碼分析(一)
YYCache原始碼分析(二)
文章作者微信公眾號:hans_iOS (有疑問可以在公眾號裡詢問!)