YYKit-YYCache分析

YYCache设计分析
1. YYCache源码分析

image

YYMemoryCache
1. YYMemoryCache如何高效的根据用户习惯调整缓存?

首先,YYMemoryCache使用LRU缓存算法,即最近最久未使用算法。根据用户习惯,用户使用了的缓存资源一般后面会再次使用。而实现LRU算法,YYMemoryCache类中使用了链表+hashMap的组合实现。_YYLinkedMap作为实现了LRU算法的缓存管理对象,其中使用CFMutableDictionaryRef保存key与对应的_YYLinkedMapNode节点,而__YYLinkedMapNode节点包括了key、value、节点的前一个节点node、节点的后一个节点对象、修改时间、消耗等。通过__YYLinkedMap实现了缓存链表+hashmap存储。

其次,在初始化YYMemoryCache过程中,程序实现了一个定时器来定时检查缓存是否达到了相关限制(最大内存大小、最晚期限、最大的缓存数量)

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];
});

- (void)_trimInBackground {
dispatch_async(_queue, ^{
    [self _trimToCost:self->_costLimit];
    [self _trimToCount:self->_countLimit];
    [self _trimToAge:self->_ageLimit];
});

同时在程序监听了相关通知:

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];

从而实现内存缓存的清除工作。

2. 对象项目中多线程对LRU算法实例的争抢,YYMemonryCache是如何解决的呢?

首先,了解下当前比较高效的线程锁,如下:

os_unfair_lock/OSSpinLock > dispatch_semaphore > pthread_mutex > NSLock > NSCondition > pthread_mutex(recursive) > NSRecursiveLock > NSConditionLock > @synchronized

对于OSSpinLock而言,它底层是通过do while实现的,对于需要等待的任务,OSSpinLock会占用cpu的资源,而对于不怎么需要等待的任务,OSSpinLock是比较合适的。对于内存缓存而言,OSSpinLock是在适合不过了。但由于OSSpinLock中设定了高优先级线程始终会在低优先级线程前执行,从而出现优先级反转问题,即OSSpinLock不再安全。苹果在ios10后已经使用os_unfair_lock代替了OSSpinLock.

dispatch_semaphore 通过设置信号总量实现锁的操作。对于需要等待的任务,dispatch_semaphore性能下降的很厉害,但是对于无需多等的任务,性能比pthread_mutex好的多。但是相对OSSpinLock而言,dispatch_semaphore不会占用cpu。此项目磁盘缓存中,使用dispatch_semaphore打造锁一方面dispatch_semaphore相对性能比较好,同时在等待过程中不会占用cpu资源。

pthread_mutex
互斥锁,性能一直比较高。
等等。

而YYMemonryCache中使用的是pthread_mutex,而不是dispatch_semaphore,主要YYCache作者考虑

“OSSpinLock 和 dispatch_semaphore 都不会产生特别明显的死锁,所以我也无法确定用 dispatch_semaphore 代替 OSSpinLock 是否正确。能够肯定的是,用 pthread_mutex 是安全的”

在项目中,设计人使用了pthread_mutex_t来保证线程安全,具体操作如下:

    pthread_mutex_t _lock;
    pthread_mutex_init(&_lock, NULL);

    使用过程中:
    pthread_mutex_lock(&_lock)
    具体操作
    pthread_mutex_unlock(&_lock)

    最后在dealloc中
    -(void)dealloc{

        pthread_mutex_destroy(&_lock)
    }
3. 考虑到缓存性能,YYMemonryCache是如何提高内存缓存的读写性能?

对于内存缓存来说,读写都是很快的,单独在YYMemonryCache中没有提供独立的子线程处理。而对于对象的释放,使用的异步线程处理。在YYCache中,单独为内存缓存的操作提供了异步线程处理的接口实现,用户根据情况可以选择。

4. 项目中是如何实现异步释放对象处理

通过将对象放入并行队列中进行操作,从而让block捕获了对象,block中操作完成后,block被释放,这个时候block中的变量由于引用计数为0,从而在异步线程中得到了释放。

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

        //其中将要释放的对象放入异步线程中,block自动捕获了node对象,当block执行完毕的时候,node在block中引用计数为0,从而在异步线程中得到了释放

        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
        });
    }
}
5. 为什么不用NSCache而使用CFMutableDictionaryRef和链表进行保存操作呢?
  1. NSCache 没有明确的控制缓存的机制
  2. NSCache 没有提供age、cost、count等方式控制缓存
  3. NSCache 它的性能和 key 的相似度有关,如果有大量相似的 key (比如 “1”, “2”, “3”, …),NSCache 的存取性能会下降得非常厉害,大量的时间被消耗在 CFStringEqual() 上,不知这是不是 NSCache 本身设计的缺陷.

为了提供以上的NSCache的不足,我们使用CFMutableDictionaryRef+链表的方式实现LRU算法。

YYDiskCache
1. 具体的存储策略是什么?

1、我们得了解sqlite3数据库中保存的相关字段:

key text, 唯一标记资源的key,比如图片路径等等

filename text, key被md5之后的64位字符串,根据资源大小限制判断是否通过key生成对应的filename,如果资源存在filename,则使用file存储。

