CoreData Stack

在文章开始之前,我们先明确两个基本点:
1.CoreData不是线程安全的,NSManagedObject和NSManagedObjectContext只能在自身的线程中使用。
2.NSPersistentStoreCoordinator(PSC)是可以多线程共享,因此一般CoreData Stack中只使用一个。

本篇旨在讨论一种合理的CoreData Stack,下面列出4种常用CoreData Stack,我们将分别讨论它们的优劣以及展示一些基本的测试数据,测试过程或者由此推断的结论若有不妥,欢迎讨论。

Stack A

StackA

如图所示,这是最基本的CoreData Stack的形式,也是默认勾选“Use CoreData”后系统默认生成的结构。
关于CoreData入门的文章一般采取这种结构,然后会告诉你“一般的”应用只要这样就够用了。但我们轻易发现,增删改查都在主线程,数据量一大,主线程肯定是要被阻塞的。带来的唯一结果就是用户觉得你的应用很卡,不流畅。

那么问题来了,什么才是“一般的”应用,数据量要多大才算大?
我们不妨从“流畅”的角度来反推,以每秒60帧来算,每帧保持17毫秒左右。
当UI的刷新率低于60Hz,那么就会出现不流畅。
所以我们要保证耗时操作要么限制在17毫秒以内,要么到其他线程去处理。

我做了一些简单的测试,模型的属性保持10个左右,在没有任何优化的情况下,增查两种操作数据量和耗时的关系如下表所示。(iPhone 6s真机测试)

数据量 查询耗时(毫秒) 插入耗时(毫秒)
100 2.6~3.6 14~18
1000 5~21.6 26~65
10000 56~112 302~573
100000 498~539 3400~3500

以上测试大致可以知道什么是“一般的”应用:
1.单次查询限制在1000条以内
2.单次插入限制在100条以内
3.结合业务场景、老机型处理能力不同等因素,实际应用中以上两个数字必须更小。

优点:易上手,适合学习和demo演示
缺点:一言不合就阻塞主线程

注意NSFetchedResultsController(FRC)performFetch:调用时机。调用performFetch:之前insert数据不会触发代理方法。

Stack B

StackB

Stack B是CoreData多线程的经典结构,采用独立的主线程MOC和后台MOC。
这种结构可以让你灵活的运用其它线程来做增删改查,而且Background MOC可以有多个。
Background MOC可以是临时的,创建MOC非常快,不需要考虑性能问题。

我们需要考虑的是,由于NSManagedObject只能在该线程使用,因此后台MOC查询到的数据模型不能在主线程使用。需要根据objectID在主线程MOC中用objectWithID:方法重新获取数据模型。

下面是StackA和StackB的比较结果,同样是查询了70万条数据并转化成主线程可用的模型:
1.StackA用了1.35秒(直接在主线程查询,不需要转换)
2.StackB用了1.92秒(在后台线程查询,需要根据查询到的objectID,在主线程MOC用objectWithID:方法获取数据)

其实查询的耗时是一样的,但是StackB需要根据objectID重新获取数据,因此多用了0.55秒的时间。
(这次是用模拟器测试的,切勿和上面真机的测试数据对比)

2016-07-01 16:33:24.172 CoreDataTest[4317:171189] StackA:本次查询740300条数据耗时:1.353634秒
2016-07-01 16:33:30.400 CoreDataTest[4317:171189] StackB:本次查询740300条数据耗时:1.919627秒 在主线程耗时:0.555359秒
这里有个问题:“转换耗时”?

主线程MOC调用objectWithID:获取NSManagedObject的时间(就叫做“转换耗时”吧)能不能省掉?
虽然70万条数据的转换耗时只有0.55秒,但是能不能把这个做到极致,在主线程一点不卡?
希望看到这里的朋友思考一下,在评论区提出您的想法,万分感谢😊

另外有个问题:NSFetchedResultsController?

我们在Stack A中使用FRC来管理数据,那么在Stack B中如何使用FRC?这是官方文档中的典型应用,但没有多线程的说明。
按照上面的结构图,FRC只放在主线程MOC,因为FRC与UI关系密切,这点似乎理所当然。
细心的朋友应该发现,图中各种关系仅包含Insert、Update和Delete三种操作,这三种操作可以通过NSManagedObjectContextDidSaveNotification来跨线程合并,那么查询操作呢?

