YYCache源码分析

YYMemoryCache


YYMemoryCache用于对内存缓存进行管理,与SDWebImage对于内存缓存管理策略的区别是,SDWebImage对于内存缓存的管理是基于系统的NSCache类,而YYMemoryCache是基于作者自定义的双向链表,并基于链表自定义了一套淘汰算法来对内存使用进行性能优化。

_YYLinkedMap和_YYLinkedMapNode

既然有自定义链表,必然也有自定义的链表节点,关于链表节点声明如下:
@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

@implementation _YYLinkedMapNode
@end

既然_YYLinkedMap是双向链表,那么结点_YYLinkedMapNode的数据结构中必然会有两个指针,_prev指向链接当前结点的上一个结点,_next指向当前结点链接的下一个结点。

_key可以理解为它是当前缓存数据的标识符,比方对于UIImage对象来说,它的key就是URL,那么_value就是这个UIImage对象。

_cost表示当前缓存数据的内存占用情况,打个比方,在YYImageCahce中对于UIImage对象的内存计算是通过获取图片的高度和行数再将这两个数据进行相乘的结果,代码如下:

- (NSUInteger)imageCost:(UIImage *)image {
    CGImageRef cgImage = image.CGImage;
    if (!cgImage) return 1;
    CGFloat height = CGImageGetHeight(cgImage);
    size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
    NSUInteger cost = bytesPerRow * height;
    if (cost == 0) cost = 1;
    return cost;
}

而在SDWebImageSDImageCache中,对于UIImage的内存计算是UIImagewidthheight以及scale相乘的结果:

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}

最后结点还有一个成员变量_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, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

成员变量_dic用于存放_YYLinkedMapNode结点数据,key就是结点的key值,value就是结点本身。另外_dic的类型是Core Foundation层的CFMutableDictionaryRef

关于_totalCost_totalCount这两个变量,意思和NSCache中的totalCostLimitcountLimit差不多,_totalCost表示图片的内存总占用,_totalCount表示总的个数,这两个变量用于后续实现淘汰算法。

关于_head_tail变量,这两个变量的类型都是_YYLinkedMapNode,后面的注释已经解释了这两个变量的作用,
_head是最近使用较多的结点(MRU),_tail最近使用最少的结点(LRU)。

关于_releaseOnMainThread_releaseAsynchronously变量,如果_releaseOnMainThread = YES,就会在主线程释放_dic变量,如果_releaseAsynchronously = YES,就会获取一个专门用来做release操作的异步队列来释放_dic

_YYLinkedMap也提供了一些操作结点的方法:

1.向表头插入一个结点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

2.将链表内某个结点移至表头
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

3.删除某个结点
- (void)removeNode:(_YYLinkedMapNode *)node;

4.删除使用最少的结点
- (_YYLinkedMapNode *)removeTailNode;

5.删除所有结点
- (void)removeAll;

YYMemoryCache

@implementation YYMemoryCache {
    pthread_mutex_t _lock;
    _YYLinkedMap *_lru;
    dispatch_queue_t _queue;
}

YYMemoryCache初始化做了这些事:
1、初始化互斥锁_lock
2、初始化_lru
3、初始化串行队列_queue
4、对_countLimit、_costLimit、_ageLimit、_autoTrimInterval设置默认值。
5、对_shouldRemoveAllObjectsOnMemoryWarning_shouldRemoveAllObjectsWhenEnteringBackground设置默认值为YES,接收到内存警告或程序进入后台都会清空内存。

YYMemoryCache提供了一些访问方法:

#pragma mark - Access Methods
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;

#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;

Access Methods下的几个方法都是操作链表_lru及其结点。

trim下的3个方法就是淘汰算法的实现,也是对双向链表的操作代码,淘汰的纬度有3个,包括内存管理容器所存储结点的数量、结点的开销、结点的使用频率等。

这里有个tip,同时也是作者分享的,就是让block捕获一个局部变量,然后扔到后台队列去随便发送个消息以避免编译器警告,这样就可以让对象在后台线程销毁:

NSMutableArray *holder = [NSMutableArray new];

if (holder.count) {
    dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
    dispatch_async(queue, ^{
         [holder count]; // release in queue
    });
}

作者还提供了一个看起来简单点的实现方式:

NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
    [tmp class];
});

另外,在YYMemoryCache初始化方法当中,会调用_trimRecursively方法,该方法每隔5秒中就会调用自己一次,另外,在这个方法中还会调用调用Trim下的3个方法对LRU进行清理,以节省内存。

以上就是YYMemoryCache的实现。

YYKVStorage


在了解YYDiskCache的实现原理前需要先了解一下YYKVStorage

作者是这样介绍YYKVStorage的:
YYKVStorage是一个基于sqlite和文件系统的键值存储。但作者不建议我们直接使用此类(ps:这个类被封装到了YYDiskCache里,可以通过YYDiskCache间接使用此类),这个类只有一个初始化方法,即initWithPath:type:,初始化后,讲根据path创建一个目录来保存键值数据,初始化后,如果没有得到当前类的实例对象,就表示你不应该对改目录进行读写操作。最后,作者还写了个警告,告诉我们这个类的实例对象并不是线程安全的,你需要确保同一时间只有一条线程访问实例对象,如果你确实需要在多线程中处理大量数据,可以把数据拆分到多个实例对象当中去。

YYKVStorage提供了一些public方法用于操作sqlite和文件系统,这些方法覆盖了增删改查这4个操作。

基于sqlite的键值存储

YYKVStorage实现了对sqlite的封装,包括数据库初始化、打开数据库、关闭数据库、执行sql等操作。这个类中绝大多数代码也是对这些操作的实现代码。

