iOS本地缓存方案之YYCache源码解析

iOS持久化方案有哪些?

简单列举一下,iOS的本地缓存方案有挺多,各有各的适用场景:html

  • NSUserDefault :

    系统提供的最简便的key-value本地存储方案,适合比较轻量的数据存储,好比一些业务flag。主要缘由仍是其底层是用plist文件存储的,在数据量逐步变大后,可能会发生性能问题。面试

  • 存文件,归档:

    不管是本身转换业务数据为二进制再writeFile,仍是直接利用系统的NSKeyedArchiver接口归档成文件,都属于文件存储的方案。优点是开发简单,业务能够自行控制单文件的存储内容以免可能发生的性能问题。算法

  • sqlite、FMDB:

    底层利用到数据的存储方案,比较适用数据量大,有查询,排序等需求的存储场景,缺点就是开发略复杂一些。sql

  • CoreData、其余ORM方案:

    CoreData感受好像应用并非很普遍?数据库

  • Key-Value接口的缓存方案:

    这里特指提供Key-Value形式接口的缓存库,底层缓存可能使用文件或者sqlite都有。本文讨论的YYCache底层是混合使用文件+sqlite的存储方式。基于接口简便,性能优于NSUserDefault的特性,应该适用于大多数的业务场景,可是没法适用上面数据库相似的使用场景。缓存

聊聊YYCache的优秀设计

这里其实yy大神本人在博文《YYCache 设计思路》中对其设计思路有比较详尽的介绍,建议你们能够先去读一读,本文就其相对于其余缓存库的一些优点点聊一聊。安全

高性能的线程安全方案

首先高性能是YYCache比较核心的一个设计目标,挺多代码逻辑都是围绕性能这个点来作的。微信

做为对比,yy提出了TMMemoryCache方案的性能缺陷。TMMemoryCache的线程安全采用的是比较常见的经过dispatch_barrier来保障并行读,串行写的方案。该方案我在上一篇《AFNetworking源码解析与面试考点思考》中有介绍。那么TMMemoryCache存在性能问题的缘由会是由于其dispatch_barrier的线程安全方案吗?多线程

答案应该在其同步接口的设计上:并发

