[转] --- Core Data 线程大揭秘

原文地址: Core Data 线程大揭秘, 作者是Marcus Zarra.

Marcus Zarra
Marcus Zarra 最为出名的就是他精通 Core Data、持久化存储以及网络等相关知识。他从 2004 年开始开发 Cocoa 应用,绝大多数时间他都用在开发软件上面。Marcus 是 Core Data: Apple’s API for Persisting Data under Mac OS X 的作者,还是 Core Animation: Simplified Animation Techniques for Mac and iPhone Development 的作者之一,此外还是 [Cocoa Is My Girlfriend] 博客的作者之一。

到了今天,Core Data 中的线程实现机制已经与其最初版本大相径庭了(也就是 iOS 6 之前的版本)。在 Core Data 的悠久历史中,多年来关于如何使用线程这个话题已经有了数种不同的解释,那么我们到底应该怎么做呢?在本次 #Pragma Conference 2015 讲演中,Marcus Zarra 为我们展示了实现线程的三种方法,旧有的、复杂的以及最佳的。

概述 (0:00)

本次演讲的目标是让大家清楚,在 2015 年的今天,我们应该如何正确地使用 Core Data。我们首先以我的基本准则开始,也就是当使用框架或者 Core Data 时候,如何正确地使用编译闭包(building blocks)。接着,我会在一个多线程环境下和大家讨论使用 Core Data 的旧有方式、复杂方式以及最佳方式。最后,我会总结一些指南,以及 Core Data 中一些复杂情况的注意事项。

Core Data 已经存在了 11 个年头了,但是对我来说它仍旧是一个新事物,我依旧孜孜不倦地汲取其中的知识。对于一个框架来说,11 年确实是一个很长的时间了,尤其是处于移动化趋势的今天。使用老框架的其中一个问题是,11 年来一直都有关于 Core Data 的博客和文章发布出来,然而其中的某些已经失效了,存在着许多冲突的信息。

为什么(不)用线程呢?(3:16)

在我使用线程之前,我都会问自己:“这是否有必要?”。经常有人过来跟我说:“好吧,我们遇到了一个关于 Core Data 的问题。我们向 Core Data 应用中添加了线程,现在我们被逼入死角了。”我会反问他们你们为什么要添加线程呢?这听起来似乎答非所问,但是我总是会这样做,因为大家总是做错了!人们的回答通常是为了解决性能问题。然而,使用线程来解决一个性能问题通常会让其变成两个、四个、八个甚至更多的性能问题。

线程不是万能的。往应用中添加线程取决于架构的设计,我们如果是在发布前的几个小时中才做出的这个决定,那么就大错特错了。向应用中添加线程应该是在我们发现 CPU 有空余的时间和带宽的时候才进行的,或者是当我们想要预测用户的下一步动作并提前进行准备的时候才使用线程。

其中一个例子就是 Twitter 应用,也是我最喜欢的例子之一。当你登录到 Twitter 的时候,我们需要使用线程来获取用户还未看到的图像。此外我们还可能会缓存头像文件、获取检索结果等等。我们需要使用线程来预测用户将要做什么,然后提前为他们准备好数据,这样当用户使用的时候,就会惊叹访问应用的速度何其迅速。其实实际上并不是访问速度够快,我们只是在后台提前做好了一切准备。

我们在后台为用户做好准备,这样用户就无需进行等待。比如说发 Twitter 这个操作,用户根本无需等待网络请求返回。我们不需要在屏幕上放一个提示框,告诉用户:“嘿,我的时间比你的时间更为宝贵。你就等在那,如果 Twitter 发送成功了我会告诉你的。在此之前,你不能做其他事情。”多么差的用户体验,对吧?线程便可以解决这些极差的用户体验。

Core Data 中的线程:核心概念 (6:43)

关键是保证数据源唯一

当我们在使用 Core Data 的时候,我们通常会使用一个或多个 NSManagedObjectContext,这其中只能有一个作为数据源,也就是用户界面唯一能够访问到的。用户界面是我们的关键所在。我们开发的应用主要是面向用户的,而不是面向服务器的。我们在 iPhone、iPad 以及个人电脑上开发这些应用。用户应该随时随地都能够看到界面,得到反馈。因此,我们只能使用一个上下文 (context) 专门与用户进行交互,以及为用户展示界面元素。

