YYCache源碼閱讀總結


為什么要有緩存?

??使用緩存的2個主要原因:

  • 降低延遲:緩存離客戶端更近,因此,從緩存請求內容比從源服務器所用時間更少,呈現速度更快。

  • 降低網絡傳輸:副本被重復使用,大大降低了用戶的帶寬使用,其實也是一種變相的省錢(如果流量要付費的話),同時保證了帶寬請求在一個低水平上,更容易維護了。

緩存的策略:

??按需緩存:應用把從服務器獲取的內容以某種格式存放在本地文件系統,之后對于每次請求,檢查緩存中是否存在這塊數據,只有當數據不存在(或者過期)的情況下才從服務器獲取。獲取數據的速度比數據本身重要。按需緩存工作原理類似于瀏覽器緩存。它允許我們查看以前查看或者訪問過的內容。按需緩存可以通過在打開一個視圖控制器時按需地緩存數據模型(創建一個數據模型緩存)來實現,而不是在一個后臺線程上做這件事。
??預緩存:這種情況是緩存全部內容(或者最近n條記錄)以便離線訪問。更加重視被緩存數據,并且能快速編輯被緩存的記錄而無需連接到服務器。對預緩存來說,數據丟失或者緩存不命中是不可接受的,比方用戶下載了文章準備在地鐵上看,但卻發現設備上不存在這些文章。實現預緩存可能需要一個后臺線程訪問數據并以有意義的格式保存,以便本地緩存無需重新連接服務器即可被編輯。編輯可能是“標記記錄為已讀”或“加入收藏”,或其他類似的操作。這里有意義的格式是指可以用這種方式保存內容,不用和服務器通信就可以在本地作出上面提到的修改,并且一旦再次連上網就可以把變更發送回服務器。
??選擇使用按需緩存還是預緩存的一個簡便方法是判斷是否需要在下載數據之后處理數據。后期處理數據可能是以用戶產生編輯的形式,也可能是更新下載的數據,比如重寫HTML頁面里的圖片鏈接以指向本地緩存圖片。如果一個應用需要做上面提到的任何后期處理,就必須實現預緩存。

存儲緩存:

??第三方應用只能把信息保存在應用程序的沙盒中。因為緩存數據不是用戶產生的,所以它應該被保存在NSCachesDirectory,而不是NSDocumentsDirectory。為緩存數據創建獨立目錄是一項不錯的實踐。把緩存存儲在緩存文件夾下的原因是iCloud(和iTunes)的備份不包括此目錄。如果在Documents目錄下創建了大尺寸的緩存文件,它們會在備份的時候被上傳到iCloud并且很快就用完有限的空間。
??預緩存是用高級數據庫(比如原始的SQLite)或者對象序列化框架(比如Core Data)實現的。我們需要根據需求認真選擇不同的技術。

應該用哪種緩存技術

??在眾多可以本地保存數據的技術中,有三種脫穎而出:URL緩存、數據模型緩存(利用NSKeyedArchiver)和Core Data。
??假設你正在開發一個應用,需要緩存數據以改善應用表現出的性能,你應該實現按需緩存(使用數據模型緩存或URL緩存)。另一方面,如果需要數據能夠離線訪問,而且具有合理的存儲方式以便離線編輯,那么就用高級序列化技術。

緩存類型

??通常一個緩存是由內存緩存和磁盤緩存組成:

  • 內存緩存利用了設備的RAM,提供容量小但高速的存取功能,避免了頻繁的讀寫磁盤,提高了應用的響應速度,缺點是程序關閉時數據會消失
  • 磁盤緩存則將數據存儲在閃存中,類似于PC機的硬盤,提供大容量但低速的持久化存儲。存儲在閃存中的數據不回因為應用的關閉或重啟而丟失數據。
YYcache閱讀

??YYCache的主要文件如下:


YYCache的主要文件

??主要架構如下:


YYCache架構

