源碼解析--YYCache

封面.jpg

前言:準(zhǔn)備看下YY系列中的YYWebImage框架,發(fā)現(xiàn)該框架是使用YYCache來做緩存的。那就從緩存開始吧.
先奉上YYCache框架的地址以及作者的設(shè)計思路
學(xué)習(xí)YYCache框架你可以get到:
1.優(yōu)雅的代碼風(fēng)格
2.優(yōu)秀的接口設(shè)計
3.YYCache的層次結(jié)構(gòu)
4.YYMemoryCache類的層次結(jié)構(gòu)和緩存機(jī)制
5.YYDiskCache類的層次結(jié)構(gòu)和緩存機(jī)制

YYCache

YYCache結(jié)構(gòu).png

YYCache最為食物鏈的最頂端的男人,并沒有什么好說的,所以我們就從YYMemoryCacheYYDiskCache開始吧。

YYMemoryCache

YYMemoryCache內(nèi)存儲存是的原理是利用CFDictionary對象的 key-value開辟內(nèi)存儲存機(jī)制和雙向鏈表原理來實現(xiàn)LRU算法。這里是官方文檔對CFDictionary的解釋:

CFMutableDictionary creates dynamic dictionaries where you can add or delete key-value pairs at any time, and the dictionary automatically allocates memory as needed.

YYMemoryCache類結(jié)構(gòu)圖.png

YYMemoryCache初始化的時候會建立空的私有對象YYLinkedMap鏈表,接下來所有的操作其實就是對這個鏈表的操作。當(dāng)然,YYMemoryCache提供了一個定時器接口給你,你可以通過設(shè)置autoTrimInterval屬性去完成每隔一定時間去檢查countLimit,costLimit是否達(dá)到了最大限制,并做相應(yīng)的操作。

- (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];
        //遞歸的調(diào)用
        [self _trimRecursively];
    });
}

- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        //檢查是否達(dá)到設(shè)置的最大消耗,并做相應(yīng)的處理
        [self _trimToCost:self->_costLimit];
        //檢查是否達(dá)到該緩存設(shè)置的最大持有對象數(shù),并做相應(yīng)的處理
        [self _trimToCount:self->_countLimit];
        //當(dāng)前的時間和鏈表最后的節(jié)點時間的差值是否大于設(shè)定的_ageLimit值,移除大于該值得節(jié)點
        [self _trimToAge:self->_ageLimit];
    });
}

YYMemoryCache以block的形式給你提供了下面接口:

  • didReceiveMemoryWarningBlock(當(dāng)app接受到內(nèi)存警告)
  • didEnterBackgroundBlock (當(dāng)app進(jìn)入到后臺)

當(dāng)然,你也可以通過設(shè)置相應(yīng)的shouldRemoveAllObjectsOnMemoryWarningshouldRemoveAllObjectsWhenEnteringBackground值來移除YYMemoryCache持有的鏈表。

下面我們來看看YYMemoryCache類的增,刪,查等操作。在這之前我們先看看YYLinkedMap這個類。

1.YYLinkedMap內(nèi)部結(jié)構(gòu)

YYLinkedMap作為雙向鏈表,主要的工作是為YYMemoryCache類提供對YYLinkedMapNode節(jié)點的操作。下圖綠色部分代表節(jié)點:

雙向鏈表結(jié)構(gòu).png

下圖是鏈表節(jié)點的結(jié)構(gòu)圖:
鏈表節(jié)點.png

現(xiàn)在我們先來看如何去構(gòu)造一個鏈表添加節(jié)點:
setObject.png

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //鎖
    pthread_mutex_lock(&_lock);
    //查找是否存在對應(yīng)該key的節(jié)點
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
        //修改相應(yīng)的數(shù)據(jù)
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        //根據(jù)LRU算法原理,將訪問的點移到最前面
        [_lru bringNodeToHead:node];
    } else {
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        //在鏈表最前面插入結(jié)點
        [_lru insertNodeAtHead:node];
    }
    //判斷鏈表的消耗的總資源是否大于設(shè)置的最大值
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    //判斷鏈表的總持有節(jié)點是否大于該緩存設(shè)置的最大持有數(shù)
    if (_lru->_totalCount > _countLimit) {  //當(dāng)超出設(shè)定的最大的值
        //移除鏈表最后的節(jié)點
        _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);
}

你可以點擊這里自己去操作雙向鏈表

addNode.gif

鏈表移除節(jié)點的操作:

- (void)removeObjectForKey:(id)key {
    if (!key) return;
    //鎖
    pthread_mutex_lock(&_lock);
    //根據(jù)key拿到相應(yīng)的節(jié)點
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    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);
}

removeNode.gif

YYMemoryCache類還為我們提供了下列接口方便我們調(diào)用:

- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)removeAllObjects;

總結(jié):YYMemoryCache是利用key-value機(jī)制內(nèi)存緩存類,所有的方法都是線程安全的。如果你熟悉NSCache類,你會發(fā)現(xiàn)兩者的接口很是相似。
當(dāng)然YYMemoryCache有著自己的特點:
1.YYMemoryCache采用LRU(least-recently-used)算法來移除節(jié)點。
2.YYMemoryCache可以用countLimit,costLimit,ageLimit屬性做相應(yīng)的控制。
3.YYMemoryCache類可以設(shè)置相應(yīng)的屬性來控制退到后臺或者接受到內(nèi)存警告的時候移除鏈表。