“关键是保证数据源唯一(Single Source of Truth)”是一个专业术语,意思是“这个数据源作为核心存在,每当我们需要确认哪几种可能性是正确的时候,这个数据源提供的数据始终都是正确的”。

UI 线程也是核心所在

所有的用户界面都是单线程的。我使用过的所有语言中,它们的用户界面都是运行在单线程上的。如果你打算在后台线程中显示一个 UIViewController 的话,这可能会导致错误的发生。由于 UI 是单线程上运行的,因此我们应该使用唯一的数据源来构建 UI。

非用户控制的数据不应该在主线程上进行操作

如果我们不对 UI 进行控制的话,那么我们就不应该在 UI 线程上进行数据操作。在解析 JSON 的时候,我们应该避免在 UI 线程上进行。在上传数据到服务器上的时候,无论是 Twitter 消息还是银行交易信息,我们也不应该在主线程上进行。UI 线程仅供用户界面的操作,其他都一边去。

坚守这三条原则就可以解决大多数 Core Data 中出现的性能问题。无论我们在哪个系统中使用 Core Data,我们都会规避掉大多数性能问题。

旧有方式 (9:34)

所谓的“旧有”方式,实质上指的是最原始的 Core Data 实现方式,也就是我们在 iOS 6 之前实现 Core Data 线程的方式。苹果的编程框架在 iOS 5 中搞了一个相当大的观念转变。在我看来,这导致大量开发者涌入 iOS 开发当中来。在 iPhone 变成街机之前,你或许可以用一只手将 Objective-C 开发者数过来。我们都有着相同的邮件列表,我们互相都认识,我们甚至还认识在苹果里面工作的那些工程师。苹果会发布一些很激进、变化很大的框架,而且不会告诉你它们的使用方式,因为他们知道我们会去自行慢慢探索。接着我们会对问题进行讨论,这完全是因为这个圈子实在太小了。线程的情况也和这类似,线程的规则不是很清晰,每个人都有各自的看法。因为这完全是一个全新的技术。

接着,突然之间一下子涌入了成千上万名开发者。在 2006 到 2008 年间,我觉得开发者的数目从 近万名变成了近百万名。我记得在 WWDC 上,整个会场都充斥着一队队人,讨论着“好的,如果你使用了 retain,它会将保留计数 (retain count) 加1。当你释放的时候,保留计数还会再减1。当它变成0的时候,这个对象就被销毁了,而如果它变成了-1的话,就会发生崩溃。”他们需要一遍又一遍的重复这个话题,因为这个概念十分难以理解。新加入 iOS 开发的很多人对 Objective-C 并不十分擅长,因此他们总是问“所以说我们必须要手动管理内存咯?”之类的问题。这是因为这个概念它们之前从未接触过。

因此,苹果必须要弄出一个范例来告诉大家如何使用这些框架和 API。其中之一就是线程,因为线程实在实在太难了。我们所有人都曾犯过错误,即使是苹果内部的开发人员也是如此。在这里,这是一个很难解决的问题。提供的范例建议大家使用队列 (queue) 而不是使用线程。如果你做的对的话,除非你将两者混合使用,不然的话使用队列和线程是没有太大区别的。队列十分好用,因为它们的工作效果和线程一样,即使我们写了错误的代码,它们仍能保护好程序现场,这的确是一个很酷的功能。

在 iOS 5 中,Core Data 发生了变化。它们展示了 ManagedObjectContext(MOC) 的三种类型,我们可以选一种类型来使用。这样做的目的是为了让线程使用更加简单,但是这在 iOS 5 中并不是那么好用。它十分古怪,并且还有点不稳定。在 iOS 6 中,苹果解决了问题,让其变得更加稳定,因此在 iOS 6 上它们才能切实的工作。

译者注:此处用的是图例,没有代码。如果希望查看 Keynote 的话,请观看。
在 iOS 6 之前,这就是我们使用 Core Data 的方式,它现在仍然能够使用,除非为了处理某些边缘案例,否则的话基本上是不会有人想使用它的。我们会以一个 PersistentStoreCoordinator(PSC) 开始,它用以处理与硬盘的所有交互动作。它一般用来从硬盘取得数据,将其转换为对象模型以及将其放回硬盘。应用中只使用一个 PSC 即可。