YYCache:主要提供對外的API接口,通過調用這些API來進行緩存操作,而不用管理緩存的實現
YYMemoryCache:定義了內存緩存的數據結構和相關操作
YYDiskCache:提供了進行磁盤操作的API接口
YYKVStorage:定義了磁盤緩存的數據結構和相關操作

內存緩存

??當系統要運行一個程序時,會將程序從磁盤調入到內存,并分配一定的內存空間用以存放系統要執行代碼和數據。當分配的內存空間都被占滿之后,如果系統需要調入的新的數據放入內存,則需要采用某些策略對內存進行清理,騰出空間。
??常用的緩存替換算法如下:

  • FIFO(先進先出算法):這種算法選擇最先被緩存的數據為被替換的對象,即當緩存滿的時候,應當把最先進入緩存的數據給淘汰掉。它的優點是比較容易實現,但是沒有反映程序的局部性。因為被淘汰的數據可能會在將來被頻繁地使用。
  • LFU(近期最少使用算法):這種算法基于“如果一個數據在最近一段時間內使用次數很少,那么在將來一段時間內被使用的可能性也很小”的思路。這是一種非常合理的算法,正確地反映了程序的局部性,因為到目前為止最少使用的緩存數據,很可能也是將來最少要被使用的緩存數據。但是這種算法實現起來非常困難,每個數據塊都有一個引用計數,所有數據塊按照引用計數排序,具有相同引用計數的數據塊則按照時間排序。所以該算法的內存消耗和性能消耗較高
  • LRU算法(最久沒有使用算法):該算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那么將來被訪問的幾率也更高”。它把LFU算法中要記錄數量上的"多"與"少"簡化成判斷"有"與"無",因此,實現起來比較容易,同時又能比較好地反映了程序局部性規律。

YYMemoryCache
??YYMemoryCache是YYCache中進行有關內存緩存操作的類,它采用了LRU算法來進行緩存替換。
??YYMemoryCache采用了兩種數據結構:

  • 雙向鏈表:用以實現LRU算法,靠近鏈表頭部的數據使用頻率高,靠近尾部的數據則使用頻率低,可以被替換掉。
  • 字典:采用key-value的方式,可以快速的讀取緩存中的數據

數據結構定義如下:

/**
 鏈表節點,緩存元數據的結構
 */
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; //上一節點指針
    __unsafe_unretained _YYLinkedMapNode *_next; //下一節點指針
    id _key;        
    id _value;
    NSUInteger _cost;           //開銷
    NSTimeInterval _time;       //時間
}
@end

/**
 緩存區域,鏈表與字典的結合
 */
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic;    //緩存字典
    NSUInteger _totalCost;          //全部開銷
    NSUInteger _totalCount;         //個數
    _YYLinkedMapNode *_head;        //鏈表頭
    _YYLinkedMapNode *_tail;        //鏈表尾
    BOOL _releaseOnMainThread;      //是否主線程釋放
    BOOL _releaseAsynchronously;    //是否異步釋放
}

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;

//增、刪、移動到頭部操作
- (void)insertNodeAtHead:(_LinkedMapNode *)node;
- (void)bringNodeToHead:(_LinkedMapNode *)node;
- (void)removeNode:(_LinkedMapNode *)node;
- (_LinkedMapNode *)removeTailNode;
- (void)removeAll;
@end

//新增節點到鏈表頭部
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    //將節點放入字典緩存起來
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    
    //修改總的開銷值和緩存數 
    _totalCost += node->_cost;
    _totalCount++;
    
    //鏈表存在
    if (_head) {
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    }
    //鏈表不存在 
    else {
        _head = _tail = node;
    }
}

//將節點移動到鏈表頭部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    //鏈表中僅一個節點
    if (_head == node) return;
    
    //節點在鏈表尾部
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    }
    //節點在鏈表中間
    else {
        node->_next->_prev = node->_prev;
        node->_prev->_next = node->_next;
    }
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}

??實現了LRU算法,基本YYMemoryCache就實現了一半了,剩下的一半工作于就在如何定時清理無用的緩存了。
??YYMemory包含的數據結構和添加操作如下:

