问题

  一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存从网络拉取的数据,下次读取可以直接从 Sqlite DB 读取。我们先忽略从网络请求数据这一环节,假设数据已经保存在 DB 里,那我们要做的事就是,ViewController 从 DB 取数据,再传给 view 渲染:

这是最简单的情况,随着程序变复杂,多个 ViewController 都要向 DB 取数据,ViewController本身也会因为数据变化重新去 DB 取数据,会有两个问题:

    • 数据每次有变动,ViewController 都要重新去DB读取,做 IO 操作。

    • 多个 ViewController 之间可能会共用数据,例如同一份数据,本来在 Controller1 已经从 DB 取出来了,在 Controller2 要使用得重新去 DB 读取,浪费 IO。

对这里做优化,自然会想到在 DB 和 VC 层之间再加一层 cache,把从 DB 读取出来的数据 cache 在内存里,下次来取同样的数据就不需要再去磁盘读取 DB 了。

几乎所有的数据库框架都做了这个事情,包括微信读书开源的 GYDataCenter,CoreData,Realm 等。但这样做会导致一个问题,就是数据的线程安全问题。

按上面的设计,Cache层会有一个集合,持有从DB读取的数据。

除了 VC 层,其他层也会从cache取数据,例如网络层。上层拿到的数据都是对 cache 层这里数据的引用:

可能还会在网络层子线程,或其他一些用于预加载的子线程使用到,如果某个时候一条子线程对这个 Book1 对象的属性进行修改,同时主线程在读这个对象的属性,就会 crash,因为一般我们为了性能会把对象属性设为nonatomic,是非线程安全的,多线程读写时会有问题:

//Network
WRBook *book = [WRCache bookWithId:@“”];
book.fav = YES; //子线程在写
[book save]; //VC1
WRBook *book = [WRCache bookWithId:@“”];
self.view.title = book.title; //主线程在读

可以通过这个测试看到 crash 场景:

@interface TestMultiThread : NSObject
@property (nonatomic) NSArray *arr;
@end @implementation TestMultiThread
@end TestMultiThread *obj = [[TestMultiThread alloc] init];
for (int i = ; i < ; i ++) {
dispatch_async(dispatch_get_global_queue(, ), ^{
id a = obj.arr;
});
dispatch_async(dispatch_get_global_queue(, ), ^{
obj.arr = [NSArray arrayWithObject:@"b"];
});
}

解决方案

对这种情况,一般有三种解决方案:

1. 加锁

既然这个对象的属性是非线程安全的,那加锁让它变成线程安全就行了。可以给每个对象自定义一个锁,也可以直接用 OC 里支持的属性指示符 atomic:

@property (atomic) NSArray *arr;

这样就不用担心多线程同时读写的问题了。但在APP里大规模使用锁很可能会导致出现各种不可预测的问题,锁竞争,优先级反转,死锁等,会让整个APP复杂性增大,问题难以排查,并不是一个好的解决方案。

2. 分线程cache

另一种方案是一条线程创建一个 cache,每条线程只对这条线程对应的 cache 进行读写,这样就没有线程安全问题了。CoreData 和 Realm 都是这种做法,但这个方案有两个缺点:

  • a.使用者需要知道当前代码在哪条线程执行。

  • b.多条线程里的 cache 数据需要同步。

  CoreData 在不同线程要创建自己的 NSManagedObjectContext,这个 context 里维护了自己的 cache,如果某条子线程没有创建 NSManagedObjectContext,要读取数据就需要通过 performBlockAndWait: 等接口跑到其他线程去读取。如果多个 context 需要同步 cache 数据,就要调用它的 merge 方法,或者通过 parent-children context 层级结构去做。这导致它多线程使用起来很麻烦,API 友好度极低。

  Realm 做得好一点,会在线程 runloop 开始执行时自动去同步数据,但如果线程没有 runloop 就需要手动去调 Realm.refresh() 同步。使用者还是需要明确知道代码在哪条线程执行,避免在多线程之间传递对象。

3.数据不可变

  我们的问题是多线程同时读写导致,那如果只读不写,是不是就没有问题了?数据不可变指的就是一个数据对象生成后,对象里的属性值不会再发生改变,不允许像上述例子那样 book.fav = YES 直接设置,若一个对象属性值变了,那就新建一个对象,直接整个替换掉这个旧的对象:

//WRCache
@implementation WRCache
+(void) updateBookWithId:(NSString *)bookId params:(NSDictionary *)params
{
[WRDBCenter updateBookWithId:@“” params:{@“fav”: @(YES)}]; //更新DB数据
WRBook *book = [WRDBCenter readBookWithId:bookId]; //重新从DB读取,新对象
[self.cache setObject:book forKey:bookId]; //整个替换cache里的对象
}
@end self.book = [WRCache bookWithId:@“”];
// book.fav = YES; //不这样写
[WRCache updateBookWithId:@“” params:{@“fav”: @(YES)}]; //在cache里整个更新
self.book = [WRCache bookWithId:@“”]; //重新读取对象

这样就不会再有线程安全问题,一旦属性有修改,就整个数据重新从DB读取,这些对象的属性都不会再有写操作,而多线程同时读是没问题的。

但这种方案有个缺陷,就是数据修改后,会在 cache 层整个替换掉这个对象,但这时上层扔持有着旧的对象,并不会自动把对象更新过来:

所以怎样让上层更新数据呢?有两种方式,push 和 pull。

a. push

  push 的方式就是 cache 层把更新 push 给上层,cache对整个对象更新替换掉时,发送广播通知上层,这里发通知的粒度可以按需求斟酌,上层监听自己关心的通知,如果发现自己持有的对象更新了,就要更新自己的数据,但这里的更新数据也是件挺麻烦的事。

  举个例子,读书有一个想法列表WRReviewController,存着一个数组 reviews,保存着想法 review 数据对象,数组里的每一个 review 会持有这个这个想法对应的一本书,也就是 review.book 持有一个 WRBook 数据对象。然后这时 cache 层通知这个 WRReviewController,某个 book 对象有属性变了,这时这个 WRReviewController 要怎样处理呢?有两个选择:

  • 遍历 reviews 数组,再遍历每一个 review 里的 book 对象,如果更新的是这个 book 对象,就把这个 book 对象替换更新。

  • 什么都不管,只要有数据更新的通知过来,所有数据都重新往 cache 层读一遍,重新组装数据,界面全部刷新。

  第一种是精细化的做法,优点是不影响性能,缺点是蛋疼,工作量增多,还容易漏更新,需要清楚知道当前模块持有了哪些数据,有哪些需要更新。

  第二种是粗犷的做法,优点是省事省心,全部大刷一遍就行了,缺点是在一些复杂页面需要组装数据,会对性能造成较大影响。

b. pull

另一种 pull 的方式是指上层在特定时机自己去判断数据有没有更新。

  首先所有数据对象都会有一个属性,暂时命名为 dirty,在 cache 层更新替换数据对象前,先把旧对象的 dirty 属性设为 YES,表示这个旧对象已经从 cache 里被抛弃了,属于脏数据,需要更新。然后上层在合适的时候自行去判断自己持有的对象的 dirty 属性是否为 YES,若是则重新在 cache 里取最新数据。

  实际上这样做发生了多线程读写 dirty 属性,是有线程安全问题的,但因为 dirty 属性读取不频繁,可以直接给这个属性的读写加锁,不会像对所有属性加锁那样引发各种问题,解决对这个 dirty 属性读写的线程安全问题。

  这里主要的问题是上层应该在什么时机去 pull 数据更新。可以在每次界面显示 -viewWillAppear 或用户操作后去检查,例如用户点个zan,就可以触发一次检查,去更新zan的数据,在这两个地方做检查已经可以解决90%的问题,剩下的就是同个界面联动的问题,例如 iPad 邮件左右两栏两个 controller,右边详情点个收藏,左边列表收藏图标也要高亮,这种情况可以做特殊处理,也可以结合上面 push 的方式去做通知。

  push 和 pull 两种是可以结合在一起用的,pull 的方式弥补了 push 后数据全部重新读取大刷导致的性能低下问题,push 弥补了 pull 更新时机的问题,实际使用中配合一些事先制定的规则或框架一起使用效果更佳。

  

总结

  对于 APP 缓存数据线程安全问题,分线程 cache 和数据不可变是比较常见的解决方案,都有着不同的实现代价,分线程 cache 接口不友好,数据不可变需要配合单向数据流之类的规则或框架才会变得好用,可以按需选择合适的方案。

APP缓存数据线程安全问题的更多相关文章

  1. 【转】数据存储——APP 缓存数据线程安全问题探讨

    http://blog.cnbang.net/tech/3262/ 问题 一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存 ...

  2. Java基础-线程操作共享数据的安全问题

    Java基础-线程操作共享数据的安全问题 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.引发线程安全问题 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运 ...

  3. redis是单进程数据库,多用户排队对统一数据进行访问,不存在并发访问生产的线程安全问题

    redis是单进程数据库,多用户排队对统一数据进行访问,不存在并发访问生产的线程安全问题. oracle是多进程数据库,存在并发访问的问题,必须事务加锁等方式进行处理.

  4. ASP.NET MVC深入浅出系列(持续更新) ORM系列之Entity FrameWork详解(持续更新) 第十六节:语法总结(3)(C#6.0和C#7.0新语法) 第三节:深度剖析各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字 各种通讯连接方式 设计模式篇 第十二节: 总结Quartz.Net几种部署模式(IIS、Exe、服务部署【借

    ASP.NET MVC深入浅出系列(持续更新)   一. ASP.NET体系 从事.Net开发以来,最先接触的Web开发框架是Asp.Net WebForm,该框架高度封装,为了隐藏Http的无状态模 ...

  5. Core Data 的线程安全问题

    前言: 很多小的App只需要一个ManagedContext在主线程就可以了,但是有时候对于CoreData的操作要耗时很久的,比如App开启的时候要载入大量数据,如果都放在主线程,毫无疑问会阻塞UI ...

  6. iOS五种本地缓存数据方式

    iOS五种本地缓存数据方式   iOS本地缓存数据方式有五种:前言 1.直接写文件方式:可以存储的对象有NSString.NSArray.NSDictionary.NSData.NSNumber,数据 ...

  7. (2.1)servlet线程安全问题

    本文参考链接:http://www.yesky.com/334/1951334.shtml 摘 要:介绍了Servlet多线程机制,通过一个实例并结合Java 的内存模型说明引起Servlet线程不安 ...

  8. SimpleDateFormat使用和线程安全问题

    SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...

  9. SimpleDateFormat时间格式化存在线程安全问题

    想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...

随机推荐

  1. css文件放在根目录之后不起作用原因

    修改为

  2. 无脑抢标——算了吧

    第一版时,我对拍拍贷是恐惧的,缓解我的恐惧的方法,就是寻找尽可能安全的方案.然后,我就发现了这个策略:超过信用等级普通利率的标的.A标一般16,B一般18--那我就寻找大于16的A,大于18的B,C我 ...

  3. ASP入门(二十二)-连接数据库

    ADO 简介 在 ASP 中访问数据库使用的 ADO 组件. ADO 是一项微软的技术. ADO 指 ActiveX 数据对象(ActiveX Data Objects). ADO 是微软的 Acti ...

  4. VS2017安装或卸载错误1303

    为添加一个Python开发环境,运行Visual Studio Installer安装报错,下面是日志: 问题日志 安装出现问题. 可通过以下方式排查包故障问题: 1. 使用以下搜索 URL 来搜索针 ...

  5. JavaScript String 对象扩展方法

    /** 在字符串末尾追加字符串 **/ String.prototype.append = function (str) { return this.concat(str); } /** 删除指定索引 ...

  6. ZH奶酪:PHP 执行时间Fatal error: Maximum execution time of...

    来源:http://stackoverflow.com/questions/5164930/fatal-error-maximum-execution-time-of-30-seconds-excee ...

  7. 使用Chrome保存网页为mht文件

    一直在使用chrome浏览器,但由于需要将部分网页保存为mht文件,却发现chrome默认不支持.chrome浏览器默认支持2种:单独的html文件,全部: 万能的chrome怎么可能保存不了mht? ...

  8. C++ 第二课:操作符号的优先级

           优先级     操作符 1 () [] -> .   ::         ! ~ ++ -- 2 - (unary) * (dereference)   & (addre ...

  9. JEECG中的validform验证ajaxurl的使用方法

    validform验证是一种非常方便的,实用的验证方式 对于需要验证后台数据的,validform是一个非常明智的选择 validform的ajaxurl属性能够完美的实现:当输入完成某一输入框,就会 ...

  10. http 请求报文

    1.报文 2.http请求方法 restful接口 post:创建 put:更新