问题一般iOSAPP做的事情是:请求数据->保存数据->显示数据,一般使用Sqlite作为持久化存储层,保存从网络拉取的数据,读取下次Fetch可以直接从SqliteDB读取。让我们忽略从网络请求数据的链接。假设数据已经存在DB中,我们需要做的是,ViewController从DB中取出数据,然后传递给view进行渲染:这是最简单的情况,随着程序变复杂,多个ViewController必须从DB中取数据,ViewController本身会因为数据变化再次从DB中取数据。会有两个问题:每次数据变化,ViewController都要去DB重新读取,进行IO操作。数据可以在多个ViewController之间共享。比如在Controller1中已经从DB中取出了同样的数据,但是在Controller2中又需要从DB中读取,浪费了IO。要对此进行优化,自然而然会想到在DB层和VC层之间加一层缓存,将从DB读取的数据缓存在内存中,这样下次取相同的数据时,就不需要去磁盘读取数据库。几乎所有的数据库框架都这样做了,包括微信读书开源的GYDataCenter、CoreData、Realm等。但是这样做会带来一个问题,就是数据的线程安全问题。按照上面的设计,Cache层会有一个集合,保存从DB读取的数据。除了VC层,其他层也会从缓存中取数据,比如网络层。上层获取的数据是对缓存层这里数据的引用:也可能被网络层的子线程使用,或者其他子线程进行预加载。修改,同时主线程正在读取这个对象的属性,会崩溃,因为一般我们为了性能都会把对象属性设置为nonatomic,是非线程安全的,多线程的时候会有问题线程读写://NetworkWRBook*book=[WRCachebookWithId:@“10000”];book.fav=YES;//子线程正在写入[booksave];//VC1WRBook*book=[WRCachebookWithId:@“10000”];self.view.title=book.title;//主线程正在读取,可以通过这个测试看到崩溃场景:@interfaceTestMultiThread:NSObject@property(nonatomic)NSArray*arr;@end@implementationTestMultiThread@endTestMultiThread*obj=[[TestMultiThreadalloc]init];for(inti=0;i<1000;i++){dispatch_async(dispatch_get_global_queue(0,0),^{NSLog(@"%@",obj.arr);});}for(inti=0;i<1000;i++){dispatch_async(dispatch_get_global_queue(0,0),^{obj.arr=[NSArrayarrayWithObject:@“b”];});}这种情况的解决方法,一般有三种解决方法:1.Locksince属性这个对象不是线程安全的,那就加个锁让它线程安全。可以为每个对象自定义一个锁,也可以直接使用OC中支持的属性指示符atomic:@property(atomic)NSArray*arr;所以你不用担心多线程同时读写的问题。但是在APP中大量使用锁,很可能会导致各种不可预知的问题,比如锁竞争、优先级倒置、死锁等,会增加整个APP的复杂度,导致问题排查困难,这不是一个好的解决方案。解决方案。2.拆分线程缓存的另一种解决方案是一个线程创建一个缓存,每个线程只读写这个线程对应的缓存,所以不存在线程安全问题。CoreData和Realm都是这样做的,但是这种方案有两个缺点:用户需要知道当前代码在哪个线程上执行。b.多个线程中的缓存数据需要同步。CoreData在不同的线程中创建自己的NSManagedObjectContext,并在这个上下文中维护自己的缓存。如果一个子线程没有创建NSManagedObjectContext,读取数据需要通过performBlockAndWait:等接口跑到其他线程去读取。如果多个context需要同步缓存数据,调用它的merge方法,或者通过父子context层级进行。这使得使用多线程很麻烦,而且API的友好度极低。Realm做的比较好,在线程runloop开始执行的时候自动同步数据,但是如果线程没有runloop,需要手动调整Realm.refresh()来同步。用户仍然需要确切地知道代码正在执行哪个线程,以避免在多个线程之间传递对象。3.数据不可变我们的问题是多线程同时读写造成的。如果只读不写,是不是没有问题?数据不变性是指一个数据对象生成后,该对象中的属性值不会再次改变。如果有变化,不允许像上例那样直接设置book.fav=YES。如果一个对象属性的值改变了,创建一个新的对象,直接替换旧的对象://WRCache@implementationWRCache+(void)updateBookWithId:(NSString*)bookIdparams:(NSDictionary*)params{[WRDBCenterupdateBookWithId:@“10000”params:{@“fav”:@(YES)}];//更新DB数据WRBook*book=[WRDBCenterreadBookWithId:bookId];//从DB重新读取,新建对象[self.cachesetObject:bookforKey:bookId];//替换缓存中的整个对象}@endself.book=[WRCachebookWithId:@“10000”];//book.fav=YES;//不要这样写[WRCacheupdateBookWithId:@“10000”params:{@“fav”:@(YES)}];//更新缓存中的整个self.book=[WRCachebookWithId:@“10000”];//Re-这样在读取对象的时候就不会出现线程安全问题了。一旦修改了属性,整个数据将再次从数据库中读取。这些对象的属性将不再有写操作,多线程同时读取也没问题。但是这个方案有一个缺陷,就是数据被修改后,整个对象会在缓存层被替换掉,但是此时上层仍然持有旧对象,不会自动更新对象:那么如何让上层更新数据么?有两种方式,推和拉。A。pushpush的方式是缓存层将更新推送到上层。当缓存替换整个对象更新时,向上层发送广播通知。这里通知的粒度可以根据需要考虑。更新对象的对象时,需要更新自己的数据,但是这里更新数据也是比较麻烦的。比如Reading有一个idealistWRReviewController,里面存放了一个数组reviews,里面存放的是ideareview数据对象。数组中的每条review都会持有这个idea对应的一本书,即review.book持有一个WRBook数据对象。那么这个时候缓存层通知WRReviewController一个书对象的一个??属性发生了变化。WRReviewController应该如何处理呢?有两种选择:遍历reviews数组,然后在每条review中遍历book对象。如果更新是Forthisbookobject,则替换并更新此bookobject。不管怎样,只要有数据更新的通知,就会重新把所有的数据读到缓存层,重新组装数据,刷新界面。第一个是精致的方法。优点是不影响性能。缺点是比较痛苦,工作量增加,容易漏更新。需要清楚地知道当前模块持有哪些数据,需要更新哪些数据。二是粗略的做法。好处是省事省心,全部刷一次就够了。缺点是有些复杂的页面需要组装数据,对性能影响很大。b.Pull另一种拉取方式,是指上层在特定时间判断数据是否有更新。首先,所有的数据对象都会有一个属性,暂时命名为dirty。在缓存层更新替换数据对象之前,将旧对象的脏属性设置为YES,表示旧对象已经从缓存中被丢弃,属于脏数据。需要更新。然后上层在适当的时候判断自己持有的对象的dirty属性是否为YES,如果是,则再次从缓存中取最新的数据。其实dirty属性的多线程读写就是这样出现的,存在线程安全问题,但是由于dirty属性的读取不频繁,直接锁住这个属性的读写就不会了触发器喜欢锁定所有属性。各种问题,解决读写这个脏属性的线程安全问题。这里的主要问题是上层应该何时拉取数据更新。每次界面显示-viewWillAppear或者用户操作后都可以勾选。例如,如果用户点击赞,则可以触发检查以更新赞数据。检查这两个地方可以解决90%的问题,剩下的就是同一个接口的联动问题。比如iPad邮件的左右栏有两个controller。单击右侧的收藏夹查看详细信息,左侧列表中的收藏夹图标也应突出显示。这种情况可以特殊处理,或者结合上面的push方法。请注意。推和拉可以结合使用。pull方式弥补了push后重新读取所有数据带来的性能低下。Push弥补了pull更新时机的问题。在实际使用中,配合一些预先制定的Rules或Framework更好的协同工作。综上所述,对于APP缓存数据线程安全问题,分线程缓存和数据不可变是比较常见的解决方案,两者的实现成本不同。分线程缓存接口不友好,数据不变性需要配合单向数据流等规则,否则框架会变得易用,大家可以根据自己的需要选择合适的方案。
