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

作者: 潇潇潇潇潇潇潇 | 来源:发表于2016-09-29 10:33 被阅读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的使用。阅读和仿写这个类库的实现也让我受益匪浅,我也会在今后继续用这种方式阅读和仿写其它的著名类库,希望大家多多支持。
    如果觉得我的这篇文章对你有帮助,请在下方点个赞支持一下,谢谢!

    相关文章

      网友评论

        本文标题:PINCache-源码分析与仿写(四)

        本文链接:https://www.haomeiwen.com/subject/jtbkyttx.html