YYKit源碼分析---YYCache

YYCache是用于Objective-C中用于緩存的第三方框架。此文主要用來講解該框架的實現(xiàn)細(xì)節(jié),性能分析、設(shè)計思路ibireme已經(jīng)講得很清楚了,我這邊就不在分析了。首推個人blog:iipanda.com,同步更新。

文件結(jié)構(gòu)

  1. YYCache:同時實現(xiàn)內(nèi)存緩存和磁盤緩存且是線程安全的
  2. YYMemoryCache:實現(xiàn)內(nèi)存緩存,所有的API都是線程安全的,與其他緩存方式比較不同的是內(nèi)部利用LRU淘汰算法(后面會介紹)來提高性能
  3. YYDiskCache:實現(xiàn)磁盤緩存,所有的API都是線程安全的,內(nèi)部也采用了LRU淘汰算法,主要SQLite和文件存儲兩種方式
  4. YYKVStorage:實現(xiàn)磁盤存儲,不推薦直接使用該類,該類不是線程安全的

LRU

LRU(Least recently used,最近最少使用)算法,根據(jù)訪問的歷史記錄來對數(shù)據(jù)進(jìn)行淘汰
<p>



<p>
簡單的來說3點:

  1. 有新數(shù)據(jù)加入時添加到鏈表的頭部
  2. 每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問),則將數(shù)據(jù)移到鏈表頭部
  3. 當(dāng)鏈表滿的時候,將鏈表尾部的數(shù)據(jù)丟棄

在YYMemoryCache中使用來雙向鏈表和NSDictionary實現(xiàn)了LRU淘汰算法,后面會介紹

關(guān)于鎖

YYCache 使用到兩種鎖

  1. OSSpinLock :自旋鎖,上一篇博客也提及到pthread_mutex
  2. dispatch_semaphore:信號量,當(dāng)信號量為1的時候充當(dāng)鎖來用

內(nèi)存緩存用的pthread_mutex:由于pthread_mutex相當(dāng)于do while忙等,等待時會消耗大量的CPU資源
磁盤緩存使用的dispatch_semaphore:優(yōu)勢在于等待時不會消耗CPU資源

簡單的科普就到這,現(xiàn)在來開始源碼的探索

_YYLinkedMap

@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

_YYLinkedMapNode:鏈表的節(jié)點

_prev、_next:分別表示指向上一個節(jié)點、下一個節(jié)點

_key:緩存的key

_value:緩存對象

_cost:內(nèi)存消耗

_time:緩存時間

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU(最近最常使用算法), do not change it directly
    _YYLinkedMapNode *_tail; // LRU(最近最少使用算法-清除較不常使用數(shù)據(jù)), do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

_YYLinkedMap:鏈表

_dic:用來保存節(jié)點

_totalCost:總緩存開銷

_head、_tail:頭節(jié)點、尾節(jié)點

_releaseOnMainThread:是否在主線程釋放_YYLinkedMapNode

_releaseAsynchronously:是否異步釋放_YYLinkedMapNode

雙向鏈表

  1. 插入節(jié)點到頭部
  2. 將除兩邊的節(jié)點移到頭部
  3. 移除除兩邊的節(jié)點
  4. 移除尾部節(jié)點
  5. 移除所有節(jié)點

看下移除所有節(jié)點的代碼:

- (void)removeAll {
    _totalCost = 0;
    _totalCount = 0;
    _head = nil;
    _tail = nil;
    if (CFDictionaryGetCount(_dic) > 0) {
        CFMutableDictionaryRef holder = _dic;
        _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        
        if (_releaseAsynchronously) {
            dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                CFRelease(holder); // hold and release in specified queue
            });
        } else if (_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                CFRelease(holder); // hold and release in specified queue
            });
        } else {
            CFRelease(holder);
        }
    }
}


這邊通過雙向鏈表來對數(shù)據(jù)進(jìn)行操作,和NSDictionary實現(xiàn)了LRU淘汰算法。時間復(fù)雜度0(1),5種操作基本上都是對頭尾節(jié)點和鏈表節(jié)點的上一個節(jié)點和下一個節(jié)點進(jìn)行操作。

YYMemoryCache

這邊介紹兩個主要的操作:添加緩存,查找緩存<p>

  • 添加緩存
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        // 緩存對象為nil,直接移除
        [self removeObjectForKey:key];
        return;
    }
    // 為了保證線程安全,數(shù)據(jù)操作前進(jìn)行加鎖
    pthread_mutex_lock(&_lock);
    // 查找緩存
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    // 當(dāng)前時間
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
        // 緩存對象已存在,更新數(shù)據(jù),并移到棧頂
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];
    } else {
        // 緩存對象不存在,添加數(shù)據(jù),并移到棧頂
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        [_lru insertNodeAtHead:node];
    }
    // 判斷當(dāng)前的緩存進(jìn)行是否超出了設(shè)定值,若超出則進(jìn)行整理
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    
    // 每次添加數(shù)據(jù)僅有一個,數(shù)量上超出時,直接移除尾部那個object即可
    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
            });
        }
    }
    // 操作結(jié)束,解鎖
    pthread_mutex_unlock(&_lock);
}

  • 異步線程釋放

    里面很多都用到類似的方法,將一個對象在異步線程中釋放,來分析下:
    - p
    1. 首先通過node來對其進(jìn)行持有,以至于不會在方法調(diào)用結(jié)束的時候被銷毀
    2. 我要要在其他線程中進(jìn)行銷毀,所以將銷毀操作放在block中,block就會對其進(jìn)行持有
    3. 這邊在block中隨便調(diào)用了個方法,保證編譯器不會優(yōu)化掉這個操作
    4. 當(dāng)block結(jié)束后,node沒有被持有的時候,就會在當(dāng)前線程被release掉了
  • 添加緩存