由于NSFetchRequest没有提供objectID相关方法,我们无法将后台MOC获取的objectID转化为FRC中的fetchedObjects。

一次尝试
这里有个哥们提出在NSFetchRequestpredicate中加入@"self IN(%@)", objectIDs来查询。

我按照他的方法做了测试,步骤:
1.在后台MOC中查询
2.遍历查询结果,得到objectID数组
3.切换到主线程,FRC修改predicate,调用performFetch:获取数据

以下是测试结果,看到结果就觉得这方法可行但并不靠谱。
StackB单单在主线程的耗时就比StackA的耗时长,还不如直接在主线程查询。

2016-07-02 10:08:14.423 CoreDataTest[924:41114] StackA:本次查询19766条数据耗时:0.051086秒
2016-07-02 10:08:18.750 CoreDataTest[924:41114] StackB:本次查询19766条数据耗时:0.101769秒 在主线程耗时:0.058902秒

2016-07-02 10:16:20.797 CoreDataTest[967:45523] StackA:本次查询119766条数据耗时:0.231401秒
2016-07-02 10:16:23.882 CoreDataTest[967:45523] StackB:本次查询119766条数据耗时:0.613336秒 在主线程耗时:0.377715秒

小插曲:
测试过程中遇到一个问题,在添加@"self IN(%@)", objectIDspredicate之后,insert操作无法正常触发FRC的代理方法,我的猜想是predicate约束了FRC的监听范围,它只管理满足这个predicate的对象。
于是我又做了一个小实验,将predicate改成@"title CONTAINS %@",@"1",也就是获取名字包含“1”的数据。接着不断insert数据,title是随机5位数字,当随机到名字包含“1”,可以成功insert,而名字不包含“1”则失败。验证猜想。

另一次尝试
这里又有个哥们提出在后台MOC中使用FRC,这样FRC的代理方法都在后台线程调用,如果需要在代理方法中更新UI,切换到主线程更新就可以了。
这样的做法看上去其实挺美好的,但稍有不慎可能就出错了。例如在主线程中修改了属性,甚至读取了属性,都是跨线程使用NSManagedObject。参考文章

目前看来,如果要使用FRC,还是老老实实在主线程查询比较妥当,如果您有更好的方案,求大神指点一二。

优点:独立的MOC可以让你灵活的使用后台线程完成数据库的耗时操作。
缺点:转换耗时问题、FRC的使用问题。

Stack C

StackC

Stack C是iOS 5之后的方案,context之间存在父子关系。主线程MOC有了几个后台MOC儿子。
从结构图中可以发现,子context调用save以后,数据不会保存到数据库,而是上交给父context处理(合并到父context)。

子contex解决了查询耗时问题,但是需要增删改大量数据时,最后还是由主线程来操作数据库,会阻塞。

这篇文章给出一个典型的应用场景:一个存在save和cancel按钮的页面,当你修改了某些数据,点击save即提交到父MOC处理,点击cancel则不需要做任何回滚的操作,因为这个临时工MOC跟你的修改数据可以一起丢弃。

另外,父MOC的修改不会自动通知子MOC,而且大多数情况下子MOC只是临时的,也没有必要通知。

优点:不需要写通知合并的逻辑。
缺点:通过主线程增删改会阻塞。

Stack D

StackD

Stack D是Stack C的一种改进。它解决了增删改时阻塞主线程的问题。
Temporary Background MOC秉承Stack C的优点,解决查询耗时问题,取消修改也不需要任何回滚操作。
Main MOC的增删改会合并到Writer MOC,不需要我们通过objectID跨线程传递,也不需要写任何合并的逻辑。
对开发者而言,Stack D相当优雅。

但这里有篇文章关于Stack BCD三种结构的性能讨论。有兴趣的朋友可以了解一下,大致的结论是Stack B的性能最优异,Stack D性能最差。

简单总结

  1. 数据量小,或者可以分批处理的情况,可以使用Stack A
  2. Stack C基本上没有存在意义,当然如果你的应用查询任务较重、增删改任务非常轻,也可以尝试Stack C
  3. Stack B和Stack D之间如何选择,前者性能更优,后者代码复杂度低
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容