YYKVStorage

YYKVStorage是一個基于sql數(shù)據(jù)庫和文件寫入的緩存類,注意它并不是線程安全。你可以自己定義YYKVStorageType來確定是那種寫入方式:

typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    
    /// The `value` is stored as a file in file system.
    YYKVStorageTypeFile = 0,
    
    /// The `value` is stored in sqlite with blob type.
    YYKVStorageTypeSQLite = 1,
    
    /// The `value` is stored in file system or sqlite based on your choice.
    YYKVStorageTypeMixed = 2,
};

1.寫入和更新

我們看看Demo中直接用YYKVStorage儲存NSNumber和NSData YYKVStorageTypeFileYYKVStorageTypeSQLite類型所用的時間:

7.png

你可以發(fā)現(xiàn)在儲存小型數(shù)據(jù)NSNumberYYKVStorageTypeFile類型是YYKVStorageTypeSQLite大約4倍多,而在大型數(shù)據(jù)的時候兩者的表現(xiàn)是相反的。顯然選擇合適的儲存方式是很有必要的。
這里需要提醒的事:

  • DemoYYKVStorageTypeFile類型其實不僅寫入了本地文件也同時寫入了數(shù)據(jù)庫,只不過數(shù)據(jù)庫里面存的是除了value值以外的key, filename, size, inline_data(NULL), 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;
    //_type為YYKVStorageTypeSQLite時候filename應(yīng)該為空,不然還是會寫入文件
    //_type為YYKVStorageTypeFile時候filename的值不能為空
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    //是否寫入文件是根據(jù)filename.length長度來判斷的
    if (filename.length) {
        //先儲存在文件里面
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        //儲存在sql數(shù)據(jù)庫
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            //儲存數(shù)據(jù)庫失敗就刪除之前儲存的文件
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        //儲存在sql數(shù)據(jù)庫
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

插入或者是更新數(shù)據(jù)庫

- (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];
    if (!stmt) return NO;
    
    int timestamp = (int)time(NULL);
    //sqlite3_bind_xxx函數(shù)給這條語句綁定參數(shù)
    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);
    //當(dāng)fileName為空的時候存在數(shù)據(jù)庫的是value.bytes,不然存的是NULl對象
    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);
    //通過sqlite3_step命令執(zhí)行創(chuàng)建表的語句
    int result = sqlite3_step(stmt);
    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.讀取

我們嘗試的去緩存里面拿取數(shù)據(jù),我們發(fā)現(xiàn)當(dāng)為YYKVStorage對象type不同,存取的方式不同所以讀取的方式也不同:
1.因為在插入的時候我們就說了,當(dāng)為YYKVStorageTypeFile類型的時候數(shù)據(jù)是存在本地文件的其他存在數(shù)據(jù)庫。所以YYKVStorage對象先根據(jù)key從數(shù)據(jù)庫拿到數(shù)據(jù)然后包裝成YYKVStorageItem對象,然后再根據(jù)filename讀取本地文件數(shù)據(jù)賦給YYKVStorageItem對象的value屬性。
2.當(dāng)為YYKVStorageTypeSQLite類型就是直接從數(shù)據(jù)庫把所有數(shù)據(jù)都讀出來賦給YYKVStorageItem對象。

- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    if (key.length == 0) return nil;
    /*先從數(shù)據(jù)庫讀包裝item,
     當(dāng)時filename不為空的時候,以為著數(shù)據(jù)庫里面沒有存Value值,還得去文件里面讀出來value值
     當(dāng)時filename為空的時候,意味著直接從數(shù)據(jù)庫來拿取Value值
     */
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
    if (item) {
        //更新的last_access_time字段
        [self _dbUpdateAccessTimeWithKey:key];
        if (item.filename) {
            //從文件里面讀取value值
            item.value = [self _fileReadWithName:item.filename];
            if (!item.value) {
                //數(shù)據(jù)為空則從數(shù)據(jù)庫刪除這條記錄
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}

3.刪除

YYKVStorage的type當(dāng)為YYKVStorageTypeFile類型是根據(jù)key將本地和數(shù)據(jù)庫都刪掉,而YYKVStorageTypeSQLite是根據(jù)key刪除掉數(shù)據(jù)庫就好了。

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

我們這里分別列取了增刪改查的單個key的操作,你還可以去批量的去操作key的數(shù)組。但是其實都大同小異的流程,就不一一累述了。上個圖吧:

屏幕快照 2016-12-28 下午10.10.38.png

這個類也就看的差不多了,但是要注意的事,YYCache作者并不希望我們直接使用這個類,而是使用更高層的YYDiskCache類。那我們就繼續(xù)往下面看吧。

YYDiskCache

YYDiskCache類有兩種初始化方式:

- (nullable instancetype)initWithPath:(NSString *)path;
- (nullable instancetype)initWithPath:(NSString *)path
                      inlineThreshold:(NSUInteger)threshold 

YYDiskCache類持有一個YYKVStorage對象,但是你不能手動的去控制YYKVStorage對象的YYKVStorageType。YYDiskCache類初始化提供一個threshold的參數(shù),默認(rèn)的為20KB。然后根據(jù)這個值得大小來確定YYKVStorageType的類型。

YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];

因為YYDiskCache類的操作其實就是去操作持有的YYKVStorage對象,所以下面的部分會比較建簡略。

寫入和更新

在調(diào)用YYKVStorage對象的儲存操作前主要做了下面幾項操作:
1.key和object的判空容錯機(jī)制
2.利用runtime機(jī)制去取extendedData數(shù)據(jù)
3.根據(jù)是否定義了_customArchiveBlock來判斷選擇序列化object還是block回調(diào)得到value
4.value的判空容錯機(jī)制
5.根據(jù)YYKVStorage的type判斷以及_inlineThreshold和value值得長度來判斷是否選擇以文件的形式儲存value值。上面我們說過當(dāng)value比較大的時候文件儲存速度比較快速。
6.如果_customFileNameBlock為空,則根據(jù)key通過md5加密得到轉(zhuǎn)化后的filename.不然直接拿到_customFileNameBlock關(guān)聯(lián)的filename。生成以后操作文件的路徑
做完上面的操作則直接調(diào)用YYKVStorage儲存方法,下面是實現(xiàn)代碼:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    //runtime 取extended_data_key的value
    NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
    NSData *value = nil;
    if (_customArchiveBlock) {
        //block返回
        value = _customArchiveBlock(object);
    } else {
        @try {
            //序列化
            value = [NSKeyedArchiver archivedDataWithRootObject:object];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (!value) return;
    NSString *filename = nil;
    if (_kv.type != YYKVStorageTypeSQLite) {
        //長度判斷這個儲存方式,value.length當(dāng)大于_inlineThreshold則文件儲存
        if (value.length > _inlineThreshold) {
            //將key 進(jìn)行md5加密
            filename = [self _filenameForKey:key];
        }
    }
    
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}

讀取

讀取操作一般都是和寫入操作相輔相成的,我們來看看在調(diào)用YYKVStorage對象的讀取操作后做了哪些操作:
1.item.value的判空容錯機(jī)制
2.根據(jù)_customUnarchiveBlock值來判斷是直接將item.value block回調(diào)還是反序列化成object
3.根據(jù)object && item.extendedData 來決定是否runtime添加extended_data_key屬性

- (id<NSCoding>)objectForKey:(NSString *)key {
    if (!key) return nil;
    Lock();
    YYKVStorageItem *item = [_kv getItemForKey:key];
    Unlock();
    if (!item.value) return nil;
    
    id object = nil;
    if (_customUnarchiveBlock) {
        object = _customUnarchiveBlock(item.value);
    } else {
        @try {
            object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (object && item.extendedData) {
        [YYDiskCache setExtendedData:item.extendedData toObject:object];
    }
    return object;
}

刪除

刪除操作就是直接調(diào)用的YYKVStorage對象來操作了。

- (void)removeObjectForKey:(NSString *)key {
    if (!key) return;
    Lock();
    [_kv removeItemForKey:key];
    Unlock();
}

當(dāng)然,YYDiskCacheYYMemoryCache一樣也給你提供了一些類似limit的接口供你操作。

- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;

YYKVStorage不一樣的是,作為更高層的YYDiskCache是一個線程安全的類。你應(yīng)該使用YYDiskCache而不是YYKVStorage

最后再帶一筆食物端最頂端的男人YYCache,當(dāng)他寫入的時候會同時調(diào)用YYDiskCache磁盤操作和YYMemoryCache內(nèi)存操作。讀取的時候先從內(nèi)存讀取,因為在內(nèi)存的讀取速度比磁盤快很多,如果沒有讀取到數(shù)據(jù)才會去磁盤讀取。

讀后感只有四個字:

如沐春風(fēng)

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

  • 概述 上一篇主要講解了YYMemoryCache的文件結(jié)構(gòu),分析了YYMemoryCache類的相關(guān)方法,本章主要...
    egoCogito_panf閱讀 3,975評論 3 11
  • 概述 上一篇主要講解了YYCache的文件結(jié)構(gòu),分析了YYCache類的相關(guān)方法,本章主要分析內(nèi)存緩存類YYMem...
    egoCogito_panf閱讀 3,267評論 2 12
  • 前言 本篇文章將帶來YYCache的解讀,YYCache支持內(nèi)存和本地兩種方式的數(shù)據(jù)存儲。我們先拋出兩個問題: Y...
    老馬的春天閱讀 3,730評論 17 32
  • 概述 YYCache是一個用來封裝客戶端緩存功能的庫,實現(xiàn)了二級緩存的機(jī)制,即同時具備內(nèi)存緩存和硬盤緩存的功能。 ...
    egoCogito_panf閱讀 2,710評論 0 5
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,644評論 18 399

友情鏈接更多精彩內(nèi)容