CoreData 与 ReactiveCocoa 混用时要注意的线程问题

今天在回顾公司项目的一个 Crash 报告时,突然想明白它是怎么发生的了。这个问题隐藏在复杂设计的背后,不容易发现,但理清楚以后却又这么简单,这让我直接有了感悟:不要做复杂的设计,同时对底层机制要保持清楚的认识。

让我简单的来描述下这个 Crash 的来龙去脉吧。

这个问题,从用户的角度来表现是这样的:

一个客户端程序,用户打开它以后,它自动刷新内容,在刷新的过程中,用户轻触了某个条目点进去看细节。这个时候,非常低的几率会 crash。

而这个动作的背后后,主要是有 ReactiveCocoa 和 CoreData 在做事情:

刷新内容的时候,CoreData 会在自己的 Queue 里面对从网络上获取到的数据进行本地持久化,完成后会更新 UI;而用户点击某个具体条目时,也是在 CoreData 的 Queue 里面判断条目的评论是不是都获取了,如果没有,则进行网络获取,不同的是,在这里获取后会进行内容渲染(在 ReactiveCocoa 的代码里,需要回到主线程),获取和渲染都完成后,发送 RACSignal 信号并进行 UI 的更新。

刚接手这个项目接触这段代码时,我也觉得有点复杂,不过它一直没出过什么问题,也就不在意了。

后来当我在项目里集成了 Crash 汇报机制后,偶尔收到的 Crash 报告让我引起了注意,这个 Crash 报告是,来自 CoreData 在存储对象时先查询这个对象之前是否存在的代码:

NSGenericException*** Collection <__NSCFSet: 0x1742769c0> was mutated while being enumerated.

这个 Crash 的意思是,在查询对象时,查询对象所在的区域正在被修改。但是我几次确认了代码,查询和存储 CoreData 对象的代码都是在 performBlock 块里面的,只有渲染内容时才会回到主线程,理应不会发生这样的问题。到底是怎么了?

今天我就突然顿悟了,根据下图作解释:

这里的线程情况有点迷惑人,直观看代码还以为只有 commentRenderSeq 和 entryRenderSeq 是 ReactiveCocoa 的 Signal,会跑在自己的线程里。实际上,还有图片箭头所指的 retrieveComments 也是一个 Signal,它通过 flattenMap 功能会创建渲染时的 Signal,它们的全部完成 commentRenderSeq 才算完成。

于是乎,在 CoreData 的 Private Queue 里面执行这个 retrieveComments 的时候,这个代码块实际执行块已经脱离当前 CoreData 的线程,跃迁至 ReactivieCocoa 自己维护的线程里面去干活了。这个时候,如果恰好有另外一个 CoreData 线程在修改数据,而这个 ReactiveCocoa 也在动相关数据,于是上述的 crash 问题造成了。

ReactiveCocoa CoreData

问题在我去看 Crash 报告时的所有线程情况时得到了验证(啊,如果我一早就养成检查所有线程情况的习惯,这个问题就能更快解决了)

这个 #8 是 Crash 的线程,如图所示,这个线程来自 ReactiveCocoa,在 CoreData 的 NSManagedObjectContext 查询时发生异常

reactivecocoa-and-coredata-01

与此同时,#4 线程,也就是通过 CoreData 的 performBlock 里自己维护的一条独立线程,在这个时候它也在 perform 数据,于是就产生了前面所述的两条线程同时修改数据造成的问题了。

reactivecocoa-and-coredata-03

怎么解决这个问题?重构这部分代码,放弃这个复杂的设计,用更简单直白的即可。

通过这个 Bug,我感悟到现在很多问题都是由多线程引起的,而我在 Debug 的时候却只看当前线程,没有养成查看所有线程的习惯,导致这个问题困扰我很久。幸好对 ReactiveCocoa 和 CoreData 的基本特性还算了解,知道它们都是会维护自己的线程并在里面做事,不然还真找不到这个问题。

最后,不要做复杂设计的,线程不是量子,不要跃迁三次以上!不然还真找不到 Bug :D

<推广> 本站架设于 Linode 东京机房,同时使用 云梯 进行科学上网

7 Comments

  1. idea

    先把 CoreData API Wrap 成 RACfy, 再组合着用比较好.
    像这种: https://github.com/apparentsoft/ReactiveCoreData

    • ReactiveCocoa 都不想用了,还是手写自己包装方便。

      • idea

        我也用 RAC 一段时间了, 感觉 RAC 那种声明式编程, 在可读和维护性上有挺大的优势.
        可以说说有哪些原因要放弃 RAC 吗?

        • 确切说应该是这样的。

          我不是这段代码的原作者,自己只会认 ReactiveCocoa,但基本不写。考虑到我自己的项目用的是 Swift,RAC 现在只维护并且在积极开发它的 Swift 版本,于是我就不考虑在项目里用 RAC,等到它的 Swift 版本出来了,我才会去考虑用。

          所以应该不是说「放弃」,而是在「观望」

  2. IKET

    楼主在花瓣网?杭州用ReactiveCocoa的似乎不多啊。

  3. PrideChung

    我也是不想用RAC了,但是又很喜欢那种异步的写法,最近在考虑用PromiseKit来代替RAC写异步。其他部分能用原生写法还是尽量用原生写法。

  4. 一直觉得 ReactiveCocoa 很 evil, 为了局部的省事, 丢掉了大局的模式!

Leave a Comment