每次我们创建 MOC 的时候,它都会直接与 PSC 进行通信。所有的 MOC 都会与 PSC 进行交互,但是MOC 之间无法进行沟通。当我们想要让某个上下文知道另一个在其他线程上的上下文发生的变化,就十分困难。我们必须通过通知系统 (notification system) 来处理这个问题,因为每次我们对 MOC 进行改变的时候,都会广播一个通知,这样我们就可以捕获这个通知,然后在另一个上下文中对其进行处理。我们必须正确地使用这个功能。

这会导致一系列问题发生,因为这个系统已经优化了好几年了。当 Core Data 第一次推出的时候,每个人都有自己的看法。然而这几年即使越来越多的规则出台,人们仍然还是十分困惑它的用法。

在这个系统中,我们通常使用一个唯一数据源来与用户界面沟通,也就是核心 MOC,我们让其将变化反馈给用户界面。我们并没有别的办法来特别定义它,因为只有一种定义上下文的方式。用户界面不会在其它线程中出现,因为它是单线程的。

使用旧有方法会出现的问题 (16:26)

这种设计导致的问题就是海量的代码。以前人们总是抱怨 Core Data 非常难用,并且在现在这个年代还要写这么多样板代码简直就是反人类。这么做会导致在你代码的末尾,可能会散落着大量的 Core Data 代码。

线程规则同样也不是很清晰。在 iOS 6 之前,如果你在网上询问如何处理 Core Data 中的线程的话,每一个人都会有不同的回答。这些回答可能有“将上下文进行锁定,然后同步使用就万事大吉了”,这实际上并不是很好用。接着,还会有“为每一个线程准备一个上下文,从某个上下文中出来的数据都只在那个线程中使用。你可以跨线程读取数据,但是切记不要写数据。”然后,就是“跨线程读取数据也是不可取的,只能够在单个线程中进行数据的读写,保持数据的线程唯一性和孤立性。”然后人们就会发现除非你遵循数据孤立 (siloing) 原则,否则的话操作就是非线程安全的。

线程的规则十分混乱。我们没有办法来验证或者决定哪个规则是完全正确的。你或许会想可以在代码复查的时候提供不同的参数。谁知道这么做什么时候会发生崩溃呢?就算应用崩溃了,你仍然无法确认崩溃原因,因为很可能并不是那段代码导致的崩溃。线程这种东西,很难说。

此外,我们有些时候会在错误的时间阻塞了错误的线程,然后整个应用就崩溃了,然而你并不知道到底发生了什么。或许有人会给其他线程中进行通知的监听,然后在错误的线程中使用的时候对其进行捕获;也有可能当 UI 做了一些诡异的操作后,然后整个应用就崩了。你会将整个应用中布满 NSLog,试图捕获错误,以找出导致错误的问题。

当你开始监听通知的时候,通知会变得十分活跃。当你开始这么做的时候,你或许会有点急于求成,然后开始在所有地方对通知进行监听。这会影响性能,进而导致意外的线程死锁,然后你就陷入了死循环,做着无用功。你会发现绝大多数情况下应用都好好的,只有偶尔才会发生崩溃,你试图找到崩溃的原因……于是你已经陷入了死胡同。

好消息是在 iOS 9 中,我们现在拥有了调试标记,这允许我们确定在使用的线程是正确的。我们现在至少能够确认这个线程是否工作正常了。

旧有的实现方式非常困难,而且容易出错。

复杂的方式 (21:53)

就我个人而言,我很喜欢使用这种复杂的方式,因为它很有极客风范,不过我并不希望经常在产品中看到这样的使用方式。

SQLite 是我们经常使用的持久层 (persistence level) 技术。我们用它向硬盘上存储东西。它被设计为准许多进程访问 (multi-process access),因此我们可以使用多个 PSC 与之相联。我们可以同时使用多个线程与之进行沟通。