@implementation MemoryCache {
    pthread_mutex_t _lock;      //互斥鎖,保證lru只有一個線程訪問
    _LinkedMap *_lru;           //cache緩存空間
    dispatch_queue_t _queue;    //執行清理操作的串行隊列
}

- (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));
    NSTimeInterval now = CACurrentMediaTime();
    //緩存已存在,修改為新值,并移動到鏈表頭部
    if (node) {
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];
    } 
    //緩存中不存在,創建新節點放入鏈表頭部
    else {
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        [_lru insertNodeAtHead: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);
}

??YYMemoryCache中_trimToCost、_trimToCount、_trimToAge根據緩存的開銷、數量和生存時間來清理cache。當cache被初始化時,會調用_trimRecursively方法,這是一個遞歸執行的方法,通過dispatch_after它會定時地將清理操作放入隊列,在后臺調用_trimInBackground執行清理操作。在_trimInBackground中則會把_trimToCost、_trimToCount、_trimToAge放入隊列_queue中按順序執行。

//定時內存清理,遞歸調用放入清理隊列,YYMemoryCache對象初始化時調用
- (void)_trimRecursively {
    __weak typeof(self) _self = self;
    //dispatch_after在規定時間后,將block放入隊列中
    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];
        //遞歸,實現了定時調用
        [self _trimRecursively];
    });
}

//所謂的后臺,即又開了一個線程執行串行queue中的block,來更行cost、count
- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}

- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    
    //上鎖
    //開銷限制為0,或緩存總開銷小于開銷限制
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    //開鎖
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    //costLimit != 0 && _lru->totalCost > costLimit
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        //pthread_mutex_trylock非阻塞上鎖
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCost > costLimit) {
                //將移除的尾部節點放入holder中
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            //解鎖
            pthread_mutex_unlock(&_lock);
        }
        //若資源已被上鎖,休眠10ms
        else {
            usleep(10 * 1000); //10 ms
        }
    }
    //判斷是否在主線程中釋放
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

磁盤緩存

??磁盤緩存需要用到數據持久化,所謂數據持久化就是將數據保存到磁盤中,使得在應用程序或機器重啟后可以繼續訪問之前保存的數據。在iOS開發,通常使用一下幾種持久化技術:plist文件(屬性列表)、preference(偏好設置)、NSKeyedArchiver(歸檔)、SQLite3、 CoreData。
沙盒機制
??每一個iOS應用程序都會為自己創建一個文件系統目錄(文件夾),這個獨立、封閉、安全的空間,叫做沙盒。每一個應用程序都會擁有一個應用程序沙盒,應用程序沙盒就是一個文件系統目錄。所有的非代碼文件都保存在這個地方,比如圖片、聲音、屬性列表(plist)、sqlite數據庫和文本文件等。
??沙盒機制的特點:

  • 每個應用程序的活動范圍都限定在自己的沙盒里
  • 不能隨意跨越自己的沙盒去訪問別的應用程序沙盒中的內容(iOS8已經部分開放訪問)
  • 應用程序向外請求或接收數據都需要經過權限認證

??應用程序的沙盒目錄下會有三個文件夾Documents、Library(下面有Caches和Preferences目錄)、tmp。

  • Documents:保存應用運行時生成的需要持久化的數據,iTunes會自動備份該目錄。蘋果建議將程序中建立的或在程序中瀏覽到的文件數據保存在該目錄下,iTunes備份和恢復的時候會包括此目錄inBox文件
  • Library/Caches:存放緩存文件,iTunes不會備份此目錄,此目錄下文件不會在應用退出刪除。一般存放體積比較大,不是特別重要的資源。
  • Library/Preferences:保存應用的所有偏好設置,iOS的Settings(設置)應用會在該目錄中查找應用的設置信息,iTunes會自動備份該目錄。您不應該直接創建偏好設置文件,而是應該使用NSUserDefaults類來取得和設置應用程序和偏好。
  • tmp:保存應用運行時所需的臨時數據,使用完畢后再將相應的文件從該目錄刪除。應用沒有運行時,系統也有可能會清除該目錄下的文件,iTunes不會同步該目錄。iPhone重啟時,該目錄下的文件會被刪除。

