PINCache-源码分析与仿写(四)
前言
阅读优秀的开源项目是提高编程能力的有效手段,我们能够从中开拓思维、拓宽视野,学习到很多不同的设计思想以及最佳实践。阅读他人代码很重要,但动手仿写、练习却也是很有必要的,它能进一步加深我们对项目的理解,将这些东西内化为自己的知识和能力。然而真正做起来却很不容易,开源项目阅读起来还是比较困难,需要一些技术基础和耐心。
本系列将对一些著名的iOS开源类库进行深入阅读及分析,并仿写这些类库的基本实现,加深我们对底层实现的理解和认识,提升我们iOS开发的编程技能。
PINCache
PINCache是线程安全的键值对缓存框架,用于缓存一些临时数据或需要频繁加载的数据,比如某些下载的数据或一些临时处理结果。它是在Tumblr 宣布不在维护 TMCache 后,由 Pinterest 维护和改进的一个缓存框架。它基于GCD支持多线程存取缓存数据。PINCache由两个部分构成,一个是内存缓存(PINMemoryCache),另一个是硬盘缓存(PINDiskCache)。如果使用内存缓存,当APP接收到内存警告或进入后台,PINCache将清理所有的内存缓存。
PINCache的地址:https://github.com/pinterest/PINCache
实现原理
PINCache使用键/值设计存储缓存数据。在内存缓存PINMemoryCache中,使用字典来存储缓存数据,一般使用多个字典配合管理数据各项信息,其中一个存储缓存内容,一个存储缓存创建日期,一个存储缓存缓存大小以及其他的缓存信息。对于磁盘缓存PINDiskCache,缓存数据存储到文件系统中,使用字典保存数据的其他信息,如文件修改日期、文件大小等。
PINCache使用异步方式存取缓存数据。在PINCache中,常见的操作如get、set、remove,都会把操作任务放到自定义的并行队列中。操作任务异步执行,执行结果通过block回调到上层。为了避免资源争夺问题,PINCache给数据操作加锁,保证多线程安全。
仿写PINCache
理解了PINCache的实现原理,我们动手模仿写一个缓存框架demo,以加深对PINCache的理解,掌握它的设计思想和实现过程。
在这个demo中,我们简化了大部分工作,只实现基本的功能,包括缓存的存储、读取和删除功能。如需要了解更详细的内容请看PINCache源码。
首先,创建一个项目,设置如下:
仿照PINCache创建缓存实现类,ZCJCache对外提供缓存存取的接口,ZCJMemoryCache是内存缓存实现类,ZCJDiskCache是磁盘缓存实现类。
类结构ZCJCache
定义ZCJCache对外的接口,包括set、get、remove缓存数据,单例方法以及只允许带名字的初始化方法:
@interface ZCJCache : NSObject
+(instancetype)sharedInstance;
- (instancetype)initWithName:(NSString *)name;
//标记init方法不可用
-(instancetype)init UNAVAILABLE_ATTRIBUTE;
+(instancetype)new UNAVAILABLE_ATTRIBUTE;
//根据key异步取缓存数据
- (void)objectForKey:(NSString *)key block:(ZCJCacheObjectBlock)block;
//异步存储缓存数据
-(void)setObject:(id)object forKey:(NSString *)key block:(ZCJCacheObjectBlock)block;
//删除缓存数据
-(void)removeObjectForKey:(NSString *)key;
@end
ZCJCache类和属性的初始化,其中currentQueue是自定义的并行队列
+(instancetype)sharedInstance {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] initWithName:@"ZCJDiskCacheShared"];
});
return instance;
}
-(instancetype)initWithName:(NSString *)name {
if (!name) {
return nil;
}
self = [super init];
if (self) {
_diskCache = [[ZCJDiskCache alloc] initWithName:name];
_memoryCache = [[ZCJMemoryCache alloc] init];
NSString *queueName = [[NSString alloc] initWithFormat:@"%@.%p", ZCJCachePrefix, (void *)self];
_currentQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@ Asynchronous Queue", queueName] UTF8String], DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
缓存存储方法,入参包括key,object以及回调block。object对象要符合NSCoding协议,才能完成数据的归档和解档。这里简单处理不做过多要求。
-(void)setObject:(id)object forKey:(NSString *)key block:(ZCJCacheObjectBlock)block {
if (!key || !object) {
return;
}
//向group追加任务队列,如果所有的任务都执行或者超时,它发出通知
dispatch_group_t group = nil;
ZCJMemoryCacheObjectBlock memBlock = nil;
ZCJDiskCacheObjectBlock diskBlock = nil;
if (block) {
group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_group_enter(group);
memBlock = ^(ZCJMemoryCache *memoryCache, NSString *memoryCacheKey, id memoryCacheObject) {
dispatch_group_leave(group);
};
diskBlock = ^(ZCJDiskCache *diskCache, NSString *key, id object) {
dispatch_group_leave(group);
};
}
[_memoryCache setObject:object forKey:key block:memBlock];
[_diskCache setObject:object forKey:key block:diskBlock];
if (group) {
__weak ZCJCache *weakSelf = self;
dispatch_group_notify(group, _currentQueue, ^{
ZCJCache *strongSelf = weakSelf;
if (strongSelf)
block(strongSelf, key, object);
});
}
}
读取缓存数据,先在内存缓存中查找,找不到再去磁盘缓存中搜索。
- (void)objectForKey:(NSString *)key block:(ZCJCacheObjectBlock)block {
if (!key) {
return;
}
__weak ZCJCache *weakSelf = self;
dispatch_sync(_currentQueue, ^{
ZCJCache *strongSelf = weakSelf;
[strongSelf.memoryCache objectForKey:key block:^(ZCJMemoryCache *memoryCache, NSString *key, id object) {
if (object) {
dispatch_sync(_currentQueue, ^{
ZCJCache *strongSelf = weakSelf;
block(strongSelf, key, object);
});
}
else {
[strongSelf.diskCache objectForKey:key block:^(ZCJDiskCache *diskCache, NSString *key, id object) {
if (object) {
dispatch_sync(_currentQueue, ^{
ZCJCache *strongSelf = weakSelf;
block(strongSelf, key, object);
});
}
}];
}
}];
});
}
删除指定键值的缓存,这里简单处理,没用异步的方式
-(void)removeObjectForKey:(NSString *)key {
if (!key) {
return;
}
[_memoryCache removeObjectForKey:key];
[_diskCache removeObjectForKey:key];
}
ZCJMemoryCache
ZCJMemoryCache的接口跟ZCJCache类似,代码就不贴了。具体看一下它的缓存set、get、remove方法。主要内容是将缓存内容放入字典中,key是唯一的关键字,value是缓存内容。
为了在多线程访问时,保证结果的安全,避免资源争夺问题,在关键设值取值处加锁。
-(void)setObject:(id)object forKey:(NSString *)key block:(ZCJMemoryCacheObjectBlock)block {
if (!key || !object) {
return;
}
__weak ZCJMemoryCache *weakSelf = self;
dispatch_sync(_currentQueue, ^{
pthread_mutex_lock(&_mutex);
[weakSelf.cacheDic setObject:object forKey:key];
pthread_mutex_unlock(&_mutex);
if (block) {
block(weakSelf, key, object);
}
});
}
-(id)objectForKey:(NSString *)key {
id object = nil;
pthread_mutex_lock(&_mutex);
object = _cacheDic[key];
pthread_mutex_unlock(&_mutex);
return object;
}
- (void)objectForKey:(NSString *)key block:(ZCJMemoryCacheObjectBlock)block{
__weak ZCJMemoryCache *weakSelf = self;
dispatch_async(_currentQueue, ^{
id object = [weakSelf objectForKey:key];
if (block) {
block(weakSelf, key, object);
}
});
}
-(void)removeObjectForKey:(NSString *)key {
if (!key) {
return;
}
pthread_mutex_lock(&_mutex);
[_cacheDic removeObjectForKey:key];
pthread_mutex_unlock(&_mutex);
}
ZCJMemoryCache在程序进入后台或收到内存警告时,清空内存缓存。以下是代码实现
//注册通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveEnterBackgroundNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
//清空内存缓存
- (void)didReceiveEnterBackgroundNotification:(NSNotification *)notification {
[self removeAllObjects];
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
[self removeAllObjects];
}
- (void)removeAllObjects {
pthread_mutex_lock(&_mutex);
[_cacheDic removeAllObjects];
pthread_mutex_unlock(&_mutex);
}
ZCJDiskCache
ZCJDiskCache与ZCJMemoryCache的过程类似,不同的地方在于ZCJMemoryCache将缓存数据存储在字典中,而ZCJDiskCache将缓存数据存储到文件系统中。ZCJDiskCache在存取缓存时需要将字符串形式的key转换成磁盘缓存路径。
看代码:
- (void)setObject:(id)object forKey:(NSString *)key block:(ZCJDiskCacheObjectBlock)block {
if (!key || !object) {
return;
}
__weak ZCJDiskCache *weakSelf= self;
dispatch_sync(_currentQueue, ^{
NSURL *fileUrl = nil;
dispatch_semaphore_wait(_lockSemaphore, DISPATCH_TIME_FOREVER);
fileUrl = [self encodedFileURLForKey:key];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
NSError *writeErr = nil;
BOOL written = [data writeToURL:fileUrl options:NSDataWritingAtomic error:&writeErr];
if (!written) {
fileUrl = nil;
}
dispatch_semaphore_signal(_lockSemaphore);
if (block) {
block(weakSelf, key, object);
}
});
}
demo的基本功能就这些,它的使用方式与PINCache基本一致。在ZCJCacheTest中,有相关的单元测试。如下图:
ZCJCache类的单元测试demo的完整代码已上传到Github,地址:https://github.com/superzcj/ZCJCache
总结
PINCache 异步执行缓存存取,它的实现过程给我们很多启发,在我们日常开发与设计中有很多可以学习的地方,比如字典存储、GCD的使用。阅读和仿写这个类库的实现也让我受益匪浅,我也会在今后继续用这种方式阅读和仿写其它的著名类库,希望大家多多支持。
如果觉得我的这篇文章对你有帮助,请在下方点个赞支持一下,谢谢!