如果我们有多个 PSC 的话,每个 PSC 都可能会有多个 MOC 与之关联。现在,我们突然就不需要担心死锁的问题了,也不用害怕线程或上下文之间的阻塞问题了。我们可以开心地从一端推送数据,然后从另一端接收这些数据。

我们愈来愈接近真正的 PSC 异步了。当然,这仍然还会存有许多阻塞的问题,即使您通过了测试,您还是有可能遇上阻塞的问题。原因就在于大多数 PSC 所做的工作都是在 CPU 当中完成的。它在内存中获取我们的对象,然后将它们放入 SQLite 调用语句当中,以此来准备与 SQLite 进行对话。这就是存储、检索以及读取操作所发生的地方。接着在一部分周期 (silver) 中与数据库建立连接。在这个周期当中,如果您访问了相同的表以及记录,并且同时对其进行操作的话,你就会陷入到死锁当中。这并不是百分百的异步操作,不过99.99%的情况它都有效,每年它的成功率都在上升,遇见死锁变得越来越困难。好消息是我们可以越来越接近所谓真实的异步。

即使在设计当中,我们仍希望用一个上下文来与用户界面关联。我们不希望使用多个上下文。我们就让其存在一个线程当中,和用户界面建立关联即可。

使用复杂方式的问题 (24:34)

最大的问题就是这个方法实在实在太难了。除非你试图解决一个非常特殊的问题,否则我不建议您使用这个方法。这个方法对于我们来说很难完全做对,因为线程本来就很难了,但是在这里我们竟然还要给线程上添加其他层级。之所以这么做是因为 PSC 之间不能互相交流。至少当我们拥有多个 MOC 的时候,我们会与单个 PSC 进行交流,这个 PSC 就会知道发生了什么。上下文就可以根据此执行查询或者从中获取一些信息。我们甚至没法让多个 PSC 同时访问单个 SQLite 文件。其中一个 PSC 用以写入文件,其他用以读取,它们之间无法沟通交流。不过我们可以通过同步很简单地取出数据。

在这个系统中通知也变得十分难以实现。不过在 iOS 9 中发生了很大变化,它们添加了一个新特性,允许我们响应远程通知。然而,您仍然不能从一个 PSC 中发送通知,然后在其他 PSC 中接收通知,因为没有办法执行这种操作。我们需要做很多处理才能让一切顺利。

相比第一个版本来说,线程的行为也变得更加诡异。在这个设计中,我们发现了成千上万条线程问题,因为我们必须确保我们的 PSC 和 MOC 都能够在正确的线程中相互沟通。

可维护性更是想都别想。当我们被问题卡住的时候我们会采用原始的方式,然后为其添加更多的复杂层面 (layer of complexity)。

我们为什么要采取这个方式呢?通过 watchOS,我们可以借此来获得多个进程访问的能力,比如说两个进程同时访问相同的 SQLite 文件。随着我们的观察,我们可能需要让多个应用都能够访问同一个 SQlite 文件。我们非常需要这个功能,因为越来越多的应用实际上都只访问一个相同的 SQLite 文件了。我能够很好地理解这个观念,因为这也是 iCloud 与 Core Data 协同工作的方式。于是您就可以理解为什么 iCloud 能够如此之快的变得好用和稳定。

最佳方式 (27:47)

最后就是我们处理线程的“最佳”方式。所谓最佳,指的并不是最快。如果你在寻找快速的持久化引擎的话,您不应当使用对象驱动的引擎。你不是在寻找 Objective-C、Core Data 之类的替代品。面向对象编程是非常慢的。如果你需要速度的话,你需要使用 C 或者 SQLQite 这种更底层的东西。通常情况下,最快的代码往往是这个世界上最丑、最令人讨厌的了。

我已经写过一些运行起来很迅速的引擎,但是我并不会为它们而骄傲。因为它们太丑了。代码中存有很多很多的注释,因为不这么搞的话六个月后我就完全看不懂了。

在我看来,“最佳”的就是最容易用的,同时也是最容易维护的。我可以用一杯咖啡的时间来阅读并理解这些代码。这些代码非常简单,我无需使用白盒测试就可以找到 BUG 所在。

如果一个代码难以调试的话,那么就没有人愿意为这个代码编写边缘测试了。如果六个月后我自己写的代码都看不懂的话,我想所有人都会疯的吧,你必须要从头开始。

在最佳的方式中,我们只用一个 PSC,但是我们要使用 iOS 6 中所提供的新 API。我们打算添加一个与 PSC 进行通话的私有 MOC。接着,我们添加并定义核心上下文,然后让其成为私有 MOC 的一部分。所有的数据操作都会在核心 MOC 下面进行,因此我们将会拥有三个级别的上下文。

除非我们定义了一个新的核心上下文,否则核心上下文不会发生改变。我们让其与 UI 进行关联。无论是哪一个核心上下文,都只能在 UI 线程上使用。如果我们试图在其他线程中使用它的话,我们就会有一个调试标记,发生崩溃,这样我们就知道哪出问题了。

这种设计允许我们异步保存,这个功能非常重要。它允许我们不必阻塞 UI 就可以存储并访问数据。用户在滑动我们应用的时候就会非常开心,因为不会有卡顿的情况,也不必让用户等待。对我们来说这么做的代码也非常少,八行代码即可搞定。

使用最佳方式的问题 (31:47)

我承认,找这个方式可能会带来的问题实在让我江郎才尽了,因为目前最正确的方式,因此它并不存在很多问题。

你在网上看到的最大问题就是它的速度比较慢。在核心 MOC 与 PSC 之间加入了一个额外的间接层,因此这里我们的访问速度会被减慢一些。我说一些,指的是如果我得编写一个测试用例,它进行成千上万次迭代操作,这样我才会发现它们在速度上的 1~2% 差异。但是从技术上来说,的确,它的速度要慢一些。重申一下,如果你追求的是行读取速度 (raw speed) 的话,你就不应该在 Objective-C 中寻找解决方案。

另一个问题是它对新手来说很不友好。这里面有太多的模块在进行工作了,内部也有很多事情发生,然而这些工作任务就没有用代码来明确指明。一个新手可能会觉得:“我在这个 NSOperation 上的私有上下文中保存了这个数据,然后 UI 就发生了更新。这是怎么发生的?”在代码中并没有直接的链接,因此对于不理解 Core Data 的开发者来说这的确有些难度。然而,公平而言,Core Data 确实对不理解它的人不友好。Core Data 与其他语言中的持久化相比,走上了一条完全不同的道路,因此它十分难懂。

这同样也会导致代码量的加重。我必须要谈一谈人们实在太依赖于代码块了。它们将代码块当作一个有趣的新东西使用。我曾经见过一个类中有超过 12000 行代码的持久化存储层,因为所有事情都在一个闭包中进行。他们可能被这个方式误导了,因为代码块的易用性,很容易导致我们写出越来越多的代码。你在代码块中嵌入一个又一个的代码块,然后六个月后当你重新检视代码的时候,你会发现:“为什么这个持久化存储层全部集中在一个对象里面?”因此,这会导致这个问题的发生。

要解决在这个系统中容易出现的我称之为“代码堆叠 (code puke)”的这个问题,比如说,代码块中嵌套代码块。代码块非常好用,但是由于在其中添加另一个代码块实在太容易了,因此总会导致左边缩进越来越长、越来越长。我们不可能让左边缩进长出好几页,对吧?这是其中一个问题。

除此之外,就没有什么太大的问题了。在速度上略有下降就导致很多人放弃使用它,这让我感到十分困惑。

指南 (27:20)

关键是保证数据源唯一

因此,绝大多数线程和 Core Data 中出现的问题,都可以通过保持用唯一一个 MOC 与用户界面关联而直接解决。如果你忽略我的其他建议,只严格执行此建议的话,你仍然可以避免人们在运行持久化系统时出现的绝大多数问题。

不要重用子 MOC

如果你在使用“最佳”方式的话,千万不要重用那些在核心 MOC 下的子 MOC。它们非常廉价,用一次然后丢掉就可以了。不要为它们建立缓存,也不要让它们和线程建立关联,因此不要在线程池中重用它们,也不要做一些其他我曾见过的“自作聪明”的事。创建出来,使用,保存,然后就可以扔掉了。它们完完全全是一次性的。

