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用法详解草稿

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 一、深复制和浅复制的区别? 1、浅复制:只是复制了指向对象的指针,即两个指针指向同一块内存单元!而不复制指向对象的...
    iOS_Alex阅读 1,346评论 1 27
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,107评论 29 470
  • 西西坚信,路边的垃圾桶、还没掉下来的树叶、从房子上升起的炊烟,都会指引他回家。 我第一次见到西西是在一个暖和的下午...
    吞二火阅读 344评论 0 0
  • 人。一半是天使。一半是魔鬼。这是真的。每个人都有两面性。无谓好坏。无谓真伪。就像自然界的生态平衡。有天就有地。有昼...
    c57e6754061d阅读 332评论 0 2
  • 业务,产品,运营,市场,营收 各个数据如何归纳进一个后台里。
    charler阅读 611评论 2 1