- (id)objectForKey:(NSString *)key
{
    if (!key)
        return nil;

    __block id objectForKey = nil;

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    [self objectForKey:key block:^(TMMemoryCache *cache, NSString *key, id object) {
        objectForKey = object;
        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    #if !OS_OBJECT_USE_OBJC
    dispatch_release(semaphore);
    #endif

    return objectForKey;
}

TMCache在同步接口里面经过信号量来阻塞当前线程,而后切换到其余线程(具体代码在其异步接口里面,是经过dispatch_async到一个并行队列来实现的)去执行读取操做。按照yy的说法主要的性能损耗应该在这个线程切换操做,同步接口不必去切换线程执行。

yy这边的思路是经过自旋锁来保证线程安全,但仍然在当前线程去执行读操做,这样就能够节省线程切换带来的开销。(不过我在YYCache的最新代码里看到的是普通的互斥锁,并无用自旋锁,应该是后面又作了方案上的修改?)

除了加锁串行,dispatch_sync实现同步的方案是否可行呢?

除了yy提供的加锁串行方案,咱们来看看前面介绍过的barrier并行读串行写方案是否也存在性能问题。若是使用该方案,同步接口多是这样的:

- (id)objectForKey:(NSString *)key
{
        __block id object = nil;
        dispatch_sync(concurrent_queue, ^{
                object = cache[key];                                            // 读接口,不用barrier,保证读与读可以并行
        });
        return object;
}

- (void)setObject:(id)object forKey:(NSString *)key
{
        dispatch_barrier_sync(concurrent_queue, ^{    // 写接口,barrier保证与读互斥
                cache[key] = object;
        });
}

通过demo验证,能够发现虽然是dipatch到一个concurrent_queue中执行,可是因为是sync同步派发,实际上并不会切换到新的线程执行。也就是说该方案也能作到节省线程切换的开销。

划重点: dispatch_sync不会切换调用线程执行,这个结论好像也是个面试考点?

那么该方案与加锁串行的方案相比,性能如何呢?

barrier实现并行读串行写 vs 互斥锁串行处理的性能比较

单线程测试

首先跑了下YYCache自带的benchmark,其原理是测试单线程作20000次读或者写的总耗时。其中TMCache new表示修改成dispatch_sync后的测试数据。

===========================
Memory cache set 200000 key-value pairs
NSDictionary:      67.53
NSDict+Lock:       73.47
YYMemoryCache:    133.08
PINMemoryCache:   257.59
NSCache:          457.63
TMCache:         7638.25
TMCache new:      297.58

===========================
Memory cache get 200000 key-value pairs
NSDictionary:      43.32
NSDict+Lock:       53.68
YYMemoryCache:     93.15
PINMemoryCache:   141.12
NSCache:           73.89
TMCache:         7446.88
TMCache new:      210.80

从结论看,单线程用dispatch_sync的方案,比YYCache的锁串行方案要慢2倍多一点,比原始的信号量强行同步操做要快25到35倍。

因此开发过程当中须要避免相似TMCache原始写法的同步接口实现方案。

多线程测试

display_barrier是并行读,串行写的方案,理论上在多线程并发的场景会更有优点,因此我尝试写了个多线程的benchmark来对比性能,代码以下:

typedef void(^exec_block)(id key, id value);
+ (void)benchmark:(NSString *)type exec:(exec_block)block keys:(NSArray *)keys values:(NSArray *)values
{
    int count = 10000;
    printf("Memory cache %s %i pairs\n", type.UTF8String, count);
    __block NSTimeInterval begin, end, time;

    begin = CACurrentMediaTime();
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create(type.UTF8String, DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < count; i++) {
        dispatch_group_async(group, queue, ^{
            block(keys[i], values[i]);  // 执行不一样cache的具体set或者get操做
        });
    }

    dispatch_group_notify(group, queue, ^{
        end = CACurrentMediaTime();
        time = end - begin;
        printf("%s:   %8.2f\n", type.UTF8String, time * 1000);
    });
}

由于是并发执行,因此结束时间是经过dispatch_group来拿的。函数接收外部传入的exec_block做为输入,block内部执行具体的YYCacheTMCache的set/get方法。

这个测试方案存在一个问题,整个耗时大头在dispatch_group_async的派发上,block内部是否执行cache的get/set方法,对总体耗时结果影响不大。因此最终我也没有获得一个比较准确的测试结果,或许固定建立几个线程来作并发测试会更靠谱一些。

高性能的本地存储方案

除了多线程的高性能实现,YYCache在本地持久化如何提升性能也有个小策略。核心问题应该就是二进制数据从文件读写和从sqlite读写究竟哪一个更快?sqlite官网有一个测试结论

image-20200608145630124.png

表格中数值表示存文件耗时除以存数据库耗时,大于1表示存数据库更快,表示为绿色。

基于这个结论和本身的实测结果,YYCache采起的方案是大于20k的采起直接存储文件,而后在sqlite里面存元信息(好比说文件路径),小于20k的直接存储到sqlite里面。

数据完整性保障:

对于有关联的数据,存储时必定须要保障其完整性,要么全成功,要么全失败。好比YYCache在存储文件时,存在数据库的元信息和实际文件的存储就必须保障原子性。若是云信息存储成功,可是文件存储失败,就会致使逻辑问题。具体YYCache代码以下:

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;

这里能够看到,只有文件存成功了才会存数据库元信息,若是数据库元信息存失败了,会去删除已经存储成功的文件。

咱们业务开发存储关联数据的时候,也须要注意这个逻辑。

缓存淘汰策略

除了性能以外,YYCache也新增了一些实用功能。

好比LRU算法,基于存储时长、数量、大小的缓存控制策略等。

LRU算法采用经典的双链表+哈希表的方案实现的,很适合不熟悉的同窗参考学习,这里就不展开了。


原文连接: http://www.luoyibu.cn/posts/1...

欢迎扫码关注个人微信公众号
image

相关文章
相关标签/搜索