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;
}
而在SDWebImage
的SDImageCache
中,对于UIImage
的内存计算是UIImage
的width
和height
以及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
中的totalCostLimit
和countLimit
差不多,_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个查询操作获取的均是单纯的二进制数据,这些二进制数据可能是由NSString
、UIImage
等对象转换而来,调用者可根据需要自己转换回原来的数据类型。
方式二:把获取到的数据封装成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
封装了YYMemoryCache
和YYDiskCache
。
YYCache
初始化需要一个NSString
类型的name
或path
,它会根据这两个值生成一个路径,根据这个路径初始化出YYDiskCache
。
所以,接下来的事情就好办了。
如果是存储操作,YYCache
首先会通过YYMemoryCache
放进内存缓存,然后通过YYDiskCache
放进磁盘缓存。
如果是查询操作,YYCache
首先会通过YYMemoryCache
先到内存缓存中取,如果内存缓存中没有,再通过YYDiskCache
到磁盘缓存中取。
如果是删除操作,YYCache
首先会通过YYMemoryCache
删除内存缓存的数据,然后通过YYDiskCache
删除磁盘缓存的数据。
总结
YYCache
自定义了内存缓存和磁盘缓存类,并实现了各自的淘汰算法,在时间和空间上对数据缓存操作都进行了优化。