YYCache源码分析

缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。

这里通过从YYCache入手,再到内存缓存YYMemoryCache、磁盘缓存YYDiskCache,一层一层地剥开它的心。

YYCache

管理和协调YYMemoryCache和YYDiskCache
YYCache存储、查询、删除的每个功能都提供了两种API,同步缓存和异步缓存。因磁盘大数据缓存可能会耗时,涉及到磁盘缓存的使用了GCD异步缓存,并提供了回调,可以在回调中做些自定义操作。

存储数据
同时存储到内存缓存和磁盘

//同步缓存
- (void)setObject:(id)object forKey:(NSString *)key {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}

//磁盘部分使用异步缓存
- (void)setObject:(id)object forKey:(NSString *)key withBlock:(void (^)(void))block {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key withBlock:block];
}

查询缓存数据
查询缓存数据思路:优先读取内存缓存的数据;内存中读取不到再去磁盘中读取,如果能读取到,同时把数据存储到内存缓存中。这样设计主要是为了提高下次查询的速度。

//查询
- (id)objectForKey:(NSString *)key {
    //优先查找内存缓存
    id object = [_memoryCache objectForKey:key];
    if (!object) {
       //其次,查找磁盘缓存
        object = [_diskCache objectForKey:key];
        if (object) {
            //磁盘缓存能命中的情况下,缓存到内存
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}

//异步查询
- (void)objectForKey:(NSString *)key withBlock:(void (^)(NSString *key, id object))block {
   //处理边界条件
    if (!block) return;
   //优先查找内存缓存
    id object = [_memoryCache objectForKey:key];
    if (object) {
//
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            block(key, object);
        });
    } else {
        [_diskCache objectForKey:key withBlock:^(NSString *key, id object) {
            if (object && ![_memoryCache objectForKey:key]) {
                [_memoryCache setObject:object forKey:key];
            }
            block(key, object);
        }];
    }
}

删除数据
将内存缓存和磁盘缓存都删掉。

- (void)removeObjectForKey:(NSString *)key {
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key];
}

- (void)removeObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key))block {
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key withBlock:block];
}

YYMemoryCache

NSCache 是苹果提供的一个简单的内存缓存,它有着和 NSDictionary 类似的 API,不同点是它是线程安全的,并且不会 retain key。NSCache 底层并没有用 NSDictionary 等已有的类,而是直接调用了 libcache.dylib,其中线程安全是由 pthread_mutex 完成的。另外,它的性能和 key 的相似度有关,如果有大量相似的 key ,NSCache 的存取性能会下降得非常厉害,大量的时间被消耗在 CFStringEqual() 上。

NSCache
NSCache是一个可变的集合类型,用于临时存放键值对,当资源不足时会被移除。
NSCache的主要特点:

  • 线程安全
  • 内存超出阈值时,会自动清理
  • Key-Value数据结构,类似字典的使用
  • 可以限制缓存对象数量和总的缓存空间大小

NSCache的OC源码没开源,可以通过阅读GNU源码NSCache.h和NSCache.m
来了解它的实现。
它的实现是基于苹果封装的NS系列类的,如:NSMapTable,NSString,NSEnumerator,NSMutableArray等。
缓存淘汰策略通过LRU算法来实现,用一个可变数组保存所有的缓存对象,然后根据对象的平均访问次数 * 0.2 + 1 这个限制来淘汰所有低于这个访问次数的对象,一直释放直到有足够的所需空间。因此,它是通过释放访问次数小的对象来实现淘汰策略的。线程安全据YY大神说是使用了互斥锁pthread_mutex来保障的,但在GNU源码并没有看到。
PS:个人觉得这里可以通过LRU-K算法解决可能导致的缓存污染问题,关于LRU-K算法可以看下 你与解决“缓存污染”只差这篇文章的距离

Swift下的NSCache是通过一个双向链表来实现的,链表里是按缓存对象大小cost进行排序的,优先驱逐占用缓存cost较小的对象。线程安全是通过NSLock来保障。
NSCache.swift源码

YYMemoryCache
YYMemoryCache优化了同步访问的性能,用互斥锁pthread_mutex来保证线程安全。另外,缓存内部用双向链表和 NSDictionary 实现了 LRU 淘汰算法。

的特点主要是使用了LRU-K算法处理内存缓存,同时尽可能避免了缓存污染问题。
YYMemoryCache中使用CF系列的类,相比NSCache中直接使用NS系列的类性能上会好一些。同时也使用性能较好的互斥锁pthread_mutex。

//节点
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}

//Map
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

通过源码对象可以大概看出,其实是用双向链表+CFDictionary实现的LRU缓存算法。

查询内存缓存

- (id)objectForKey:(id)key {
    if (!key) return nil;
   //加锁保证线程安全
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {
        //存在的数据,通过LRU算法将它移到头结点
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
  //解锁
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

修改内存缓存数据
根据

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
   //处理特殊情况
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
       //对已有节点操作,并通过LRU算法放到头结点
    } else {
       //新增节点,并通过LRU算法放到头结点
    }
   //异步处理总的缓存大小大于缓存限制的情况
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
   //处理缓存对象个数大于限制的情况
    if (_lru->_totalCount > _countLimit) {
        //删除一次链表尾部节点,因为只setObject一次
        _YYLinkedMapNode *node = [_lru removeTailNode];
        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);
}

