如何设计一个高效的缓存框架

iOS中缓存无处不在,从底层的方法缓存到应用层的网络缓存等,缓存的存在,大大提高了应用的运行速度,提升了用户体验。这里讨论一下如何设计一个好的缓存框架。

缓存的数据结构

如果我们仅将缓存中的数据维护在一个链表中, 那么当我们需要从缓存中查找数据时, 就意味着我们需要遍历链表。 这样的设计, 其时间复杂度为O(n),是一种比较低效率的做法。而哈希表访问数据的时间复杂度为O(1), 另外缓存是以key-value形式保存,用哈希表再好不过了。

缓存淘汰策略

所谓缓存淘汰策略, 就是当缓存满了之后, 又有新数据需要加入内存时, 我们怎么从缓存中为新数据腾出空间的策略。这个权衡的过程就是所谓的缓存淘汰策略。

常见的淘汰算法:FIFO、LFU、LRU.

FIFO算法

FIFO(First in First out),先进先出。在FIFO Cache设计中,核心原则就是:如果一个数据最先进入缓存中,则应该最早淘汰掉。也就是说,当缓存满的时候,应当把最先进入缓存的数据给淘汰掉。

LFU算法

LFU(Least Frequently Used)最近最少使用算法。它是基于“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路。

注意LFU和LRU算法的不同之处,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。

LRU算法

缓存策略一般采用LRU算法。LRU, Least Recently Used的简写, 即近期最少使用算法。 该算法依据与程序的局部性原理, 其淘汰旧数据的策略是, 距离当前最久没有被访问过的数据应该被淘汰。

举个简单的例子:

假设缓存大小为3,数据访问序列为set(2,2),set(1,1),get(2),get(1),get(2),set(3,3),set(4,4),
则在set(4,4)时对于LFU算法应该淘汰(3,3),而LRU应该淘汰(1,1),FIFO应该淘汰(2,2)。

实现思路

用哈希表存储key-value键值对, 为了解决冲突,采用链地址法。为了实现LRU算法,还使用了双向链表将哈希表中的元素”串起来”,最新插入和最近访问的元素被插入或者移动到链表头部,这样当元素总量或者元素的总cost达到阈值时,从链表尾部开始删除元素(淘汰)。原理图如下:

二级缓存设计

缓存通常采用二级缓存设计。内存缓存是临时的、可以高速访问的,如果内存缓存没命中需要的数据,则需要在磁盘缓存中查找,若还未命中,则进行网络请求。

下面谈谈iOS自带的有缓存功能的类。

NSURLCache

NSURLCache专门用于对iOS中的网络请求进行缓存,使用起来十分简单。NSURLCache工作原理如下:

NSURLCache内部一个NSURLRequest对应一个NSCachedURLResponse,缓存的数据都保存到数据库中,对于大文件如视频、音频文件,以文件的形式另外保存。

NSURLCache的常见用法

(1)获得全局缓存对象

NSURLCache *cache = [NSURLCache sharedURLCache]; 

(2)设置内存缓存的最大容量(字节为单位,默认为512KB)

- (void)setMemoryCapacity:(NSUInteger)memoryCapacity;

(3)设置硬盘缓存的最大容量(字节为单位,默认为10M)

- (void)setDiskCapacity:(NSUInteger)diskCapacity;

(4)硬盘缓存的位置:

沙盒/Library/Caches

(5)取得某个请求的缓存

- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request; 

(6)清除某个请求的缓存

- (void)removeCachedResponseForRequest:(NSURLRequest *)request;

(7)清除所有的缓存

- (void)removeAllCachedResponses;

缓存策略

iOS对NSURLRequest提供了7种缓存策略:

NSURLRequestUseProtocolCachePolicy // 默认的缓存策略(取决于协议)

NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,重新请求

NSURLRequestReloadIgnoringLocalAndRemoteCacheData // 未实现

NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,重新请求

NSURLRequestReturnCacheDataElseLoad// 有缓存就用缓存,没有缓存就重新请求

NSURLRequestReturnCacheDataDontLoad// 有缓存就用缓存,没有缓存就不发请求,当做请求出错处理(用于离线模式)

NSURLRequestReloadRevalidatingCacheData // 未实现

对于更新频率较高的资源不建议使用缓存,同时,缓存要定时清理以免占用太多磁盘空间。

使用示例

下面用NSURLCache来缓存MP4文件:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 1. 设置 NSURLCache 2MB内存、100MB磁盘空间
    NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024  diskCapacity:100 * 1024 * 1024  diskPath:nil]; [NSURLCache setSharedURLCache:sharedCache];

    // 2.创建请求
    NSURL *url = [NSURL URLWithString:@"https://video.ubtrobot.com/jimu/post/180616131624932685.mp4"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    _request = request;
    // 3.设置缓存策略(有缓存就用缓存,没有缓存就重新请求)
    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;

    // 4.发送请求
    NSURLSession *session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.defaultSessionConfiguration];
    _session = session;
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data) {
            NSLog(@"data.length === %lu", data.length);

        }
    }];
    _dataTask = dataTask;
    [dataTask resume];
    return YES;
}

首次联网之后启动APP,视频文件会被缓存下来。使用一些代码获取缓存数据。

NSCachedURLResponse *response =  [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
if (response) {
    NSLog(@"---这个请求已经存在缓存---");
    if (response.data) {
        NSLog(@"%@----%li", response.response.URL, response.data.length);
        // 这里可以进行视频播放
    }
} else {
    NSLog(@"---这个请求没有缓存---");
}

注意: 启动的时候设置缓存为更大的容量十分重要,使用默认的你会发现无法缓存视频文件。

缓存下来的文件如下图所示:

更多NSURLCache内容参考:这里

NSCache

NSCache是专门用来处理内存缓存的类,使用方法跟NSMutableDictionary类似,但却又有很大的不同:

1. NSMutableDictionary会对key进行copy,value retain, NSCache不会对key进行copy,所以NSMutableDictionary的key要遵守NSCopy协议。
2. NSCache线程安全, NSMutableDictionary非线程安全。
3. NSCache有对象淘汰策略,通过在存值的时候指定cost,当总cost或者key-value个数达到limit时,会进行对象释放(非LRU算法)。这种淘汰策略在没收到内存警告是也会执行,从而降低内存峰值(minimizing its memory footprint)。
4. Retrieving something from an NSCache object returns an autoreleased result.

NSCache官方实现戳这里

YYCache

可以参考这篇解析YYCache实现原理的文章

如果你想自己实现一个简单的Cache可以参考C语言版本实现
代码