// 這邊從memory中取數(shù)據(jù)時,根據(jù)LRU原則,將最新取出的object放到棧頭
- (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();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

YYKVStorage

該文件主要以兩種方式來實現(xiàn)磁盤存儲:SQLite、File,使用兩種方式混合進(jìn)行存儲主要為了提高讀寫效率。寫入數(shù)據(jù)時,SQLite要比文件的方式更快;讀取數(shù)據(jù)的速度主要取決于文件的大小。據(jù)測試,在iPhone6中,當(dāng)文件大小超過20kb時,F(xiàn)ile要比SQLite快的多。所以當(dāng)大文件存儲時建議用File的方式,小文件更適合用SQLite。<p>
下邊分別對Save、Remove、Get分別進(jìn)行分析

  • Save
- (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) {    // filename存在 SQLite File兩種方式并行
        // 用文件進(jìn)行存儲
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        // 用SQLite進(jìn)行存儲
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            // 當(dāng)使用SQLite方式存儲失敗時,刪除本地文件存儲
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {               // filename不存在 SQLite
        if (_type != YYKVStorageTypeSQLite) {
            // 這邊去到filename后,刪除filename對應(yīng)的file文件
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        // SQLite 進(jìn)行存儲
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}


  • Remove
- (BOOL)removeItemForKey:(NSString *)key {
    if (key.length == 0) return NO;
    switch (_type) {
        case YYKVStorageTypeSQLite: {
            // 刪除SQLite文件
            return [self _dbDeleteItemWithKey:key];
        } break;
        case YYKVStorageTypeFile:
        case YYKVStorageTypeMixed: {
            // 獲取filename
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                // 刪除filename對的file
                [self _fileDeleteWithName:filename];
            }
            // 刪除SQLite文件
            return [self _dbDeleteItemWithKey:key];
        } break;
        default: return NO;
    }
}

  • Get
- (NSData *)getItemValueForKey:(NSString *)key {
    if (key.length == 0) return nil;
    NSData *value = nil;
    switch (_type) {
        case YYKVStorageTypeFile: { //File
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                // 根據(jù)filename獲取File
                value = [self _fileReadWithName:filename];
                if (!value) {
                    // 當(dāng)value不存在,用對應(yīng)的key刪除SQLite文件
                    [self _dbDeleteItemWithKey:key];
                    value = nil;
                }
            }
        } break;
        case YYKVStorageTypeSQLite: {
            // SQLite 方式獲取
            value = [self _dbGetValueWithKey:key];
        } break;
        case YYKVStorageTypeMixed: {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            // filename 存在文件獲取,不存在SQLite方式獲取
            if (filename) {
                value = [self _fileReadWithName:filename];
                if (!value) {
                    [self _dbDeleteItemWithKey:key];
                    value = nil;
                }
            } else {
                value = [self _dbGetValueWithKey:key];
            }
        } break;
    }
    if (value) {
        // 更新文件操作時間
        [self _dbUpdateAccessTimeWithKey:key];
    }
    return value;
}

File方式主要使用的writeToFile進(jìn)行存儲,SQLte直接使用的sqlite3來對文件進(jìn)行操作,具體數(shù)據(jù)庫相關(guān)的操作這邊就不在進(jìn)行分析了,感興趣的自己可以閱讀下

YYDiskCache

YYDiskCache是對YYKVStorage進(jìn)行的一次封裝,是線程安全的,這邊使用的是dispatch_semaphore_signal來確保線程的安全。另外他結(jié)合LRU算法,根據(jù)文件的大小自動選擇存儲方式來達(dá)到更好的性能。

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    // 獲取緩存的 YYDiskCache
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    // 確定存儲的方式
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
    // 初始化 YYKVStorage
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    
    // 初始化數(shù)據(jù)
    _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];
    // 對當(dāng)前對象進(jìn)行緩存
    _YYDiskCacheSetGlobal(self);
    
    // 通知 APP即將被殺死時
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

其他的一些操作基本上都是對YYKVStorage的一些封裝,這邊就不一一分析了。

參考文獻(xiàn)

  1. http://blog.ibireme.com/2015/10/26/yycache/
  2. http://blog.csdn.net/yunhua_lee/article/details/7599671
最后編輯于
?著作權(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)容