这样做的原因是因为数据变化只会向高层走,而不会向下流动,也不会平级移动。如果我在核心 MOC 中的一个子 MOC 接受数据的话,我一旦保存了数据,它所造成的变化就会立即在核心 MOC 中得以显现,然而这个变化却不会反馈给其他子 MOC 。如果我在内存线程池中包含了 10 个子 MOC以便重用,它们会很快的变得完全不同步。可以将它们视为某一个时间点的快照。不要期望其他变化能够在它们上面显现出来。

在子 MOC 存储中使用 NSFetchedResultsController 会阻塞主线程

如果你在使用 Core Data,你们的 UI 开发过来告诉你“你的 NSFetchedResultsController 阻塞了我的用户界面。Core Data 就是个垃圾。”的时候,然后他们会拿出 Instrument,然后证明 NSFetchedResultsController 是导致 UI 卡顿的罪魁祸首。出现这个情况的时候,你需要将那个小箭头放到 NSFetchedResultsController 下方,这样你就可以发现实际上是他们的某个表视图单元格造成了这个性能问题。

这看起来很盲目。这通常是当人们第一次寻找 UI 性能问题时所做的,简单而言是因为这是 iPad/iPhone UI 和 Core Data 之间的分歧所在。当你在 Core Data 中进行大量的变动时,这会导致整个用户界面发生变化,因此问题会在那里显示出来。

Instruments,Instruments,Instruments,重要的事情说三遍

当我们在使用 Core Data 以及编写 UI 的时候,我们应当经常使用 Instruments 这个工具。我们应该确定造成性能问题的原因所在,而不是靠日志输出或者其他测试来猜想原因。在使用 Core Data 、编写数据输入和数据输出的时候应当经常使用 Instruments,并且还要确保你没有在主线程上执行这些操作。我在写代码的时候有无数次觉得“哇,这段代码好赞”,然而一把它在 Instruments 中运行后,我就发现我在主线程上传递了一个 JSON,从而阻塞了 UI。Instruments 会阻止我们做类似的事。它会帮助我们查看数据是如何传递的,并且数据传递所在的线程,从而避免绝大多数性能问题的发生。

问与答 (41:24)

问:一个有 12000 行代码的类……这些人一年能做几次代码校对啊?

Marcus:他们从来没做过。他们是一个在 San Francisco 刚成立 80 个小时的创业公司。没有人有时间做代码校对。这对于小的创业公司来说是一个非常常见的问题。这就是所谓的“创业者心态”:“我们有钱,但我们有不现实的截止时间,所以我们可以让程序员加班加点以确保能在规定时间内让产品上线,之后我们再来决定应用应该怎么做比较好。”这种心态驱使着他们。不幸的是,这已经司空见惯了。

问:所以说最后他们开始第二个版本的构建时,决定开启代码校对工作,结果却……

Marcus:没错,产品发布之后,他们就把我请去了三个星期,因为他们的主程序去休假了三个星期,然而他们在两周内就上架了。之后,主程决定去别处工作。我们搭建了一个全新的开发团队,决心重构整个应用。

问:您说用 Core Data “最佳”方式来组织代码。我想,当您存储子 MOC 的时候,应不应该将存储事件 (save event) 传送给核心上下文呢,这样让子 MOC 最终保存到持久化存储区域内?或者说,只需要存储对核心上下文的变更,然后等待某些其他的事件将子 MOC 存储到数据库中呢?

