你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS開發之緩存框架、內存緩存、磁盤緩存等

iOS開發之緩存框架、內存緩存、磁盤緩存等

編輯:IOS開發綜合

1.在項目中我們難免會用到一些緩存方式來保存服務器傳過來的數據,以減少服務器的壓力。 緩存的方式分為兩種分別為內存緩存和磁盤緩存,內存緩存速度快容量小,磁盤緩存容量大速度慢可持久化。常見的內存緩存有NSCache、TMMemoryCache、PINMemoryCache、YYMemoryCache。常見的磁盤緩存有TMDiskCache、PINDiskCache、YYCache.

2.本文章著重講下YYCache。 這是為什麼呢,因為他比其他的緩存框架更加高效,使用方便。

  YYCache:去掉了異步訪問的接口,盡量優化了同步訪問的性能,用 OSSpinLock 來保證性能安全。另外,緩存內部用雙向鏈表和 NSDictionary 實現了 LRU 淘汰算法.體現高效的部分,也是采用的 SQLite 配合文件的存儲方式。在存取小數據 (NSNumber) 時,YYDiskCache 的性能遠遠高出基於文件存儲的庫;而較大數據的存取性能則比較接近了。但得益於 SQLite 存儲的元數據,YYDiskCache 實現了 LRU 淘汰算法、更快的數據統計,更多的容量控制選項。   YYCache的使用: \   上圖是YYCache的相關類文件。   重點介紹:

1. 內存緩存(YYMemoryCache)

存儲的單元是_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 。


2. 硬盤緩存(YYDiskCache)

采用的是文件和數據庫相互配合的方式.

有一個參數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;
}

    3. 刪除操作
- (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  _Nonnull object) {
         //讀取後操作 
     }];
本文總結: 由衷的佩服YYCache的作者, 真是太牛了,裡面用到了很多技巧,本文就寫到這裡。YYCache git地址:點擊打開鏈接  
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved