存儲的單元是_YYLinkedMapNode,除了key和value外,還存儲了它的前後Node的地址_prev,_next.
整個實現基於_YYLinkedMap,它是一個雙向鏈表,除了存儲了字典_dic外,還存儲了頭結點和尾節點.它實現的功能很簡單,就是:有新數據了插入鏈表頭部,訪問過的數據結點移到頭部,內存緊張時把尾部的結點移除.就這樣實現了淘汰算法.
因為內存訪問速度很快,鎖占用的時間少,所以用的速度最快的OSSpinLockLock
部分源碼解析:
- (instancetype)init { self = super.init; pthread_mutex_init(&_lock, NULL); //初始化互斥鎖的方法 ,在dealloc方法中進行回收 _lru = [_YYLinkedMap new]; _queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL); _countLimit = NSUIntegerMax; _costLimit = NSUIntegerMax; _ageLimit = DBL_MAX; _autoTrimInterval = 5.0; _shouldRemoveAllObjectsOnMemoryWarning = YES; _shouldRemoveAllObjectsWhenEnteringBackground = YES; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; //注冊內存警告的通知,方便回收 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil]; //注冊程序進入後台的通知,可以回收內存。 [self _trimRecursively]; return self; }刪除某個值的操作
- (void)removeObjectForKey:(id)key { if (!key) return; pthread_mutex_lock(&_lock); //加鎖,保證線程安全 _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //獲取對應key的node if (node) { [_lru removeNode:node]; //移除這個節點 if (_lru->_releaseAsynchronously) { //判斷當前是異步線程還是多線程 dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); // dispatch_async(queue, ^{ [node class]; //hold and release in queue }); } else if (_lru->_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ [node class]; //hold and release in queue }); } } pthread_mutex_unlock(&_lock); //解鎖 }增加一對鍵值相關代碼 //解釋在注釋中
- (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)); //取出對應key的node NSTimeInterval now = CACurrentMediaTime(); //保存一個時間戳,把最新的修改時間或添加保存進入node中。 if (node) { //存在的操作 _lru->_totalCost -= node->_cost; //把原有緩存內存開銷減去 _lru->_totalCost += cost; //加入新的內存開銷 node->_cost = cost; //指定內存開銷 node->_time = now; //修改的最新時間 node->_value = object; // 賦值 [_lru bringNodeToHead:node]; // 修改操作,把修改node放到雙鏈表的頭部。 } else { //不存在次node的操作 node = [_YYLinkedMapNode new]; //創建一個 _YYLinkedMapNode node->_cost = cost; node->_time = now; node->_key = key; node->_value = object; [_lru insertNodeAtHead:node]; //插入操作,把最新的node放到雙鏈表的頭部 } if (_lru->_totalCost > _costLimit) { //如果內存花銷超出了最大限制的內存大小 dispatch_async(_queue, ^{ [self trimToCost:_costLimit]; //刪除雙鏈表的尾部節點 }); } if (_lru->_totalCount > _countLimit) { //如果超出緩存數量,也是刪除雙鏈表的尾節點 _YYLinkedMapNode *node = [_lru removeTailNode]; if (_lru->_releaseAsynchronously) { //當前線程的判斷 dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [node class]; //hold and release in queue }); } else if (_lru->_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ [node class]; //hold and release in queue }); } } pthread_mutex_unlock(&_lock); //解鎖 }
查詢某個key
- (id)objectForKey:(id)key { if (!key) return nil; pthread_mutex_lock(&_lock); //加鎖 _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); if (node) { node->_time = CACurrentMediaTime(); //更改當前node的時間 [_lru bringNodeToHead:node]; //把查找到的node,移到雙向鏈表的頭部 } pthread_mutex_unlock(&_lock); //解鎖 return node ? node->_value : nil; //利用三目運算符,進行返回value or nil。 }
總結:(三種常見的操作)
1. 插入: 沒有對應key,只需要把新的數據插入到雙鏈表的頭部
2. 替換: 有對應的key, 修改時間,value,並把數據移到雙鏈表的頭部,加上是否超出內存大小限制,是否超出文件數量限制,如果有,就移除雙鏈表的尾節點。
3. 查找: 有對應的key,返回值,並修改node的time,並把數據移到雙鏈表的頭部, 無對應的key,返回nil 。
采用的是文件和數據庫相互配合的方式.
有一個參數inlineThreshold,默認20KB,小於它存數據庫,大於它存文件.能獲得效率的提高.
key:path,value:cache存儲在NSMapTable裡.根據path獲得cache,進行一系列的set,get,remove操作
更底層的是YYKVStorage,它能直接對sqlite和文件系統進行讀寫.
每次內存超過限制時,select key, filename, size from manifest order by last_access_time desc limit ?1
會根據時間排序來刪除最近不常用的數據.
硬盤訪問的時間比較長,如果用OSSpinLockLock鎖會造成CPU消耗過大,所以用的dispatch_semaphore_wait來做.
YYDiskCache的核心部分是YYKVStorage. YYKVStorage解析:不直接使用,通過YYDiskCache調用。 1. 新增OR替換操作- (BOOL)saveItem:(YYKVStorageItem *)item { return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData]; } - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value { return [self saveItemWithKey:key value:value filename:nil extendedData:nil]; } - (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]; //保存到數據庫 } }文件保存操作
- (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data { NSString *path = [_dataPath stringByAppendingPathComponent:filename]; return [data writeToFile:path atomically:NO]; //寫入文件 }
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData { 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); //綁定key sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); //綁定 filename sqlite3_bind_int(stmt, 3, (int)value.length); //綁定value if (fileName.length == 0) { sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0); } else { 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); //執行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; }2. 查找操作
- (YYKVStorageItem *)getItemForKey:(NSString *)key { if (key.length == 0) return nil; YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO]; //從數據庫返回對應的對象 if (item) { [self _dbUpdateAccessTimeWithKey:key]; //更新這個key對應的對象時間 if (item.filename) { item.value = [self _fileReadWithName:item.filename]; //從文件中取得對應key的value,並賦值 if (!item.value) { //如果value不存在,就把對應的key刪除掉 [self _dbDeleteItemWithKey:key]; item = nil; } } } return item; }
- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData { 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;"; //sql語句拼接,通過key去查看對應的YYKVStorageItem. 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) { //查詢成功的值 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; }
- (NSData *)_fileReadWithName:(NSString *)filename { NSString *path = [_dataPath stringByAppendingPathComponent:filename]; //通過文件名,拼接路徑 NSData *data = [NSData dataWithContentsOfFile:path]; //返回對應的data return data; }
- (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; } }數據庫緩存刪除
- (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); //執行 if (result != SQLITE_DONE) { //結果 SQLITE_DONE 為成功 if (_errorLogsEnabled) NSLog(@"%s line:%d db delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; }文件緩存
- (BOOL)_fileDeleteWithName:(NSString *)filename { NSString *path = [_dataPath stringByAppendingPathComponent:filename]; return [[NSFileManager defaultManager] removeItemAtPath:path error:NULL]; //刪除操作 }那肯定有人問,咦, 怎麼YYCache類,還沒有冒頭呀。 哈哈,這個是我們最終使用的類喲。 也就是說,上面的什麼內存緩存呀,磁盤緩存呀,我們壓根就不是直接使用它們的,我們使用封裝好它們的YYCache類。 下面就開始介紹YYCache類。 直接上代碼:
@interface YYCache : NSObject // 讀取當前數據庫名稱 @property (copy, readonly) NSString *name; @property (strong, readonly) YYMemoryCache *memoryCache; //內存緩存 @property (strong, readonly) YYDiskCache *diskCache; //文件緩存 // 可通過下面三種方法來實例化YYCache對象 - (nullable instancetype)initWithName:(NSString *)name; - (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER; + (nullable instancetype)cacheWithPath:(NSString *)path; // 禁止通過下面兩個方式實例化對象 - (instancetype)init UNAVAILABLE_ATTRIBUTE; + (instancetype)new __attribute__((unavailable("new方法不可用,請用initWithName:"))); // 通過key判斷是否緩存了某個東西,第二個法是異步執行,異步回調 - (BOOL)containsObjectForKey:(NSString *)key; - (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block; // 讀--通過key讀取緩存,第二個法是異步執行,異步回調 - (nullable id看了上面的代碼後,使用那就是更加簡單了)objectForKey:(NSString *)key; - (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id object))block; // 增、改--緩存對象(可緩存遵從NSCoding協議的對象),第二個法是異步執行,異步回調 - (void)setObject:(nullable id )object forKey:(NSString *)key; - (void)setObject:(nullable id )object forKey:(NSString *)key withBlock:(nullable void(^)(void))block; // 刪--刪除緩存 - (void)removeObjectForKey:(NSString *)key; - (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block; - (void)removeAllObjects; - (void)removeAllObjectsWithBlock:(void(^)(void))block; - (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress endBlock:(nullable void(^)(BOOL error))end; @end
// 0.初始化YYCache YYCache *cache = [YYCache cacheWithName:@"myFirstDb"]; // 1.緩存普通字符 [cache setObject:@"緩存" forKey:@"savaKey"]; NSString *name = (NSString *)[cache objectForKey:@"savaKey"]; //根據key取value NSLog(@"name: %@", name); // 2.緩存模型 (model需要遵循NSCoding協議) [cache setObject:model forKey:@"user"]; // 異步緩存 [cache setObject:array forKey:@"user" withBlock:^{ }]; //讀取 [cache objectForKey:@"user" withBlock:^(NSString * _Nonnull key, id本文總結: 由衷的佩服YYCache的作者, 真是太牛了,裡面用到了很多技巧,本文就寫到這裡。YYCache git地址:點擊打開鏈接_Nonnull object) { //讀取後操作 }];