Marcus:我想给你一个我最喜欢的答案:这取决于你的需求。当我们使用“最佳”方式设计的时候,我们不再试图考虑何时存储这个问题。在旧有方式中,我们曾经会这么考虑:“好吧,我需要在每出现 10 条记录或者对 UI 变化的时候进行存储”。如果我们这样做的话,就可能会得到某种奇怪的记录,于是我们使用 #define 去定义:“好吧,如果我们每 6 条记录进行存储的话,那么就应该足够快了,这样我们就不会觉得表视图会有卡顿的现象了”。然而,使用“最佳”方式设计的话,我们不再受限于此。我们不在主线程上执行 IO 操作,因此我们可以根据我们的需求来决定如何处理这个问题。这完全取决于那时的数据。这个数据是可恢复的吗?再次获取这个数据容不容易?如果是,我可能会过会儿才进行存储,也可能会在退出的时候进行存储,还有可能完全不管这事儿,因为就像 Twitter 广播一样我们可以重新获取。此外,这个记录要不要用在其他地方呢?如果是,我就会打算立即对其进行存储,从而让其成为一个通用数据或者通用的业务需求。这个数据是不是很有价值?恢复这个数据容不容易?如果恢复很困难,甚至不可恢复的话,我们或许会对其进行保存,进行备份,创建拷贝。然而,如果获取十分容易的话,我或许会在之后对其保存,可能会在主线程将其抛出,也可能在退出应用或者用户观看视频的时候进行保存。万一我们在启动 Twitter 的时候接到了电话呢?检测到这个情形后就可以对数据进行存储了。这让我们可以根据需求来决定是否“我必须立即对数据进行保存,因为这会影响到 UI”这个问题。

问:如果你必须要有两个子上下文的话,那么如何在它们之间共享变化呢?

Marcus:这个操作是完全开放的,你可以实现它们,但是这和平常的操作格格不入。您可以使用通知来强制其中一个子上下文从其他子上下文那里接收更新,但是不要这么做。这是一个非常糟糕的注意。最好的办法就是将这种方法通通抛弃。如果你必须要要有两个子上下文的话,并且其中一个将会决定另一个的行为表现的话,那么应该确定第一个之后才创建第二个。创建子上下文非常简单,因此你应该在需要的时候才创建它们。这样做的话,或许看起来就像同一个操作了,这样你就可以用相同的上下文来操作不同的部分。不过不要试图在子上下文之间共享数据,这会导致意想不到的情况发生。

问:我知道您对 Realm 非常熟悉。我想知道您对 Realm 以及 Core Data 的看法。我们应该使用哪一个呢,或者说哪一个更加适合您呢?

Marcus:我会说一些关于 Realm 的问题。首先,我对第三方代码的观念已经众所周知了:所有的这些代码都糟糕透顶。我觉得 Realm 在试图解决一个本身就不正确的问题。他们试图比 Core Data 更快,而 Core Data 已经足够快了,只是可维护性较差。我在尝试 Realm 的过程中,发现两者的代码写起来基本差不多。他们的迁移操作对我来说也有点诡异。他们试图更快,这是一个好主意,不过这并不是我想要的。作为一名项目经理或者说一名开发者,我所要的是可维护性和连贯性。我对第三方框架的首要看法就是他们如同昙花一现,没有什么可持续性。这事发生了一遍又一遍。我们不知道 Realm 能够存在多久,我不是很了解他们的商业模型。Core Data 对我来说也已经足够了。它已经非常成熟了,存在了很久,并且也已经足够快了。如果它的速度不够快的话,就说明我可能在哪做错了。关于 Realm 来说我仍然还有很多未知的地方。它的存储机制没有公开,这让我有点害怕。相反,对于 Core Data 而言,它已经是广为人知了。苹果不会抛弃它的。SQLite 的机制是透明的,我可以查看数据,我可以获取数据。即使第二天它就垮了,我仍然可以查看它们。对我来说,这已经足够好了,这也是我经常所使用的工具。对于 Realm 来说有没有什么不好的呢?并没有,大家可以尽情地尝试。它或许对大家来说十分有用,不过对我来说,它并没有解决我想要的问题。它并没有显著优于 Core Data,你没法说:“哇,这简直太赞了,为什么还会有人用 Core Data呢?”,相反,你只能说:“好吧,这非常快。棒极了,挺好的。”它的代码仍然很多,并且它还没有 Core Data 那样成熟。一年后再来问我这个问题,或许我就会改变我的想法了。

Realm 注:我们认为我们的首要设计目标是简化开发和增强可维护性,而不是增加速度。特别是,我们已经花费了很多时间来设计线程模型,我们相信这应该是比 Core Data 更为简单的。Realm 的 Objective-C 和 Swift 框架 层目前已经开源,我们的底层存储层也即将开源。关于我们商业模型的更多信息,您可以随时查看我们的价格页面,或者如果有任何问题话欢迎来联系我们!

你可能感兴趣的:(iOS-CoreData)