本书向读者介绍使用Core Data时需要特别注意的事项,这将帮助读者避开使用Core Data这个十分灵活且异常强大的框架时的一些陷阱。我们从一个简单的应用例子开始,逐步将其扩展为包含关系、高级数据类型、并发、同步以及其他很多特性的完整例子,并在这个过程中对所有这些主题进行讲解。在本书后半部分,我们还会超出这个基本应用所需要涉及的范围,将知识点深入扩展到Core Data幕后的工作原理上。我们会学习如何获取高性能、不同Core Data设置之间的权衡,以及如何对Core Data代码进行调试和性能测试。
本书所有的代码都使用Swift编写,我们也展示了如何将Swift的语言特性融入Core Data中,并写出优雅和安全的代码。我们希望读者在阅读本书的时候有一定的Swift和iOS开发基础,不过相信不论是新人还是富有经验的开发者,都能从本书中找到实用的信息和设计模式。
苹果原生数据库首著问世 喵神领衔objc精品iOS开发系列图书
译序
在 20 世纪 60 年代,导航式数据库的概念随着磁盘直接存取而发展起来;从 70 年代开始,关系型数据库登上历史舞台,它的概念一直延续至今。我们无法想象现代的计算机程序中离开了数据库会是怎样的景象,数据库技术已经成了这个世界方方面面的基石。
在数据管理和数据库相关的方面,Apple给出的选择是Core Data。正如在简介中所提到的那样,Core Data其实并不是一个传统意义上的数据库,而是一套对象图管理系统。这套系统默认使用SQLite作为底层存储,通过由低向高地将相关的管理组件构建为一个栈,来提供缓存和对象管理机制。这让我们对于数据对象的存储和访问都能够高效而有序地进行。从这一点上来说,Core Data与单纯的数据库相比,实在是强大得多。
但是能力越大,责任也越大。如果使用不当,那么Core Data不但不能为你提供良好的数据存储和访问的性能,甚至会连最基本的操作都难以保证。在这种情况下,Core Data将不再是你开发的助力,反而会成为掣肘。不幸的是,Core Data本身学习曲线比较陡峭,而涉及的概念又非常多,所以真正想要精通Core Data并完全发挥它的效能并不是很容易的事情。
Apple在iOS的很多原生应用中大量使用了Core Data,比如照片、音乐和iBooks等,并且事实证明它们都出色地完成了任务。在国外,也有很多开发者使用Core Data作为应用程序的数据层和持久化的选择。相比其他第三方的解决方案,Core Data不需要引入额外的框架,也相对稳定可靠。但是在国内,现在使用这项技术的开发者较少,大家对Core Data的研究也普遍没有国外深入,这导致了提到Core Data很多人会不自觉地抗拒和躲避。将Core Data的使用方法和最佳实践以更容易理解的方式带给国内开发者,促进大家接触Core Data的架构和思想,这正是我们选择翻译本书的目的。
本书的结构和阅读方法在前言中会有所说明,这里就不再赘述了。需要补充的是,本书里提供了大量的例子和相应的代码,它们大多是需要进行权衡的选择,并对应了不同的场景。只有在你充分理解这些例子的含义后,你才可能在实际使用时做出正确的判断。另外,Core Data的灵活性是一把双刃剑,当你选择了更多的上下文以及协调器时,也意味着你为项目引入了更多的复杂度。尽可能在能够满足需求的前提下,选择最简单的Core Data栈设置,是高效、正确使用Core Data的关键。
本书原著的两位作者有着多年的Core Data使用经验。Florian Kugler是objc.io的联合创始人,曾经为objc.io撰写了很多Core Data相关的文章,深受读者喜爱。Daniel Eggert曾供职于Apple,帮助Apple将照片应用迁移到Core Data框架内。他们的努力让Core Data这个看起来有些“可怕”的框架变得平易近人,借此我们可以一窥Core Data的究竟。不过不论是原作者还是译者,其实和各位读者一样,都只不过是普通开发者中的一员,所以本书出现谬漏可能在所难免。如果你在阅读时发现了问题,可以通过出版社联系我们,我们将及时研究并加以改进。
最后,祝你阅读愉快。
——徐 涛 钱世家 王 巍
前言
Core Data是Apple为iOS、OS X、watchOS和tvOS而设计的对象图管理(object graph management)和数据持久化框架。如果你的App需要存储结构化的数据,那么Core Data是一个显而易见的方案:它是现成的,Apple仍然在积极地维护它,而且它已经存在超过10年了。
Core Data是一个成熟、经过实践检验的代码库。然而 Core Data 最初会让人有一些困惑:它非常灵活,但是 API 的最佳实践却并非显而易见。换句话说,本书的目标是帮助读者快速入门Core Data。我们希望提供给读者一系列包
括从简单到高级的使用场景中的最佳实践,这样你可以充分利用Core Data的能力而又不会迷失在一些不必要的复杂性中。
比如,Core Data经常被诟病难以在多线程环境中使用。其实Core Data的并发模型非常明确和一致。如果正确使用,那么它可以帮助你避免许多并发编程中一些固有的陷阱。其他的复杂性并不是由Core Data引入的,它们的根源其实是并发本身。我们会在第9章中对其进行深入研究,另外我们还会实际演示一个后台同步方案的例子。
除此之外,Core Data也经常被吐槽性能糟糕。如果你像使用关系型数据库那样来使用Core Data,那么你会发现与直接使用类似SQLite这样的数据库相比,Core Data的性能开销会很高。但如果把 Core Data 当成一个对象图管理系统来正确使用,那么得益于内建的缓存和对象管理机制,它在很多方面实际上反而更快。此外,抽象级别更高的API可以让你专注于优化App里关键部分的性能,而不是从头开始来实现如何持久化。在本书中,我们会介绍保持Core Data高性能的最佳实践,并在专门讲性能以及性能分析的章节中探讨如何解决Core Data的性能问题。
本书使用Core Data的方式本书展示了如何在实际例子中使用Core Data,而不仅仅是简单地对API手册进行一些扩展。我们有意专注于完整例子的最佳实践。根据我们的经验,正确地组合使用Core Data的各个部分往往是最大的挑战。
此外,本书还深入解释了Core Data内部的运作原理。了解Core Data这个灵活框架可以帮助你做出正确的决定,同时能让你的代码保持简单易懂。特别是当遇到并发和性能问题时,这一点尤为重要。
示例代码
你可以在GitHub上找到一个完整的示例程序的源代码。我们在本书中很多地方都将用这个示例程序来演示Core Data在较大的项目中面临的挑战和相应的解决方案。
请注意该示例程序代码有时会和本书前面的一些章节中的示例程序有所不同。因为示例项目是最终形态的完整的代码,而本书前面章节中描述的是该示例程序早期、简单阶段的代码。
结构
在本书的第一部分,我们会创建一个简单版本的应用程序,来演示如何使用Core Data以及Core Data的基本工作原理。即使早期的示例对读者来说可能相当容易,但我们仍然建议读者浏览本书的这些部分,因为后面更复杂的例子是建立在前面介绍的最佳实践和技术基础之上的。我们还想告诉你的是,即便在简单的应用场景中,Core Data也会非常有用。
第二部分则着重深入介绍Core Data各个部分是如何一起协作的。我们会仔细探讨当以不同方式访问数据时会发生什么,我们也会对插入或者操作数据时发生的情况进行研究。这部分所覆盖的内容会比写一个简单的Core Data应用程序所必要得多,这些方面的知识在处理更大或更复杂的情况时可以派上用场。在此基础上,我们将以性能方面的考量来对这个部分进行总结。
第三部分从描述一个用来保持本地数据与网络服务一致的通用同步架构开始,然后我们会深入探讨如何在Core Data中同时使用多个托管对象上下文(managed object context)。我们提出设置Core Data栈的不同方案,并讨论了它们的优缺点。在第9章里,介绍了如何应对同时使用多个上下文带来的额外复杂性。
第四部分涉及一些高级的主题,比如高级的谓词(predicate)、搜索和文本排序、如何在不同的数据模型版本之间迁移数据,以及分析Core Data栈的性能时所需要的工具和技术等。这部分中有一章是从Core Data视角介绍有关关系数据库和SQL查询语言的基本知识的。如果你不熟悉这些内容,那么这些章节能对你有所帮助,特别是可以让你理解Core Data潜在的性能问题,以及解决这些问题所需要的分析技术。
关于Swift的一些说明
贯穿本书,我们所有的示例都使用Swi编写。我们拥抱Swi的语言特性——比如泛型、协议以及扩展——它们能让我们更优雅、简单、安全地使用Core Data的API。
用Swi表示的最佳实践和设计模式同样也适用于Objective-C的代码。在实现上,由于语言上的不同,或许在某些方面会稍有不同,但是底层的原则是相通的。
可选值的约定
Swi提供了Optional数据类型,这迫使我们显式地思考和处理没有值的情况。我们非常喜欢这个功能,所以我们在所有的例子里都使用了它。
因此我们尽量避免使用 Swi 的 ! 操作符来强制解包 (包括用它来定义隐式解包类型的用法),在我们看来这是一种坏代码的味道,因为它破坏了我们使用可选值类型所带来的类型安全。
唯一的例外是那些必须设置但又无法在初始化时设置的属性。比如 Interface Builder 的outlets或必要的代理(delegate)属性等。在这些情况下,使用隐式解包的可选值符合“尽早崩溃”原则:我们会立刻知晓这些必须要设置而又没有正确设置的属性。
错误处理的约定
Core Data中许多方法会抛出错误。基于它们是不同类型的错误这一基本事实,我们可以分类处理这些错误。我们将区分逻辑错误和其他错误。
逻辑错误是指程序员犯错的结果。它们应该从代码层面上修复而不应该尝试动态恢复程序的运行。
举一个例子,当你尝试读取应用程序包里的一个文件时,因为应用程序包是只读的,那么一个文件要么存在,要么不存在,而且它的内容永远不会变。所以如果我们无法打开或者解析应用程序包里的文件,那么这就是一个逻辑错误。
对于这些类型的错误,我们使用 Swi 的 try! 或 fatalError() 来尽可能早地让应用程序崩溃。
同样的思想可以适用于 as! 操作符的强制类型转换: 如果我们知道一个对象必须是某种类型,转换失败的唯一原因会是逻辑错误,那么在这种时候我们实际上是希望应用程序崩溃的。
很多时候我们用 Swi 的 guard 关键字来更好地表达哪些地方出错了。例如 fetched resultscontroller返回的类型是NSManagedObject的对象,我们知道它必须是一个特定的子类,我们使用guard来保证向下转换,并在出错的时候使用fatal error来中止程序:
func objectAtIndexPath(indexPath: NSIndexPath) -> Object {
guard let result = fetchedResultsController.objectAtIndexPath(indexPath)
as? Object else
{
fatalError("Unexpected object at \(indexPath)")
}
return result
}
对于可恢复的非逻辑性错误,我们使用Swi的错误传递方法:抛出(throw)或者重新抛出(rethrow)这些错误。
——Florian Daniel