注:本文最好是配着代码一起阅读,如果我把代码加到文章里面,篇幅太大。
YYCache的一些基本方法:
//根据名称或者路径获取YYCache对象
- (instancetype)cacheWithName:(NSString *)name;
- (instancetype)cacheWithPath:(NSString *)path;
//判断缓存是否存在
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key, BOOL contains))block;
//读取缓存 - (id<NSCoding>)objectForKey:(NSString *)key;
(void)objectForKey:(NSString *)key withBlock:(void (^)(NSString *key, id<NSCoding> object))block;
//存入对象 - (void)setObject:(id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key withBlock:(void (^)(void))block;
//移除缓存 - (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key))block;
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
endBlock:(void(^)(BOOL error))end;
根据名称或者路径获取yycache对象:
如果是传入名称,就获取Documents目录路径和传入的名称进行拼接来生成缓存路径,如果是传入的路径,则开始进行缓存初始化的操作。(错误检测:传入的path是否为空)
1、YYDiskCache的初始化(传入路径),2、根据路径获取名称,3、初始化YYMemoryCache。4、赋值
YYDiskCache的初始化:初始化有路径和一个20kb的数值类型(方法需要传入缓存路径和缓存阈值threshold参数。在作者设计思路文章中分析到,超过20k数据使用文件缓存读写快,而低于20k数据使用数据库读写比较快),初始化方法里面:1、YYDiskCache的初始化,2、YYKVStorageType的初始化,3、YYKVStorage的初始化,4、方法- (void)_trimRecursively;(在初始化的时候调用_trimRecursively方法每隔60s时间检测一下缓存数据大小是否超过容量。),5、创建程序终止的监听。(错误检测:以上每一个初始化如果依靠返回的值的话,如果为空则直接return nil。)
上述1中,YYDiskCache于_YYDiskCacheGetGlobal,_YYDiskCacheGetGlobal首先单例初始化一个static NSMapTable *_globalInstances;和static dispatch_semaphore_t _globalInstancesLock; 然后在_globalInstances里取值cache,取值过程利用信号量(_globalInstancesLock)来保证线程安全。然后返回cache赋值给YYDiskCache对象。
上述2:根据传入的数值的大小来初始化存储类型,三种类型(YYKVStorageTypeFile = 0,
YYKVStorageTypeSQLite = 1,YYKVStorageTypeMixed = 2,)
上述3:YYKVStorage的初始化会传入路径和存储类型,进入初始化函数(错误判断:会判断传入的路径和类型。),在初始化函数里会根据传入名称,数据名称和废弃的垃圾名称来创建NSFileManager文件对象。然后会判断数据库是否打开和是否初始化过(算是一种容错的操作)。然后进入方法_fileEmptyTrashInBackground(如果上次失败,清空垃圾,并且会将清除操作放在异步队列里进行)
上述5:程序终止的监听,接收到的时候会把YYKVStorage对象赋值为nil。
存入对象:双缓存
先存内存,然再存磁盘。
存内存:模拟NSCache的方法,传入对象,key值然后封装成传入对象,key值,大小cost。- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost; 方法内部(错误判断:key为空则返回nil,对象为空则根据key移除存储对象),存储过程中利用pthread_mutex_lock互斥所保证线程安全,存储过程:主要是模拟lru,利用CFDictionaryGetValue方法从_YYLinkedMap *_lru;(对象定义看注解①)里面的字典对象根据key值遍历查找,结果返回并赋值给_YYLinkedMapNode *node(对象定义看注解②),如果找到则重新赋值,未找到则创建node并重新赋值,然后放置lru的链表头部。然后进行缓存大小是否超过限制的判断,如果超过则异步进行清理操作(- (void)_trimToCost:(NSUInteger)costLimit;(详情可看注解③)),然后再进行判断。(内存的清理可以设置成在主线程里清除或者异步清除)。
存磁盘:老规矩,先来一手错误的判断,判断key值和对象的值是否为空,然后根据YYKVStorage里的kv对象来进行操作([_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];),这个过程加锁,依旧是判断传入的值是否错误,然后根据存储类型来进行sqlite还是文件磁盘的存储。
读取缓存:双缓存
老规矩,先从YYMemoryCache内存里读取,内存读取方法里面(错误判断传入的key是否为空)用pthread_mutex_lock互斥锁来保证线程安全,然后用CFDictionaryGetValue方法来遍历lru里面的链表,如果内存找到先把找到的对象的节点放于表头然后返回,如果未找到则进入磁盘查找。
移除缓存
依旧是错误的判断以及加锁,如果链表尾部或者是根据CFDictionaryGetValue找到lru相应的节点并删除,还有就是获取的对象在主线程释放还是异步释放的判断。
一些解释:
NSMapTable: NSMapTable和NSDictionary相对应,相对于NSDictionary/NSMutableDictionary,NSMapTable有如下的特征: NSDictionary/NSMutableDictionary会copy对应的key,强引用相应的value。 NSMapTable是可变的,没有一个不变的类与其对应。 NSMapTable可以对其key和value弱引用,在这种情况下当key或者value被释放的时候,此entry会自动从NSMapTable中移除。 NSMapTable在加入一个(key,value)的时候,可以对其value设置为copy。 NSMapTable可以包含任意指针,使用指针去做相等或者hashing检查。
dispatch_semaphore_t:信号量,用于线程同步。
@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;
}
@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
注解③:- (void)_trimToCost:(NSUInteger)costLimit;
流程:同样的互斥锁加锁,并用一个bool值finsh来判断是否完成清理然后解锁。清理的过程主要是移除lru链表尾部的对象(首先是移除尾部,然后把尾部的对象放在一个可变数组里,最后去判断在主线程里清理还是异步清理,并在串行队列里面去释放这个可变数组里面的对象)
关于
_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
});
}
[node class]; 这种在queue上调用对象的方法
这种写法的解释:应该是node在执行完这个方法后就出了作用域了,reference会减1,但是此时node不会被dealloc,因为block 中retain了node,使得node的reference count为1,当执完block后,node的reference count又-1,此时node就会在block对应的queue上release了。
YYDiskCache里用dispatch_semaphore_wait二不是用OSSpinLockLock的原因:DiskCache 锁占用时间可能会比较长,如果用 SpinLock 会在锁存在竞争时占用大量 CPU 资源。
特定线程释放资源的解释:避免了过多线程导致的性能问题。