YYCach 源码小记

  • YYCache中,永远都是先访问内存缓存,然后再访问磁盘缓存(包括了写入,读取,查询,删除缓存的操作)。而且关于内存缓存(_memoryCache)的操作,是不存在block回调的。

  • YYCache 如果不指定存储方式,默认存入内存的时候同时写入磁盘

  • 在读取缓存的操作中,如果在内存缓存中无法获取对应的缓存,则会去磁盘缓存中寻找。如果在磁盘缓存中找到了对应的缓存,则会将该对象再次写入内存缓存中,保证在下一次尝试获取同一缓存时能够在内存中就能返回,提高速度。

  • 双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
    双向链表相对与单向链表来说多,在每个节点中多了一个指向前驱的prior指针。这样在删除一个节点时操作会比较方便。

    为什么不选择单向链表:单链表的节点只知道它后面的节点(只有指向后一节点的指针),而不知道前面的。所以如果想移动其中一个节点的话,其前后的节点不好做衔接。

  • YYDiskCache与YYMemoryCache的相同点:
    都具有查询,写入,读取,删除缓存的接口。
    不直接操作缓存,也是间接地通过另一个类(YYKVStorage)来操作缓存。
    它使用LRU算法来清理缓存。
    支持按 cost,count 和 age 这三个维度来清理不符合标准的缓存。

  • LRU淘汰算法:
    LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”;
    新数据插入到链表头部;
    每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
    当链表满的时候,将链表尾部的数据丢弃。

  • 在YYMemoryCache中,使用了双向链表这个数据结构来保存这些缓存:
    当写入一个新的缓存时,要把这个缓存节点放在链表头部,并且并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
    当访问一个已有的缓存时,要把这个缓存节点移动到链表头部,原位置两侧的缓存要接上,并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
    (根据清理维度)自动清理缓存时,要从链表的最后端逐个清理。

  • YYMemoryCache不同点是:
    根据缓存数据的大小来采取不同的形式的缓存:
    数据库sqlite: 针对小容量缓存,缓存的data和元数据都保存在数据库里。
    文件+数据库的形式: 针对大容量缓存,缓存的data写在文件系统里,其元数据保存在数据库里。
    除了 cost,count 和 age 三个维度之外,还添加了一个磁盘容量的维度。

  • YYDiskCache 缓存数据的长度大于20kb,就使用文件存储;如果小于这个值,就是用sqlite存储

  • YYKVStorage实例负责保存和管理所有磁盘缓存

  • YYKVStorage 读写缓存模式:
    YYKVStorageTypeFile 文件读取
    YYKVStorageTypeSQLite 数据库读写
    YYKVStorageTypeMixed 根据策略决定使用文件还是数据库读写数据

  • YYKVStorage 缓存:
    都是通过saveItemWithKey: value: filename: extendedData:方法将缓存数据写入硬盘;
    写入时,判断filename是否存在,不存在则把value存入数据库,反之value就不存入;
    无论filename是否存
    在,key、filename、extendedData都会写入数据库;
    当filename为空时,会先在数据库中查找key对应的filename, 如果filename存在,从文件中删除,再存入数据库,不存在则直接存入数据库。

  • YYMemoryCache是利用key-value机制内存缓存类,所有的方法都是线程安全的: 调用增删时,都会用互斥锁pthread_mutex_lock来的保证线程安全。

  • 互斥锁pthread_mutex_lock :
    互斥锁是在多线程程序中同步访问手段是使用互斥量。程序员给某个对象加上一把“锁”,每次只允许一个线程去访问它。如果想对代码关键部分的访问进行控制,你必须在进入这段代码之前锁定一把互斥量,在完成操作之后再打开它。

@implementation YYMemoryCache {
   // 声明互斥锁
   pthread_mutex_t _lock;
   .....
}
- (instancetype)init {
   self = super.init;
   //初始化互斥锁
   pthread_mutex_init(&_lock, NULL);
   ......
   return self;
}

- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread {
   //加琐
   pthread_mutex_lock(&_lock);

   _lru->_releaseOnMainThread = releaseOnMainThread;

   //解琐
   pthread_mutex_unlock(&_lock);
}

- (void)dealloc{
   //释放该锁的数据结构
   pthread_mutex_destroy(& _lock);  
}

注意点:
1、互斥量需要时间来加锁和解锁。锁住较少互斥量的程序通常运行得更快。所以,互斥量应该尽量少,够用即可,每个互斥量保护的区域应则尽量大。
2、互斥量的本质是串行执行。如果很多线程需要领繁地加锁同一个互斥量,
则线程的大部分时间就会在等待,这对性能是有害的。如果互斥量保护的数据(或代码)包含彼此无关的片段,则可以特大的互斥量分解为几个小的互斥量来提高性能。这样,任意时刻需要小互斥量的线程减少,线程等待时间就会减少。所以,互斥量应该足够多(到有意义的地步),每个互斥量保护的区域则应尽量的少。

为什么使用互斥锁:
互斥锁缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务,但对于内存缓存的存取来说,它非常合适。

  • YYDiskCache 则选择了更适合它的 dispatch_semaphore 来保证线程安全:
    dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源,YYDiskCache在写入比较大的缓存时,可能会有比较长的等待时间,所以采用dispatch_semaphore

  • 内存警告和进入后台的监听
    YYCache默认在收到内存警告和进入后台时,自动清除所有内存缓存,清除缓存默认在子线程中进行。

  • static inline 内联函数:
    修饰函数,可以当作一个宏定义调用
    inline内联函数的说明:
    1.内联函数只是我们向编译器提供的申请,编译器不一定采取inline形式调用函数
    2.内联函数不能承载大量的代码.如果内联函数的函数体过大,编译器会自动放弃内联
    3.内联函数的定义须在调用之前

优点相比于函数:
inline函数避免了普通函数的,在汇编时必须调用call的缺点:取消了函数的参数压栈,减少了调用的开销,提高效率.所以执行速度确比一般函数的执行速度要快,集成了宏的优点,使用时直接用代码替换(像宏一样);

优点相比于宏:
1、避免了宏的缺点:需要预编译.因为inline内联函数也是函数,不需要预编译.
2、编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。
3、可以使用所在类的保护成员及私有成员。

inline函数注意事项:
1、你可以使用inline函数完全取代表达式形式的宏定义。
2、内联函数一般只会用在函数内容非常简单的时候,这是因为,内联函数的代码会在任何调用它的地方展开,如果函数太复杂,代码膨胀带来的恶果很可能会大于效率的提高带来的益处。
3、在内联函数内不允许用循环语句和 开关语句。如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数(自己调用自己的函数)是不能被用来做内联函数的。内联 函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现,函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

你可能感兴趣的:(YYCach 源码小记)