YYCache 设计思路

cache_all_the_things

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 本身设计的缺陷。

TMMemoryCacheTMCache 的内存缓存实现,最初由 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 性能基准测试:

memory_cache_bench_result

可以看到 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 淘汰算法、更快的数据统计,更多的容量控制选项。

disk_cache_bench_result

备注:

关于锁:

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 的可以注意一下。

iOS JSON 模型转换库评测

iOS 开发中总会用到各种 JSON 模型转换库,这篇文章将会对常见的几个开源库进行一下评测。评测的内容主要集中在性能、功能、容错性这几个方面。

评测的对象:

Manually
手动进行 JSON/Model 转换,不用任何开源库,可以进行高效、自由的转换,但手写代码非常繁琐,而且容易出错。

YYModel
我造的一个新轮子,比较轻量(算上 .h 只有 5 个文件),支持自动的 JSON/Model 转换,支持定义映射过程。API 简洁,功能也比较简单。

FastEasyMapping
Yalantis 开发的一个 JSON 模型转换库,可以自定义详细的 Model 映射过程,支持 CoreData。使用者较少。

JSONModel
一个 JSON 模型转换库,有着比较简洁的接口。Model 需要继承自 JSONModel。

Mantle
Github 官方团队开发的 JSON 模型转换库,Model 需要继承自 MTLModel。功能丰富,文档完善,使用广泛。

MJExtension
国内开发者"小码哥"开发的 JSON 模型库,号称性能超过 JSONModel 和 Mantle,使用简单无侵入。国内有大量使用者。

(更多…)

最近一段时间的工作整理

正式转为全职 iOS 工程师已经有一年多了,这一年里工作并不忙,使得我有更多的时间和精力来研究些有意思的东西。最近整理了下这一年攒下来的代码,拆分成了几个库,在接下来的一段时间内会陆续开源:

YYModel 类似 Mantle/JSONModel 的工具,性能比 Mantle 高一个数量级,有更好的容错性,更简洁的 API。
YYCache 类似 TMCache 那样的工具,有着更好的性能,支持 LRU,磁盘缓存支持 SQLite。
YYImage iOS图像库,支持高性能的 APNG/WebP/GIF 动图播放、编码和解码,支持帧动画等。
YYWebImage 类似 SDWebImage 的工具,基于 YYImage 和 YYCache,有更好的性能、更丰富的功能。
YYText UILabel 和 UITextView 的开源实现,支持异步排版渲染、图文混排、更多文字特效/点击效果、动画/表情输入、竖排版等。

YYKeyboardManager 从 YYText 分离出来的一个键盘监听工具,能实时监听和获取键盘视图、位置、动画。
YYDispatchQueuePool 从 YYText 分离出来的一个很简单的队列管理工具,用于管理全局并发任务。
YYAsyncLayer 从 YYText 分离出来的一个很简单的 CALayer 的子类,用于进行异步绘制和显示。
YYCategories Category 类型的工具库。

YYKit 上面所有工具的打包工具集,全部工具都兼容 iOS6~9。
YYKitDemo YYKit 的功能/性能演示,实现有 Twitter 和 Weibo 的 Feed 列表、发布视图,有着和官方 App 完全一致的 UI 和更流畅的交互体验。

每个库都会配有几篇博客来介绍相关技术、性能评测,这也算是我最近一年工作的总结吧。

深入理解RunLoop

RunLoop 是 iOS 和 OSX 开发中非常基础的一个概念,这篇文章将从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理。之后会介绍一下在 iOS 中,苹果是如何利用 RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的。

Index
RunLoop 的概念
RunLoop 与线程的关系
RunLoop 对外的接口
RunLoop 的 Mode
RunLoop 的内部逻辑
RunLoop 的底层实现
苹果用 RunLoop 实现的功能
AutoreleasePool
事件响应
手势识别
界面更新
定时器
PerformSelecter
关于GCD
关于网络请求
RunLoop 的实际应用举例
AFNetworking
AsyncDisplayKit

(更多…)

2015年了呢...

又是新的一年了呢。。偶尔也写写年终总结之类的东西,然后顺便稍微展望一下未来吧。

算起来,来到帝都工作已经快三年了呢;相对于刚毕业前后那段紧张绝望迷茫的日子来说,刚过去的这一年已经算得上不错了。这一年里换了一个新工作,从 Java 转回了 iOS,上班从酒仙桥挪到了中关村,工资涨了点儿,活儿轻松了点儿。和小伙伴租的房子,一年一搬。新的房子有个面朝阳台的书房,午后的阳光很棒;书房有一个很大的书架,能放下我在这边买的所有的书。看上去,这一年过得还不错。

但是到底是怎样的呢?我也不知道。能说上话的人越来越少,从前的朋友和同学也都各奔东西,不再联系。朋友们有的去留学了,有的留学又回来了,有的去创业了,有的仍留在出生的城市工作。大家都有自己的圈子,即使能有空聚一聚,或者在网上聊一聊,也都没什么话题了。我自己也不太擅长与人交流吧,所以总是习惯了一个人。用村上的话来说:"哪里会有人喜欢孤独,不过是不喜欢失望罢了"。

2015年来了,我要做些什么呢?如果可以的话,腾出更多个人时间来做些有意思的事儿。再仔细学一下 dsp 相关的东西,希望能做出类似 GarageBand 那样的 App 吧,我想要在 iPad 上编辑和录制 midi。在屋里腾出块空间来放下我的电钢,试试看能不能捡起一些过去的技能。换一块儿好点的数位板,认真学一下数码绘,希望能创造点东西,而不是总在复制。然后,一定要多读书!想得太多读书少这肯定是不行的。我知道现在如果立下一堆计划怕是用处不大(笑),但是不管怎么样,希望未来能更好一些吧。

最后还是想说一下我的第一份工作。08年我第一次注册人人网,加上了很多大学好友,玩得不亦乐乎。11年底,我拿到人人的 Offer,到人人网实习,做一些移动方面的东西。12年毕业后,便正式加入了人人网,只是转做起了 Java 后台,先后负责开发开放平台、应用中心、支付等一些项目。虽然当时由移动开发转做 Java 有点不情愿,但现在看来这让我增长了很多技能,眼界也开阔了很多。在这期间,部门的头头(给我面试机会、招我进去,算是有知遇之恩吧)走了,一同去的几个同学也先后走了,部门同事也前前后后走了很多,最后我也走了。。我在人人网玩了六年多,工作了两年多,到现在每天新鲜事儿都刷不出来几条了,多少有点感伤,稍微祝福一下人人吧。。

第 2 页,共 9 页12345...最旧 »