size integer, 资源的大小

inline_data blob,资源的二进制值,如果是文件存储,则此值为NULL,如果是sql存储,则此值为对应的二进制数值。blob类型表示原始数据二进制,不进行转换

modification_time integer, 文件修改时间

last_access_time integer, 文件最近访问时间

extended_data blob 文件的元数据,一般为null

2、初始化YYKVStorage的时候,会设置存储类型:

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

   YYKVStorageType type;
  if (threshold == 0) {
    type = YYKVStorageTypeFile;
  } else if (threshold == NSUIntegerMax) {
    type = YYKVStorageTypeSQLite;
  } else {
    type = YYKVStorageTypeMixed;
  }

首先根据threshold设置支持的类型,如果threshold为0 ,表示必须用文件存储,如果threshold为无限大,则用sql存储。
其次在磁盘缓存的时候,首先判断type,如果type为sql,则直接使用sql存储,不用文件存储,如果不是sql,则根据key生成对应的文件名,后面根据文件名存在与否进行文件存储还是sql存储。

当YYDiskCache读取/删除/更新等操作的时候,根据type的值:如果选择YYKVStorageTypeSQLite,则只进行sql存储(inline_data为二进制值).如果选择YYKVStorageTypeFile/YYKVStorageTypeMixed,则表示文件存储,这种模式一方面会将索引信息存储db(inline_data为null),另一方面存储文件。

由于sql索引速度很快,所以对于索引类,我们使用sql操作。由于对于文件大小是20kb或者更小的,用sql存储比较快,而如果大小超过了20kb,则使用文件存储比较快。

为了实现对磁盘缓存的LRU算法,对于使用文件存储的资源,一方面我们将资源的相关索引信息(以上的字段)保存到数据库,另一方面将资源以filename的形式存储到文件中。当要根据cost、time、count来控制磁盘缓存时,我们先通过db根据const、time、count查找到对应的资源,然后进行资源的控制。

2. 磁盘的IO操作是比较阻塞主线程的,项目中如何保证磁盘操作不阻塞主线程呢?

在YYDiskCache中,使用dispatch_semaphore实现了锁的操作。由于磁盘读写,一般等待相对内存读取会慢很多,而dispatch_semaphore在等待过程中不会占用cpu,从而优化了性能。

     #define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)

     #define Unlock() dispatch_semaphore_signal(self->_lock)

在YYDiskCache中,针对每个操作类中都提供了同步和异步方法,在异步方法中,YYDiskCache使用dispatch_semaphore来同步YYKVStorage的读写操作。

3. YYKVStorage用于调度sqlite3和file存储,为什么不在YYKVStorage中直接使用多线程和同步操作呢?

一个功能模块尽可能实现单一职责,而对于多线程还是同步线程的选择,ibireme直接提供接口让用户进行选择,这样做使得模块扩展性更好,逻辑性更加独立,减少了bug的出现。

4. YYDiskCache中还有哪些性能优化点:
  1. 使用NSMapTable 来针对path保存对应的YYDiskStorage对象,因为YYDiskStorage是比较大的对象,一般使用单例或者唯一存储来防止创建耗用大量性能。

  2. 在对资源进行存储之前,YYDiskCache会对资源使用NSKeyedArchiver进行压缩,从而减少文件的大小。在对资源进行读取的时候,YYDiskCache会将读取的data进行NSKeyedUnArchiver操作,从而对资源进行解压缩操作。

  3. sqlite3中操作过程中,YYKVStorage使用字典将sql-stm保存起来,当同样的sql操作出现,则从字典中直接取出stm进行操作。

  4. sqlite3中使用wal模式,使用WAL模式时,改写操是附加(append)到WAL文件,而不改动数据库文件,因此数据库文件可以被同时读取。当执行checkpoint操作时,WAL文件的内容会被写回数据库文件。当WAL文件达到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默认值是1000)页(默认大小是1KB)时,会自动使用当前COMMIT的线程来执行checkpoint操作

等等

总结:

  1. YYCache整体的设计结构清晰、每个模块职责单一。
  2. 针对缓存设计,YYCache兼顾了一下设计点:
    1.1 做到内存缓存+磁盘缓存
    1.2 在多线程的情况下做到线程安全
    1.3 针对缓存的控制,实现了LRU算法,同时可以根据cost、time、count来动态控制缓存。
    1.4 性能优化:
    1. 对于对象的释放延迟到了异步队列
    2. 锁的选择,根据内存和磁盘的使用场景选择对应的线程锁
    3. 数据库的stm缓存及sql的wal模式
    4. YYDiskCache被NSMapTable单例管理
    5. 项目中使用CoreFundation性能更好,但是经过测试发现NSDictionary性能反而更好。

参考资料:
https://blog.ibireme.com/2015/10/26/yycache/
http://www.bubuko.com/infodetail-765226.html
http://www.cnblogs.com/hustcat/archive/2009/03/01/1400757.html
https://blog.csdn.net/zzqhost/article/details/7840259
https://juejin.im/post/59f6e3b051882534af253d4a

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值