缓存清理策略

- (instancetype)init {
    self = super.init;
    pthread_mutex_init(&_lock, NULL);
    _lru = [_YYLinkedMap new];
    _queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
    //初始化配置
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _autoTrimInterval = 5.0;
   //默认情况下
    //收到内存警告就清除所有缓存
    _shouldRemoveAllObjectsOnMemoryWarning = YES;
    //收到app进入后台通知就清除所有缓存
    _shouldRemoveAllObjectsWhenEnteringBackground = YES;
    //监听内存警告、进入后台的通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
    //初始化时自动递归清除缓存
    [self _trimRecursively];
    return self;
}

递归清除缓存

- (void)_trimRecursively {
    __weak typeof(self) _self = self;
   //延迟_autoTrimInterval=5s后并行清理缓存,不影响主线程
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        [self _trimInBackground];
        [self _trimRecursively];
    });
}

同时也提供了根据缓存数量限制、缓存大小、时间节点来手动清除缓存。

//缓存数量限制
- (void)trimToCount:(NSUInteger)count;
//缓存大小
- (void)trimToCost:(NSUInteger)cost;
//缓存时间节点
- (void)trimToAge:(NSTimeInterval)age;

YYMemoryCache和NSDictionary、NSCache
通过YYCache作者在单线程的 Memory Cache 性能基准测试,读写性能上,YYMemoryCache 仅次于 NSDictionary,优于NSCache,即NSDictionary>YYMemoryCache>NSCache。

YYDiskCache

YYDiskCache和YYMemoryCache的实现思路上有些类似,都是使用LRU算法来处理缓存,在初始化时延迟清理下缓存,都具有增删改查的特点,同时也支持缓存数量限制countLimit、缓存大小限制costLimit、时间限制ageLimit等。
由于对磁盘操作会是耗时的,YYDiskCache的每个功能api设计上都加了个异步处理的。

- (nullable id)objectForKey:(NSString *)key;

- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id _Nullable object))block;

和YYMemoryCache不同,这里使用的是dispatch_semaphore信号量来保证线程安全。其中原因是信号量在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。它的优势在于等待时不会消耗 CPU 资源。它比较合适磁盘缓存。

YY作者评测过SQLite读写的性能

iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。这和 SQLite 官网的描述基本一致。另外,直接从官网下载最新的 SQLite 源码编译,会比 iOS 系统自带的 sqlite3.dylib 性能要高很多。基于 SQLite 的这种表现,磁盘缓存最好是把 SQLite 和文件存储结合起来:key-value 元数据保存在 SQLite 中,而 value 数据则根据大小不同选择 SQLite 或文件存储。

因此,YYDiskCache 采用了SQLite +文件的存储方式。在存取小数据 (NSNumber) 时,YYDiskCache 的性能远远高出基于文件存储的库;而较大数据的存取性能则比较接近了。

另外,YYDiskCache还支持释放到指定的剩余空间

- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
    if (targetFreeDiskSpace == 0) return;
    int64_t totalBytes = [_kv getItemsSize];
    if (totalBytes <= 0) return;
    int64_t diskFreeBytes = _YYDiskSpaceFree();
    if (diskFreeBytes < 0) return;
    int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
    if (needTrimBytes <= 0) return;
    int64_t costLimit = totalBytes - needTrimBytes;
    if (costLimit < 0) costLimit = 0;
    [self _trimToCost:(int)costLimit];
}

YYKVStorage
YYKVStorage是YYDiskCache的核心类,它主要封装了 SQLite 数据库的操作和文件存储操作。YYKVStorageItem是保存了磁盘缓存的基本信息。

@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key;                ///< key
@property (nonatomic, strong) NSData *value;                ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size;                             ///< value's size in bytes
@property (nonatomic) int modTime;                          ///< modification unix timestamp
@property (nonatomic) int accessTime;                       ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end

YYKVStorage内主要是对YYKVStorageItem的操作来实现进行缓存管理。提供了三种情况的缓存处理针对文件,SQLite和混合的方式。文件操作使用系统的NSFileManager。

typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    
    /// The `value` is stored as a file in file system.
    YYKVStorageTypeFile = 0,
    
    /// The `value` is stored in sqlite with blob type.
    YYKVStorageTypeSQLite = 1,
    
    /// The `value` is stored in file system or sqlite based on your choice.
    YYKVStorageTypeMixed = 2,
};

YYDiskCache中对20KB以上的使用YYKVStorageTypeMixed。大小为NSUIntegerMax的使用YYKVStorageTypeSQLite。默认情况下YYKVStorageTypeFile。

总结:YYCache在性能上使用C语言,少用NS系列类,同时锁的使用也是使用性能最高的pthread_和信号量,保障了缓存实现的性能。同时使用LRU算法提高了缓存的命中率,大大提高了查找效率。但这里我个人觉得可以使用LRU-K算法来优化,避免偶发性的操作导致的缓存污染问题,比如一个A页面跳转到B页面,又跳回来等情况。

如有不对的地方可以指出留言哈,若对您有帮助,可以考虑点个赞哈

参考资料:
YYCache 设计思路
你与解决“缓存污染”只差这篇文章的距离

你可能感兴趣的:(YYCache源码分析)