上一篇讲解了YYCache的使用方法,架构与内存缓存的设计。这一篇讲解磁盘缓存的设计与缓存组件的设计思路。
YYDiskCache负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作。作为YYCache的第二级缓存,它与第一级缓存YYMemoryCache的相同点是:
它与YYMemoryCache不同点是:
这里需要说明的是: 对于上面的第一条:我看源码的时候只看出来有这两种缓存形式,但是从内部的缓存type枚举来看,其实是分为三种的:
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0,
YYKVStorageTypeSQLite = 1,
YYKVStorageTypeMixed = 2,
};
也就是说我只找到了第二,第三种缓存形式,而第一种纯粹的文件存储(YYKVStorageTypeFile)形式的实现我没有找到:当type为 YYKVStorageTypeFile和YYKVStorageTypeMixed的时候的缓存实现都是一致的:都是讲data存在文件里,将元数据放在数据库里面。
在YYDiskCache的初始化方法里,没有发现正确的将缓存类型设置为YYKVStorageTypeFile的方法:
//YYDiskCache.m
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
return [self initWithPath:@"" inlineThreshold:0];
}
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
...
}
从上面的代码可以看出来,当给指定初始化方法initWithPath:inlineThreshold:
的第二个参数传入0的时候,缓存类型才是YYKVStorageTypeFile。而且比较常用的初始化方法initWithPath:
的实现里,是将20kb传入了指定初始化方法里,结果就是将type设置成了YYKVStorageTypeMixed。
而且我也想不出如果只有文件形式的缓存的话,其元数据如何保存。如果有读者知道的话,麻烦告知一下,非常感谢了~~
在本文暂时对于上面提到的”文件+数据库的形式”在下文统一说成文件缓存了。
在接口的设计上,YYDiskCache与YYMemoryCache是高度一致的,只不过因为有些时候大文件的访问可能会比较耗时,所以框架作者在保留了与YYMemoryCache一样的接口的基础上,还在原来的基础上添加了block回调,避免阻塞线程。来看一下YYDiskCache的接口(省略了注释):
//YYDiskCache.h
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block;
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> _Nullable object))block;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable 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:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
- (NSInteger)totalCount;
- (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block;
- (NSInteger)totalCost;
- (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block;
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block;
- (void)trimToAge:(NSTimeInterval)age;
- (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block;
从上面的接口代码可以看出,YYDiskCache与YYMemoryCache在接口设计上是非常相似的。但是,YYDiskCache有一个非常重要的属性,它作为用sqlite做缓存还是用文件做缓存的分水岭:
//YYDiskCache.h
@property (readonly) NSUInteger inlineThreshold;
这个属性的默认值是20480byte,也就是20kb。即是说,如果缓存数据的长度大于这个值,就使用文件存储;如果小于这个值,就是用sqlite存储。来看一下这个属性是如何使用的:
首先我们会在YYDiskCache的指定初始化方法里看到这个属性:
//YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
_inlineThreshold = threshold;
...
}
在这里将_inlineThreshold赋值,也是唯一一次的赋值。然后在写入缓存的操作里判断写入缓存的大小是否大于这个临界值,如果是,则使用文件缓存:
//YYDiskCache.m
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
...
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
//如果长度大临界值,则生成文件名称,使得filename不为nil
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}
Lock();
//在该方法内部判断filename是否为nil,如果是,则使用sqlite进行缓存;如果不是,则使用文件缓存
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
现在我们知道了YYDiskCache相对于YYMemoryCache最大的不同之处是缓存类型的不同。 细心的朋友会发现上面这个写入缓存的方法(saveItemWithKey:value:filename:extendedData:)实际上是属于_kv的。这个_kv就是上面提到的YYKVStorage的实例,它在YYDiskCache的初始化方法里被赋值:
//YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;
_kv = kv;
...
}
同样地,再举其他两个接口为例,内部也是调用了_kv的方法:
- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}
- (void)removeObjectForKey:(NSString *)key {
if (!key) return;
Lock();
[_kv removeItemForKey:key];
Unlock();
}
所以是时候来看一下YYKVStorage的接口和实现了:
YYKVStorage实例负责保存和管理所有磁盘缓存。和YYMemoryCache里面的_YYLinkedMap将缓存封装成节点类_YYLinkedMapNode类似,YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等):
//YYKVStorageItem.h
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; //键
@property (nonatomic, strong) NSData *value; //值
@property (nullable, nonatomic, strong) NSString *filename; //文件名
@property (nonatomic) int size; //值的大小,单位是byte
@property (nonatomic) int modTime; //修改时间戳
@property (nonatomic) int accessTime; //最后访问的时间戳
@property (nullable, nonatomic, strong) NSData *extendedData; //extended data
@end
既然在这里将缓存封装成了YYKVStorageItem实例,那么作为缓存的管理者,YYKVStorage就必然有操作YYKVStorageItem的接口了:
//YYKVStorage.h
//写入某个item
- (BOOL)saveItem:(YYKVStorageItem *)item;
//写入某个键值对,值为NSData对象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
//写入某个键值对,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;
#pragma mark - Remove Items
//移除某个键的item
- (BOOL)removeItemForKey:(NSString *)key;
//移除多个键的item
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;
//移除大于参数size的item
- (BOOL)removeItemsLargerThanSize:(int)size;
//移除时间早于参数时间的item
- (BOOL)removeItemsEarlierThanTime:(int)time;
//移除item,使得缓存总容量小于参数size
- (BOOL)removeItemsToFitSize:(int)maxSize;
//移除item,使得缓存数量小于参数size
- (BOOL)removeItemsToFitCount:(int)maxCount;
//移除所有的item
- (BOOL)removeAllItems;
//移除所有的item,附带进度与结束block
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
#pragma mark - Get Items
//读取参数key对应的item
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
//读取参数key对应的data
- (nullable NSData *)getItemValueForKey:(NSString *)key;
//读取参数数组对应的item数组
- (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys;
//读取参数数组对应的item字典
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;
大家最关心的应该是写入缓存的接口是如何实现的,下面重点讲一下写入缓存的接口:
//写入某个item
- (BOOL)saveItem:(YYKVStorageItem *)item;
//写入某个键值对,值为NSData对象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
//写入某个键值对,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;
这三个接口都比较类似,上面的两个方法都会调用最下面参数最多的方法。在详细讲解写入缓存的代码之前,我先讲一下写入缓存的大致逻辑,有助于让大家理解整个YYDiskCache写入缓存的流程:
- (BOOL)saveItem:(YYKVStorageItem *)item {
return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData];
}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value {
return [self saveItemWithKey:key value:value filename:nil extendedData:nil];
}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
if (key.length == 0 || value.length == 0) return NO;
if (_type == YYKVStorageTypeFile && filename.length == 0) {
return NO;
}
if (filename.length) {
//如果文件名不为空字符串,说明要进行文件缓存
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
//写入元数据
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
//如果缓存信息保存失败,则删除对应的文件
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
//如果文件名为空字符串,说明不要进行文件缓存
if (_type != YYKVStorageTypeSQLite) {
//如果缓存类型不是数据库缓存,则查找出相应的文件名并删除
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
// 缓存类型是数据库缓存,把元数据和value写入数据库
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
从上面的代码可以看出,在底层写入缓存的方法是_dbSaveWithKey:value:fileName:extendedData:
,这个方法使用了两次:
不过虽然调用了两次,我们可以从传入的参数是有差别的:第二次filename传了nil。那么我们来看一下_dbSaveWithKey:value:fileName:extendedData:
内部是如何区分有无filename的情况的:
下面结合代码看一下:
//数据库存储
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
//sql语句
NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
int timestamp = (int)time(NULL);
//key
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
//filename
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
//size
sqlite3_bind_int(stmt, 3, (int)value.length);
//inline_data
if (fileName.length == 0) {
//如果文件名长度==0,则将value存入数据库
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
//如果文件名长度不为0,则不将value存入数据库
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
//modification_time
sqlite3_bind_int(stmt, 5, timestamp);
//last_access_time
sqlite3_bind_int(stmt, 6, timestamp);
//extended_data
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
框架作者用数据库的一条记录来保存关于某个缓存的所有信息。 而且数据库的第四个字段是保存缓存对应的data的,从上面的代码可以看出当filename为空和不为空的时候的处理的差别。
上面的sqlite3_stmt
可以看作是一个已经把sql语句解析了的、用sqlite自己标记记录的内部数据结构。
而sqlite3_bind_text和sqlite3_bind_int是绑定函数,可以看作是将变量插入到字段的操作。
OK,现在看完了写入缓存,我们再来看一下获取缓存的操作:
//YYKVSorage.m
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
//更新内存访问的时间
[self _dbUpdateAccessTimeWithKey:key];
if (item.filename) {
//如果有文件名,则尝试获取文件数据
item.value = [self _fileReadWithName:item.filename];
//如果此时获取文件数据失败,则删除对应的item
if (!item.value) {
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}
从上面这段代码我们可以看到获取YYKVStorageItem的实例的方法是_dbGetItemWithKey:excludeInlineData:
我们来看一下它的实现:
来看一下代码:
- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return nil;
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
YYKVStorageItem *item = nil;
int result = sqlite3_step(stmt);
if (result == SQLITE_ROW) {
//传入stmt来生成YYKVStorageItem实例
item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData];
} else {
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
}
}
return item;
}
我们可以看到最终生成YYKVStorageItem实例的是通过_dbGetItemFromStmt:excludeInlineData:
来实现的:
- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData {
//提取数据
int i = 0;
char *key = (char *)sqlite3_column_text(stmt, i++);
char *filename = (char *)sqlite3_column_text(stmt, i++);
int size = sqlite3_column_int(stmt, i++);
//判断excludeInlineData
const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i);
int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++);
int modification_time = sqlite3_column_int(stmt, i++);
int last_access_time = sqlite3_column_int(stmt, i++);
const void *extended_data = sqlite3_column_blob(stmt, i);
int extended_data_bytes = sqlite3_column_bytes(stmt, i++);
//将数据赋给item的属性
YYKVStorageItem *item = [YYKVStorageItem new];
if (key) item.key = [NSString stringWithUTF8String:key];
if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename];
item.size = size;
if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes];
item.modTime = modification_time;
item.accessTime = last_access_time;
if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes];
return item;
}
上面这段代码分为两个部分:
需要注意的是:
stringWithUTF8String:
来转成NSString类型。excludeInlineData
:我相信对于某个设计来说,它的产生一定是基于某种个特定问题下的某个场景的
由上文可以看出:
在YYMemoryCache中,是使用互斥锁来保证线程安全的。 首先在YYMemoryCache的初始化方法中得到了互斥锁,并在它的所有接口里都加入了互斥锁来保证线程安全,包括setter,getter方法和缓存操作的实现。举几个例子:
- (NSUInteger)totalCost {
pthread_mutex_lock(&_lock);
NSUInteger totalCost = _lru->_totalCost;
pthread_mutex_unlock(&_lock);
return totalCost;
}
- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread {
pthread_mutex_lock(&_lock);
_lru->_releaseOnMainThread = releaseOnMainThread;
pthread_mutex_unlock(&_lock);
}
- (BOOL)containsObjectForKey:(id)key {
if (!key) return NO;
pthread_mutex_lock(&_lock);
BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
pthread_mutex_unlock(&_lock);
return contains;
}
- (id)objectForKey:(id)key {
if (!key) return nil;
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
//如果节点存在,则更新它的时间信息(最后一次访问的时间)
node->_time = CACurrentMediaTime();
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}
而且需要在dealloc方法中销毁这个锁头:
- (void)dealloc {
...
//销毁互斥锁
pthread_mutex_destroy(&_lock);
}
框架作者采用了信号量的方式来给 首先在初始化的时候实例化了一个信号量:
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
_lock = dispatch_semaphore_create(1);
_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
...
然后使用了宏来代替加锁解锁的代码:
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
简单说一下信号量:
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是
当信号量为0时,就会做等待处理,这是其他线程如果访问的话就会让其等待。所以如果信号量在最开始的的时候被设置为1,那么就可以实现“锁”的功能:
需要注意的是:如果有多个线程等待,那么后来信号量恢复以后访问的顺序就是线程遇到dispatch_semaphore_wait的顺序。
这也就是信号量和互斥锁的一个区别:互斥量用于线程的互斥,信号线用于线程的同步。
那么问题来了:为什么内存缓存使用的是互斥锁(pthread_mutex),而磁盘缓存使用的就是信号量(dispatch_semaphore)呢?
答案在框架作者的文章YYCache 设计思路里可以找到:
为什么内存缓存使用互斥锁(pthread_mutex)?
框架作者在最初使用的是自旋锁(OSSpinLock)作为内存缓存的线程锁,但是后来得知其不够安全,所以退而求其次,使用了pthread_mutex。
为什么磁盘缓存使用的是信号量(dispatch_semaphore)?
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
因为YYDiskCache在写入比较大的缓存时,可能会有比较长的等待时间,而dispatch_semaphore在这个时候是不消耗CPU资源的,所以比较适合。
可以参考上一部分YYMemoryCache 和YYDiskCache使用的不同的锁以及原因。
在YYMemoryCache中,作者选择了双向链表来保存这些缓存节点。那么可以思考一下,为什么要用双向链表而不是单向链表或是数组呢?
无论缓存的自动清理和释放,作者默认把这些任务放到子线程去做:
看一下释放所有内存缓存的操作:
- (void)removeAll {
//将开销,缓存数量置为0
_totalCost = 0;
_totalCount = 0;
//将链表的头尾节点置空
_head = nil;
_tail = nil;
if (CFDictionaryGetCount(_dic) > 0) {
CFMutableDictionaryRef holder = _dic;
_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//是否在子线程操作
if (_releaseAsynchronously) {
dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
CFRelease(holder); // hold and release in specified queue
});
} else if (_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
CFRelease(holder); // hold and release in specified queue
});
} else {
CFRelease(holder);
}
}
}
这里的YYMemoryCacheGetReleaseQueue()
使用了内联函数,返回了低优先级的并发队列。
//内联函数,返回优先级最低的全局并发队列
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
同样是字典实现,但是作者使用了更底层且快速的CFDictionary而没有用NSDictionary来实现。
YYCache有4个供外部调用的初始化接口,无论是对象方法还是类方法都需要传入一个字符串(名称或路径)。
而两个原生的初始化方法被框架作者禁掉了:
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
如果用户使用了上面两个初始化方法就会在编译期报错。
而剩下的四个可以使用的初始化方法中,有一个是指定初始化方法,被作者用NS_DESIGNATED_INITIALIZER
标记了。
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
指定初始化方法就是所有可使用的初始化方法都必须调用的方法。更详细的介绍可以参考我的下面两篇文章:
为了异步将某个对象释放掉,可以通过在GCD的block里面给它发个消息来实现。这个技巧在该框架中很常见,举一个删除一个内存缓存的例子:
首先将这个缓存的node类取出,然后异步将其释放掉。
- (void)removeObjectForKey:(id)key {
if (!key) return;
pthread_mutex_lock(&_lock);
_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();
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
});
}
}
pthread_mutex_unlock(&_lock);
}
为了释放掉这个node对象,在一个异步执行的(主队列或自定义队列里)block里给其发送了class
这个消息。不需要纠结这个消息具体是什么,他的目的是为了避免编译错误,因为我们无法在block里面硬生生地将某个对象写进去。
其实关于上面这一点我自己也有点拿不准,希望理解得比较透彻的同学能在下面留个言~ ^^
YYCache默认在收到内存警告和进入后台时,自动清除所有内存缓存。所以在YYMemoryCache的初始化方法里,我们可以看到这两个监听的动作:
//YYMemoryCache.m
- (instancetype)init{
...
//监听app生命周期
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
...
}
然后实现监听到消息后的处理方法:
//内存警告时,删除所有内存缓存
- (void)_appDidReceiveMemoryWarningNotification {
if (self.didReceiveMemoryWarningBlock) {
self.didReceiveMemoryWarningBlock(self);
}
if (self.shouldRemoveAllObjectsOnMemoryWarning) {
[self removeAllObjects];
}
}
//进入后台时,删除所有内存缓存
- (void)_appDidEnterBackgroundNotification {
if (self.didEnterBackgroundBlock) {
self.didEnterBackgroundBlock(self);
}
if (self.shouldRemoveAllObjectsWhenEnteringBackground) {
[self removeAllObjects];
}
}
#if __has_include(<YYCache/YYCache.h>)
#import <YYCache/YYMemoryCache.h>
#import <YYCache/YYDiskCache.h>
#import <YYCache/YYKVStorage.h>
#elif __has_include(<YYWebImage/YYCache.h>)
#import <YYWebImage/YYMemoryCache.h>
#import <YYWebImage/YYDiskCache.h>
#import <YYWebImage/YYKVStorage.h>
#else
#import "YYMemoryCache.h"
#import "YYDiskCache.h"
#import "YYKVStorage.h"
#endif
在这里作者使用__has_include来检查Frameworks是否引入某个类。 因为YYWebImage已经集成YYCache,所以如果导入过YYWebImage的话就无需重再导入YYCache了。
通过看该组件的源码,我收获的不仅有缓存设计的思路,还有:
相信读过这篇文章的你也会有一些收获~ 如果能趁热打铁,下载一个YYCache源码看就更好啦~