YYCache 设计思路
iOS 开发中总会用到各种缓存,最初我是用的一些开源的缓存库,但到总觉得缺少某些功能,或某些 API 设计的不够好用。YYCache (https://github.com/ibireme/YYCache) 是我新造的一个轮子,下面说一下这个轮子的设计思路。
内存缓存
通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。相对于磁盘缓存来说,内存缓存的设计要更简单些,下面是我调查的一些常见的内存缓存。
NSCache 是苹果提供的一个简单的内存缓存,它有着和 NSDictionary 类似的 API,不同点是它是线程安全的,并且不会 retain key。我在测试时发现了它的几个特点:NSCache 底层并没有用 NSDictionary 等已有的类,而是直接调用了 libcache.dylib,其中线程安全是由 pthread_mutex 完成的。另外,它的性能和 key 的相似度有关,如果有大量相似的 key (比如 "1", "2", "3", ...),NSCache 的存取性能会下降得非常厉害,大量的时间被消耗在 CFStringEqual() 上,不知这是不是 NSCache 本身设计的缺陷。
TMMemoryCache 是 TMCache 的内存缓存实现,最初由 Tumblr 开发,但现在已经不再维护了。TMMemoryCache 实现有很多 NSCache 并没有提供的功能,比如数量限制、总容量限制、存活时间限制、内存警告或应用退到后台时清空缓存等。TMMemoryCache 在设计时,主要目标是线程安全,它把所有读写操作都放到了同一个 concurrent queue 中,然后用 dispatch_barrier_async 来保证任务能顺序执行。它错误的用了大量异步 block 回调来实现存取功能,以至于产生了很大的性能和死锁问题。
PINMemoryCache 是 Tumblr 宣布不在维护 TMCache 后,由 Pinterest 维护和改进的一个内存缓存。它的功能和接口基本和 TMMemoryCache 一样,但修复了性能和死锁的问题。它同样也用 dispatch_semaphore 来保证线程安全,但去掉了dispatch_barrier_async,避免了线程切换带来的巨大开销,也避免了可能的死锁。
YYMemoryCache 是我开发的一个内存缓存,相对于 PINMemoryCache 来说,我去掉了异步访问的接口,尽量优化了同步访问的性能,用 OSSpinLock 来保证线程安全。另外,缓存内部用双向链表和 NSDictionary 实现了 LRU 淘汰算法,相对于上面几个算是一点进步吧。
下面的单线程的 Memory Cache 性能基准测试:
可以看到 YYMemoryCache 的性能不错,仅次于 NSDictionary + OSSpinLock;
NSCache 的写入性能稍差,读取性能不错;
PINMemoryCache 的读写性能也还可以,但读取速度差于 NSCache;
TMMemoryCache 性能太差以至于图上都看不出来了。
磁盘缓存
为了设计一个比较好的磁盘缓存,我调查了大量的开源库,包括 TMDiskCache、PINDiskCache、SDWebImage、FastImageCache 等,也调查了一些闭源的实现,包括 NSURLCache、Facebook 的 FBDiskCache 等。他们的实现技术大致分为三类:基于文件读写、基于 mmap 文件内存映射、基于数据库。
TMDiskCache, PINDiskCache, SDWebImage 等缓存,都是基于文件系统的,即一个 Value 对应一个文件,通过文件读写来缓存数据。他们的实现都比较简单,性能也都相近,缺点也是同样的:不方便扩展、没有元数据、难以实现较好的淘汰算法、数据统计缓慢。
FastImageCache 采用的是 mmap 将文件映射到内存。用过 MongoDB 的人应该很熟悉 mmap 的缺陷:热数据的文件不要超过物理内存大小,不然 mmap 会导致内存交换严重降低性能;另外内存中的数据是定时 flush 到文件的,如果数据还未同步时程序挂掉,就会导致数据错误。抛开这些缺陷来说,mmap 性能非常高。
NSURLCache、FBDiskCache 都是基于 SQLite 数据库的。基于数据库的缓存可以很好的支持元数据、扩展方便、数据统计速度快,也很容易实现 LRU 或其他淘汰算法,唯一不确定的就是数据库读写的性能,为此我评测了一下 SQLite 在真机上的表现。iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。这和 SQLite 官网的描述基本一致。另外,直接从官网下载最新的 SQLite 源码编译,会比 iOS 系统自带的 sqlite3.dylib 性能要高很多。基于 SQLite 的这种表现,磁盘缓存最好是把 SQLite 和文件存储结合起来:key-value 元数据保存在 SQLite 中,而 value 数据则根据大小不同选择 SQLite 或文件存储。NSURLCache 选定的数据大小的阈值是 16K;FBDiskCache 则把所有 value 数据都保存成了文件。
我的 YYDiskCache 也是采用的 SQLite 配合文件的存储方式,在 iPhone 6 64G 上的性能基准测试结果见下图。在存取小数据 (NSNumber) 时,YYDiskCache 的性能远远高出基于文件存储的库;而较大数据的存取性能则比较接近了。但得益于 SQLite 存储的元数据,YYDiskCache 实现了 LRU 淘汰算法、更快的数据统计,更多的容量控制选项。
备注:
关于锁:
OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
关于 Realm:
Realm 是一个比较新的数据库,针对移动应用所设计。它的 API 对于开发者来说非常友好,比 SQLite、CoreData 要易用很多,但相对的坑也有不少。我在测试 SQLite 性能时,也尝试对它做了些简单的评测。我从 Realm 官网下载了它提供的 benchmark 项目,更新 SQLite 到官网最新的版本,并启用了 SQLite 的 sqlite3_stmt 缓存。我的评测结果显示 Realm 在写入性能上差于 SQLite,读取小数据时也差 SQLite 不少,读取较大数据时 Realm 有很大的优势。当然这只是我个人的评测,可能并不能反映真实项目中具体的使用情况。我想看看它的实现原理,但发现 Realm 的核心 realm-core 是闭源的(评论里 Realm 员工提到目前有在 Apache 2.0 授权下的开源计划),能知道的是 Realm 应该用 了 mmap 把文件映射到内存,所以才在较大数据读取时获得很高的性能。另外我注意到添加了 Realm 的 App 会在启动时向某几个 IP 发送数据,评论中有 Realm 员工反馈这是发送匿名统计数据,并且只针对模拟器和 Debug 模式。这部分代码目前是开源的,并且可以通过环境变量 REALM_DISABLE_ANALYTICS 来关闭,如果有使用 Realm 的可以注意一下。
YYCache 设计思路的更多相关文章
- YYCache设计思路及源码学习
设计思路 利用YYCache来进行操作,实质操作分为了内存缓存操作(YYMemoryCache)和硬盘缓存操作(YYDiskCache).内存缓存设计一般是在内存中开辟一个空间用以保存请求的数据(一般 ...
- 磁盘缓存--YYCache 设计思路
为了设计一个比较好的磁盘缓存,我调查了大量的开源库,包括 TMDiskCache.PINDiskCache.SDWebImage.FastImageCache 等,也调查了一些闭源的实现,包括 NSU ...
- BBWebImage 设计思路
BBWebImage 设计思路 BBWebImage 是高性能 Swift 图片组件,用于图片下载.缓存.编解码.编辑与展示. GitHub 地址: https://github.com/Silenc ...
- TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析
TYPESDK 服务端设计思路与架构之一:应用场景分析 作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够同时接入几十个甚至几百个各 ...
- 分享一个CQRS/ES架构中基于写文件的EventStore的设计思路
最近打算用C#实现一个基于文件的EventStore. 什么是EventStore 关于什么是EventStore,如果还不清楚的朋友可以去了解下CQRS/Event Sourcing这种架构,我博客 ...
- ENode框架单台机器在处理Command时的设计思路
设计目标 尽量快的处理命令和事件,保证吞吐量: 处理完一个命令后不需要等待命令产生的事件持久化完成就能处理下一个命令,从而保证领域内的业务逻辑处理不依赖于持久化IO,实现真正的in-memory: 保 ...
- WebGIS中快速整合管理多源矢量服务以及服务权限控制的一种设计思路
文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 在真实项目中,往往GIS服务数据源被其他多个信息中心或者第三方 ...
- OpenStack 通用设计思路 - 每天5分钟玩转 OpenStack(25)
API 前端服务 每个 OpenStack 组件可能包含若干子服务,其中必定有一个 API 服务负责接收客户请求. 以 Nova 为例,nova-api 作为 Nova 组件对外的唯一窗口,向客户暴露 ...
- Redis入门指南(第2版) Redis设计思路学习与总结
https://www.qcloud.com/community/article/222 宋增宽,腾讯工程师,16年毕业加入腾讯,从事海量服务后台设计与研发工作,现在负责QQ群后台等项目,喜欢研究技术 ...
随机推荐
- Latex 横排图片
\begin{figure} \begin{minipage}[t]{0.5\linewidth} \centering \includegraphics[width=2.2in]{figure/an ...
- A JavaFX based Game Authoring System
http://www.mirkosertic.de/doku.php/javastuff/javafxgameauthoring ——————————————————————————————————— ...
- 实时监控MySql状态
大多网站的性能瓶颈都会出在数据库上,所以想把Mysql监控起来,就搜索了下相关资料. 后来和同事讨论了下cacti和nagios有些老套和过时,graphite比较时尚,然后就搜了下相关的资料,最后搞 ...
- HDU 2196Computer(树形DP)
给你一颗边带权值的树,求树上的每一点距离其最远的一个点的距离 比较典型的题了,主要方法是进行两次DFS,第一次DFS求出每一个点距离它的子树的最远距离和次远距离,然后第二次DFS从父节点传过来另一侧的 ...
- pureMVC学习之一
//1var MainWindow: TMainWindow;begin Application.Initialize; Application.MainFormOnTaskbar := Tru ...
- android ListView中的Item有Button时候点击异常处理
1.当ListView中有Button的时候往往会遇到很多问题,比较常见的一个问题是: 假设:在ListView中有N个Item当点击其中某个Item中的Button的时候,需要改变当前Button的 ...
- [hihoCoder]#1039 : 字符消除
Description 小Hi最近在玩一个字符消除游戏.给定一个只包含大写字母"ABC"的字符串s,消除过程是如下进行的: 1)如果s包含长度超过1的由相同字母组成的子串,那么这些 ...
- 文件字符读写函数fscanf()和 fgets() 比较
一. 文件格式化读入函数 fscanf() int fscanf(文件指针,格式化字符串,输入列表); 返回值: 整形,输入列表中定义字符串的个数. 1, 例如读取字符串: char str1[ ...
- js时间转换相关
1.json时间格式转换 function ChangeDateFormat(jsondate) { if (!jsondate||jsondate.length < 1) {return &q ...
- (剑指Offer)面试题19:二叉树的镜像
题目: 操作给定的二叉树,将其变换为源二叉树的镜像. 二叉树的定义如下: struct TreeNode{ int val; TreeNode* left; TreeNode* right; }; 输 ...