为了加强数据库检索时的性能,在建表的同时又为表建立了索引。

表名叫manifest,表内有几个字段:

key:主键,增删改查操作都围绕这个主键来完成
filename:文件名称
size:文件大小
inline_data:存储的二进制数据
modification_time:文件修改时间
last_access_time:文件最后访问时间
extended_data:文件扩建时间
1. 保存

方法:saveItemWithKey:value:filename:extendedData:

数据保存的目标地点分为两种,一种是直接放在沙盒指定目录下,另外一种是存储在数据库(虽然db也是放在沙盒里的),具体采用哪种目标地点会根据filename做判断,如果filename存在即表明数据是直接存储在沙盒某个目录下的文件里,如果filename不存在就会走和数据库相关的流程。

如果是写入文件:
  • 会根据filename生成完整的存储路径,再把value写入到目标文件中。
    • 如果写入成功不再执行后续代码。
    • 如果写入失败,则尝试把数据写入到数据库中。
      • 如果写入成功不再执行后续代码;
      • 如果数据库也写入失败了,就会删除前面生成的存储路径下的文件,避免产生垃圾文件。
如果是写入数据库:
  • 首先会判断当前的storage对象是不是用来做数据库缓存操作的实例对象。
    • 如果是,根据方法传入的参数重新向数据库中插入数据。
    • 如果不是,根据key到数据库里查询对应的filename,根据filename删除相应路径下的文件,最后根据方法传入的参数重新向数据库中插入数据。
2. 查询
方式一:直接获取二进制数据:
(1)从文件中查询:

1.根据key从数据库查找到对应的filename
2.根据步骤1中查询到的filename生成完成的文件存储路径并读取文件数据,更新数据中该条数据的访问时间。
3.如果步骤2中没读取到数据,则把这条数据从数据库中删除。

(2)从数据库中查询:

1.直接根据key到数据库中查询inline_data,这个inline_data对应着YYKVStorageItem中的value属性。
2.更新数据中该条数据的访问时间

(3)混合查询(文件&数据库):

1.根据key找到filename
2.filename存在读取文件数据,如果不存在数据则删除文件。
3.filename不存在则去数据库中通过key获取inline_data
4.更新数据中该条数据的访问时间

以上3个查询操作获取的均是单纯的二进制数据,这些二进制数据可能是由NSStringUIImage等对象转换而来,调用者可根据需要自己转换回原来的数据类型。

方式二:把获取到的数据封装成YYKVStorageItem

1.根据key到数据库中查找数据,根据数据生成YYKVStorageItem实例对象。
2.如果1中获取的对象存在则同时更新当前数据的last_access_time(最后访问时间)。
3.通过拿到的filename生成文件路径,读取该文件,获取文件内存储的二进制数据。
4.如果二进制数据不存在,就把该数据从数据库中删除,最后返回这个YYKVStorageItem实例对象。

YYDiskCache


知道YYKVStorage做什么之后,再来看YYDiskCache就简单了。

作者这样介绍YYDiskCache
YYDiskCache是一个线程安全的缓存,用于存储SQLite支持的键值对和文件系统(类似于NSURLCache的磁盘缓存)。

YYDiskCache具有以下功能:
1.它使用LRU(最近最少使用)来删除对象。
2.它可以通过成本,计数和年龄来控制。
3.它可以配置为在没有可用磁盘空间时自动驱逐对象。
4.它可以自动决定每个对象的存储类型(sqlite / file)。

总结一下就是:
YYDiskCache封装了YYKVStorage,在YYDiskCache中对于disk的缓存操作实际上都是通过YYKVStorage完成的,除此之外,YYDiskCache又自定义了淘汰规则,删除那些最近时间段内不常用的对象。

YYCache


YYCache封装了YYMemoryCacheYYDiskCache

YYCache初始化需要一个NSString类型的namepath,它会根据这两个值生成一个路径,根据这个路径初始化出YYDiskCache

所以,接下来的事情就好办了。

如果是存储操作,YYCache首先会通过YYMemoryCache放进内存缓存,然后通过YYDiskCache放进磁盘缓存。

如果是查询操作,YYCache首先会通过YYMemoryCache先到内存缓存中取,如果内存缓存中没有,再通过YYDiskCache到磁盘缓存中取。

如果是删除操作,YYCache首先会通过YYMemoryCache删除内存缓存的数据,然后通过YYDiskCache删除磁盘缓存的数据。

总结


YYCache自定义了内存缓存和磁盘缓存类,并实现了各自的淘汰算法,在时间和空间上对数据缓存操作都进行了优化。

推荐阅读更多精彩内容

  • 从 YYCache 源码 Get 到如何设计一个优秀的缓存 来源:Lision 前言 iOS 开发中总会用到各种缓...
    今天lgw阅读 3,969评论 1 18
  • YYCache是用于Objective-C中用于缓存的第三方框架。此文主要用来讲解该框架的实现细节,性能分析、设计...
    JonesCxy阅读 330评论 0 2
  • YYCache源码分析(二) 原文链接:http://www.jianshu.com/p/492c3c3a0485...
    kakukeme阅读 465评论 0 51
  • 安装 官方网站Charles 是一款收费软件,可以免费体验30天。网上有破解版。 使用 infoq 上有一篇很棒的...
    plusend阅读 886评论 0 0
  • 道盛哲学学习会)打卡第20天 姓名:严娜 部门:分水碶 组别:待定 【知~学习 诵读《活法》第二章 思维方式决定人...
    NinaYanWellin阅读 51评论 0 0
  • 作者 李昊澄 导师 刘艳 这是我第一次尝试把导图应用在物理。全图分为三个分支,第一分支讲声种类,分为次声...
    阿宝宝啊阅读 196评论 0 0