缓存的意义

缓存在很多硬件、软件系统中都有广泛的使用。CPU 靠缓存来提高处理速度,服务器软件靠缓存来提高处理能力。同样在 iOS 中,我们也可以对 “热” 数据进行缓存,比如图片,API,用户配置等等。在开发中,经常会使用一些现成的缓存框架来帮我们做缓存,比如专门缓存图片的 SDWebImage,也可以使用数据库来做缓存,比如 RealmSQLite。缓存可以有效减少资源的重复获取,减轻服务器压力,提升用户体验。

缓存位置

根据需要,我们可以将数据缓存到内存、磁盘中。存取速度内存大于磁盘,但是磁盘的容量一般大于内存,并且磁盘上的缓存断电后不会消失。对于一些访问频次比较高、占用空间比较小的数据,可以放到内存中,访问频次比较低、占用空间比较大的数据,可以放到磁盘中,这样可以最大程度发挥缓存的优势。一般缓存框架都会结合缓存数据的特点,将内存和磁盘结合起来。

YYCache

YYCache 是使用 Objective-C 开发的一款 iOS 平台的缓存框架,提供内存和磁盘两种缓存方式,实现了 LRU(least-recently-used)淘汰算法,所有方法都是线程安全的。使用起来也很简单:

YYCache *cache = [YYCache cacheWithName:@"cache"];
[cache.memoryCache setObject:@"Slark" forKey:@"name"];

YYCache 的性能也不错,这里有两张作者给出的性能测试图:

iPhone 6 上,内存缓存每秒响应次数 (越高越好):

iPhone 6 上,磁盘缓存每秒响应次数 (越高越好):

初始化

YYCache 提供了非常简洁的初始化方法:

//根据 name 初始化 cache
- (nullable instancetype)initWithName:(NSString *)name;

//根据 path 初始化 cache
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;

//类方法:根据 name 初始化 cache
+ (nullable instancetype)cacheWithName:(NSString *)name;

//类方法:根据 path 初始化 cache
+ (nullable instancetype)cacheWithPath:(NSString *)path;

//禁止使用 init 和 new 方法初始化
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;

在这里有两个宏定义比较有趣,NS_DESIGNATED_INITIALIZERUNAVAILABLE_ATTRIBUTENS_DESIGNATED_INITIALIZER 为能初始化全部变量的方法,在实现中我们可以看到:

- (instancetype)initWithName:(NSString *)name {
    if (name.length == 0) return nil;
    NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *path = [cacheFolder stringByAppendingPathComponent:name];
    return [self initWithPath:path];
}

- (instancetype)initWithPath:(NSString *)path {
    if (path.length == 0) return nil;
    YYDiskCache *diskCache = [[YYDiskCache alloc] initWithPath:path];
    if (!diskCache) return nil;
    NSString *name = [path lastPathComponent];
    YYMemoryCache *memoryCache = [YYMemoryCache new];
    memoryCache.name = name;
    
    self = [super init];
    _name = name;
    _diskCache = diskCache;
    _memoryCache = memoryCache;
    return self;
}

+ (instancetype)cacheWithName:(NSString *)name {
	return [[self alloc] initWithName:name];
}

+ (instancetype)cacheWithPath:(NSString *)path {
    return [[self alloc] initWithPath:path];
}

这几种初始化方法最后都使用 initWithPath 这个方法来初始化。所以可以将这个方法定义为:"NS_DESIGNATED_INITIALIZER",让调用者可以清楚的知道该使用哪个方法初始化。同时如果不想使用某个方法,可以使用 UNAVAILABLE_ATTRIBUTE 来标记,这样在调用时编译器就会出现 method is unavailable 提示。

存取方法

YYCache 提供的存取方法也很简洁:

//判断是否包含 object
- (BOOL)containsObjectForKey:(NSString *)key;

//根据 key 获取 object
- (nullable id<NSCoding>)objectForKey:(NSString *)key;

//根据 key 设置 object
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;

//根据 key 移除 object
- (void)removeObjectForKey:(NSString *)key;

//移除所有 object
- (void)removeAllObjects;

setObject 方法会将 object 同时存到 memory 和 disk 中:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}

Memory 缓存

memory cache 的存储结构为双向链表,链表节点 YYLinkedMapNode 的定义如下:

@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;
}
@end

双向链表的定义如下:

@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;
}

为了操作方便,定义了一些操作双向链表的方法:

// 插入一个 Node 到链表最前面
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

// 把一个 Node 移到链表最前面
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

// 删除一个 Node
- (void)removeNode:(_YYLinkedMapNode *)node;

// 删除最后一个 Node
- (_YYLinkedMapNode *)removeTailNode;

// 在后台线程删除所有 Node
- (void)removeAll;

在 Head 插入一个 Node 的实现:

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    // 增加存储大小
    _totalCost += node->_cost;
    // 总数 +1
    _totalCount++;
    if (_head) {
        // 如果存在 _head,将 node 的 _next 设为 _head,将 _head 的 _prev 设为 node。最后将 _head 设为 node。
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    } else {
      // 如果不存在 _head,将 _head 和 _tail 都指向 node。
        _head = _tail = node;
    }
}

下面是存储到 memory cache 的实现:

- (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();
    //如果已存在 node,则更新 node;如果不存在,则新建 Node。并将 Node 移动到链表最前面。
    if (node) {
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];
    } else {
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        [_lru insertNodeAtHead:node];
    }
    //判断 cost 是否超过限制,超过则执行淘汰算法。
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    //判断 count 是否超过限制,超过则执行淘汰算法。
    if (_lru->_totalCount > _countLimit) {
        _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);
}

在访问双向链表的时候使用 pthread_mutex_lock 加上了锁,可以保证线程安全。

Disk 缓存

Disk 缓存使用 SQLite 和 File 来实现,通过 YYKVStorage 实现了对 SQLite 和 File 相关操作的封装。可以在初始化的时候提供 threshold(阈值),来判断是否使用数据库来存储数据(默认为 20KB):

- (instancetype)initWithPath:(NSString *)path {
    return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}

YYDiskCache 的保存 object 的 API 如下:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    
    //根据 Object 获取扩展 Data
    NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
    NSData *value = nil;
    if (_customArchiveBlock) {
        //实现自定义归档方法
        value = _customArchiveBlock(object);
    } else {
        @try {
            value = [NSKeyedArchiver archivedDataWithRootObject:object];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (!value) return;
    NSString *filename = nil;
    if (_kv.type != YYKVStorageTypeSQLite) {
        if (value.length > _inlineThreshold) {
            //获取文件名
            filename = [self _filenameForKey:key];
        }
    }
    
    //加锁保存 object
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}

添加缓存的实现如下:

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    
    if (filename.length) {
        //如果存在 filename,则使用文件缓存
        if (![self _fileWriteWithName:filename data:value]) {
            //文件保存失败返回
            return NO;
        }
        //把 key,filename,extendedData 保存到数据库
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            //数据库保存失败,删除文件,并返回
            [self _fileDeleteWithName:filename];
            return NO;
        }
        //文件和数据库都保存成功返回 YES。
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            //根据 key 查找 filename
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

其他的查找,更新,删除操作和 memory 的实现类似,就不在此赘述了。

总结

YYCache 通过 Memory 和 Disk 实现对数据的缓存,并结合 LUR 算法对数据进行淘汰,可以让使用者结合自己的需求自定义阈值,同时还提供了统计缓存大小,收到内存警告时自动清理缓存等功能,在易用性上做的非常棒。在代码实现上,逻辑也非常清晰,提供了非常完善的注释文档,非常值得我们学习。