?? YYDiskCache采用的 SQLite 配合文件的存儲方式,當單條數據小于 20K 時,數據越小 SQLite 讀取性能越高;單條數據大于 20K 時,直接寫為文件速度會更快一些,基于數據庫的緩存可以很好的支持元數據、擴展方便、數據統計速度快,也很容易實現 LRU 或其他淘汰算法。

YYDiskCache
??YYDiskCache的數據結構:

@implementation YYDiskCache {
    YYKVStorage *_kv;       //對數據進行緩存操作的對象
    dispatch_semaphore_t _lock;     //同步鎖,每次僅允許一個線程操作
    dispatch_queue_t _queue;        //執行block的隊列
}

YYDiskCache中需要注意到的數據結構和函數:

static NSMapTable *_globalInstances;        //全局字典,用來存放YYDiskCache對象
static dispatch_semaphore_t _globalInstancesLock;    //互斥鎖,保證每次僅一個線程方法全局字典

//初始化字典和互斥鎖
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];
    });
}

//獲取YYDiskCache對象
static DiskCache *_YYDiskCacheGetGlobal(NSString *path) {
    if (path.length == 0) return nil;
    _YYDiskCacheInitGlobal();
    dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
    id cache = [_globalInstances objectForKey:path];
    dispatch_semaphore_signal(_globalInstancesLock);
    return cache;
}

//保存YYDiskCache對象
static void _YYDiskCacheSetGlobal(DiskCache *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);
}

??每一條存儲路徑下,都對應一個YYDiskCache對象,不同的YYDiskCache都共享一個NSMapTable集合。在創建某條路徑的YYDiskCache對象時,會首先查找集合,若該路徑下的YYDiskCache對象存在,則從集合中獲取。若沒有,則重新創建。這樣在不通路徑下切換時,節省了大量時間。
??NSMapTable對于NSDictionary來說,有幾點特別的地方,其中表現在它可以指定key/value是需要strong,weak,甚至是copy,如果使用的是weak,當key、value在被釋放的時候,會自動從NSMapTable中移除這一項。NSMapTable中可以包含任意指針,使用指針去做檢查操作。
??NSDcitionary或者NSMutableDictionary中對于key和value的內存管理是,對key進行copy,對value進行強引用。NSDcitionary中對于key的類型,是需要key支持NSCopying協議,并且在NSDictionary中,object是由“key”來索引的,key的值不能改變,為了保證這個特性在NSDcitionary中對key的內存管理為copy,在復制的時候需要考慮對系統的負擔,因此key應該是輕量級的,所以通常我們都用字符串和數字來做索引,但這只能說是key-to-object映射,不能說是object-to-object的映射。
??NSMapTabTable更適合于我們一般所說的映射標準,它既可以處理key-to-value又可以處理object-to-object
??YYDiskCache中實現定時清理緩存的方式與YYMemoryCache一樣,首先在初始化中調用_trimRecursively方法。_trimRecursively方法的實現就是遞歸和dispatch_after結合的方式。
??初始化方法與添加數據緩存方法

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    //從NSMapTable里取出cache
    DiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    //創建cache,設置緩存類型
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
    KVStorage *kv = [[KVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    
    _kv = kv;
    _path = path;
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);    //執行block的并發隊列
    _inlineThreshold = threshold;
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _freeDiskSpaceLimit = 0;
    _autoTrimInterval = 60;
    
    //清理緩存
    [self _trimRecursively];
    
    //放入NSMapTable中緩存
    _YYDiskCacheSetGlobal(self);
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
    return self;
}

