傲视苍穹iOS《Objective-C》VIP专题第三方库收集iOS高级实用技术

PINCache-源码分析与仿写(四)

2016-09-29  本文已影响490人  潇潇潇潇潇潇潇

前言

阅读优秀的开源项目是提高编程能力的有效手段,我们能够从中开拓思维、拓宽视野,学习到很多不同的设计思想以及最佳实践。阅读他人代码很重要,但动手仿写、练习却也是很有必要的,它能进一步加深我们对项目的理解,将这些东西内化为自己的知识和能力。然而真正做起来却很不容易,开源项目阅读起来还是比较困难,需要一些技术基础和耐心。
本系列将对一些著名的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的使用。阅读和仿写这个类库的实现也让我受益匪浅,我也会在今后继续用这种方式阅读和仿写其它的著名类库,希望大家多多支持。
如果觉得我的这篇文章对你有帮助,请在下方点个赞支持一下,谢谢!

上一篇 下一篇

猜你喜欢

热点阅读