//添加緩存
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    if (!key) return;
    //添加數據為空,則從緩存中移除
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    
    //獲取擴展數據,用戶可以在存儲緩存數據前調用類方法設置擴展數據
    NSData *extendedData = [DiskCache getExtendedDataFromObject:object];
    NSData *value = nil;
    //自定義block進行歸檔操作
    if (_customArchiveBlock) {
        value = _customArchiveBlock(object);
    } 
    //系統歸檔的方式
    else {
        @try {
            value = [NSKeyedArchiver archivedDataWithRootObject:object];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (!value) return;
    NSString *filename = nil;
    //緩存方式不為sqlite類型且數據大小超過規定值,獲取文件名
    if (_kv.type != YYKVStorageTypeSQLite) {
        if (value.length > _inlineThreshold) {
            filename = [self _filenameForKey:key];
        }
    }
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}

//根據key值創建緩存文件名
- (NSString *)_filenameForKey:(NSString *)key {
    NSString *filename = nil;
    //自定義block的到文件名
    if (_customFileNameBlock) filename = _customFileNameBlock(key);
    //md5加密得到文件名
    if (!filename) filename = _YYNSStringMD5(key);
    return filename;
}


/**
清理大小為targetFreeDiskSpace的磁盤空間
 */
- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
    if (targetFreeDiskSpace == 0) return;
    
    //磁盤已緩存的大小
    int64_t totalBytes = [_kv getItemsSize];
    if (totalBytes <= 0) return;
    
    //磁盤可用空間大小
    int64_t diskFreeBytes = _YYDiskSpaceFree();
    if (diskFreeBytes < 0) return;
    
    //磁盤需要清理的大小 = 目標要清除的空間大小 - 可用空間大小
    int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
    if (needTrimBytes <= 0) return;
    
    //磁盤緩存的空間大小限制 = 已緩存大小 - 需要清理的空間大小
    int64_t costLimit = totalBytes - needTrimBytes;
    if (costLimit < 0) costLimit = 0;
    [self _trimToCost:(int)costLimit];
}

//磁盤空閑的大小
static int64_t _YYDiskSpaceFree() {
    NSError *error = nil;
    //獲取主目錄的文件屬性,主目錄下包含Document、Liberary等目錄
    NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:&error];
    if (error) return -1;
    //獲取主目錄的可用空間,即磁盤的可用空間
    int64_t space =  [[attrs objectForKey:NSFileSystemFreeSize] longLongValue];
    if (space < 0) space = -1;
    return space;
}

文件目錄地址結構和數據庫表結構

??上圖是YYDiskCache中文件存放的路徑和數據庫表的結構。在每個path下面,都有data和trash文件夾,其中data文件是存放數據的文件緩存,文件名都是通過md5加密的,trash則是在放置丟棄的緩存文件的文件夾。此外path下的manifest.sqlite則是數據庫的文件,manifest.sqlite-shm和manifest.sqlite-wal是sqlite數據庫WAL機制所需文件。
??WAL機制的原理是:修改并不直接寫入到數據庫文件中,而是寫入到另外一個稱為WAL的文件中;如果事務失敗,WAL中的記錄會被忽略,撤銷修改;如果事務成功,它將在隨后的某個時間被寫回到數據庫文件中,提交修改。
YYKVStorage
??YYKVStorage中對文件緩存讀寫操作實現非常簡單:

- (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data {
    NSString *path = [_dataPath stringByAppendingPathComponent:filename];
    return [data writeToFile:path atomically:NO];
}

- (NSData *)_fileReadWithName:(NSString *)filename {
    NSString *path = [_dataPath stringByAppendingPathComponent:filename];
    NSData *data = [NSData dataWithContentsOfFile:path];
    return data;
}

- (BOOL)_fileDeleteWithName:(NSString *)filename {
    NSString *path = [_dataPath stringByAppendingPathComponent:filename];
    return [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
}

??SQLite3的使用過程大致如下:

  • sqlite3_open():打開數據庫這個函數打開一個sqlite數據庫文件的連接并且返回一個數據庫連接對象。

  • sqlite3_prepare():這個函數將sql文本轉換成一個準備語句(prepared statement)對象,同時返回這個對象的指針。這個接口需要一個數據庫連接指針以及一個要準備的包含SQL語句的文本。它實際上并不執行(evaluate)這個SQL語句,它僅僅為執行準備這個sql語句。sqlite3_prepare執行代價昂貴,所以通常盡可能的重用prepared語句

  • sqlite3_setp():這個過程用于執行有前面sqlite3_prepare創建的準備語句。這個語句執行到結果的第一行可用的位置。繼續前進到結果的第二行的話,只需再次調用sqlite3_setp()。繼續調用sqlite3_setp()知道這個語句完成,那些不返回結果的語句(如:INSERT,UPDATE,或DELETE),sqlite3_step()只執行一次就返回

  • sqlite3_column():每次sqlite3_step得到一個結果集的列停下后,這個過程就可以被多次調用去查詢這個行的各列的值。對列操作是有多個函數,均以sqlite3_column為前綴

  • sqlite3_finalize:這個過程銷毀前面被sqlite3_prepare創建的準備語句,每個準備語句都必須使用這個函數去銷毀以防止內存泄露。

  • sqlite3_close:這個過程關閉前面使用sqlite3_open打開的數據庫連接,任何與這個連接相關的準備語句必須在調用這個關閉函數之前被釋放

數據庫操作,以添加緩存數據為例(其它操作都與此類似)

- (BOOL)_dbOpen {
    if (_db) return YES;
    
    int result = sqlite3_open(_dbPath.UTF8String, &_db);
    if (result == SQLITE_OK) {
        CFDictionaryKeyCallBacks keyCallbacks = kCFCopyStringDictionaryKeyCallBacks;
        CFDictionaryValueCallBacks valueCallbacks = {0};
        _dbStmtCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &keyCallbacks, &valueCallbacks);
        _dbLastOpenErrorTime = 0;
        _dbOpenErrorCount = 0;
        return YES;
    } else {
        _db = NULL;
        if (_dbStmtCache) CFRelease(_dbStmtCache);
        _dbStmtCache = NULL;
        _dbLastOpenErrorTime = CACurrentMediaTime();
        _dbOpenErrorCount++;
        
        if (_errorLogsEnabled) {
            NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result);
        }
        return NO;
    }
}

- (BOOL)_dbClose {
    if (!_db) return YES;
    
    int  result = 0;
    BOOL retry = NO;
    BOOL stmtFinalized = NO;
    
    if (_dbStmtCache) CFRelease(_dbStmtCache);
    _dbStmtCache = NULL;
    
    do {
        retry = NO;
        result = sqlite3_close(_db);
        if (result == SQLITE_BUSY || result == SQLITE_LOCKED) {
            if (!stmtFinalized) {
                stmtFinalized = YES;
                sqlite3_stmt *stmt;
                while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) {
                    sqlite3_finalize(stmt);
                    retry = YES;
                }
            }
        } else if (result != SQLITE_OK) {
            if (_errorLogsEnabled) {
                NSLog(@"%s line:%d sqlite close failed (%d).", __FUNCTION__, __LINE__, result);
            }
        }
    } while (retry);
    _db = NULL;
    return YES;
}

- (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_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) {
        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);
    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;
}

- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql {
    if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL;
    sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql));
    if (!stmt) {
        int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
        if (result != SQLITE_OK) {
            if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
            return NULL;
        }
        CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt);
    } else {
        sqlite3_reset(stmt);
    }
    return stmt;
}

??YYKVStorage中會創建一個字典,用以存儲準備語句對象,這樣在下次進行同樣的操作時,就能對之前創建的準備語句對象進行服用,減少了不必要的耗時。當要關閉數據庫時,通過sqlite3_next_stmt不斷的獲取準備對象,然后使用sqlite3_finalize進行銷毀,避免內存泄漏
??YYKVStorage提供給外部使用的初始化方法和添加緩存方法(其它操作與此類似)

- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {
    if (path.length == 0 || path.length > kPathLengthMax) {
        NSLog(@"YYKVStorage init error: invalid path: [%@].", path);
        return nil;
    }
    if (type > YYKVStorageTypeMixed) {
        NSLog(@"YYKVStorage init error: invalid type: %lu.", (unsigned long)type);
        return nil;
    }
    
    self = [super init];
    _path = path.copy;
    _type = type;
    _dataPath = [path stringByAppendingPathComponent:kDataDirectoryName];
    _trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName];
    _trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL);
    _dbPath = [path stringByAppendingPathComponent:kDBFileName];
    _errorLogsEnabled = YES;
    NSError *error = nil;
    //創建文件系統目錄
    if (![[NSFileManager defaultManager] createDirectoryAtPath:path
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error] ||
        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kDataDirectoryName]
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error] ||
        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kTrashDirectoryName]
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error]) {
        NSLog(@"YYKVStorage init error:%@", error);
        return nil;
    }
    
    //打開sqlite
    if (![self _dbOpen] || ![self _dbInitialize]) {
        // db file may broken...
        [self _dbClose];
        [self _reset]; // rebuild
        if (![self _dbOpen] || ![self _dbInitialize]) {
            [self _dbClose];
            NSLog(@"YYKVStorage init error: fail to open sqlite db.");
            return nil;
        }
    }
    
    [self _fileEmptyTrashInBackground]; // empty the trash if failed at last time
    return self;
}

- (void)dealloc {
    UIBackgroundTaskIdentifier taskID = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{}];
    [self _dbClose];
    if (taskID != UIBackgroundTaskInvalid) {
        [_YYSharedApplication() endBackgroundTask:taskID];
    }
}

- (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 {
        //根據key找到文件名,若文件存在,則將紀錄從文件系統中刪除
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        //紀錄存入數據庫
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

??通過對YYMemoryCache和YYDiskCache的代碼進行分析后,在來看YYCache提供的接口實現,其實就非常簡單了,YYCache包含YYMemoryCache和YYDiskCache對象,當進行緩存操作時,分別對這兩個對象進行操作,就實現了內存緩存和磁盤緩存。
??YYCache的初始化方法和緩存查詢方法(添加、刪除操作都與此類似)

- (instancetype)initWithName:(NSString *)name {
    if (name.length == 0) return nil;
    NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *path = [cacheFolder stringByAppendingPathComponent:name];
    return [self initWithPath:path];
}

- (instancetype)initWithPath:(NSString *)path {
    if (path.length == 0) return nil;
    YYDiskCache *diskCache = [[YYDiskCache alloc] initWithPath:path];
    if (!diskCache) return nil;
    NSString *name = [path lastPathComponent];
    YYMemoryCache *memoryCache = [YYMemoryCache new];
    memoryCache.name = name;
    
    self = [super init];
    _name = name;
    _diskCache = diskCache;
    _memoryCache = memoryCache;
    return self;
}

- (id<NSCoding>)objectForKey:(NSString *)key {
    id<NSCoding> object = [_memoryCache objectForKey:key];
    if (!object) {
        object = [_diskCache objectForKey:key];
        if (object) {
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}

參考文檔

兩種常見的緩存淘汰算法LFU&LRU
緩存算法(頁面置換算法)-FIFO、LFU、LRU
iOS緩存機制詳解
關于NSMapTable
YYCache 設計思路
YYCache源碼分析(一)
YYCache源碼分析(二)
YYCache源碼分析(三)
沙盒機制
SQLite的WAL機制
sqlite3用法詳解草稿

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、深復制和淺復制的區別? 1、淺復制:只是復制了指向對象的指針,即兩個指針指向同一塊內存單元!而不復制指向對象的...
    iOS_Alex閱讀 1,424評論 1 27
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,212評論 30 472
  • 西西堅信,路邊的垃圾桶、還沒掉下來的樹葉、從房子上升起的炊煙,都會指引他回家。 我第一次見到西西是在一個暖和的下午...
    吞二火閱讀 370評論 0 0
  • 人。一半是天使。一半是魔鬼。這是真的。每個人都有兩面性。無謂好壞。無謂真偽。就像自然界的生態平衡。有天就有地。有晝...
    c57e6754061d閱讀 357評論 0 2
  • 業務,產品,運營,市場,營收 各個數據如何歸納進一個后臺里。
    charler閱讀 620評論 2 1