Core Data是Apple为iOS、OS X、watchOS和tvOS而设计的对象图管理(object graph man-agement)和数据持久化框架。如果你的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的一些说明
贯穿本书,我们所有的示例都使用Swift编写。我们拥抱Swift的语言特性——比如泛型、协议以及扩展——它们能让我们更优雅、简单、安全地使用Core Data的API。
用Swift表示的最佳实践和设计模式同样也适用于Objective-C的代码。在实现上,由于语言上的不同,或许在某些方面会稍有不同,但是底层的原则是相通的。
可选值的约定
Swift提供了Optional数据类型,这迫使我们显式地思考和处理没有值的情况。我们非常喜欢这个功能,所以我们在所有的例子里都使用了它。
因此我们尽量避免使用Swift的! 操作符来强制解包(包括用它来定义隐式解包类型的用法),在我们看来这是一种坏代码的味道,因为它破坏了我们使用可选值类型所带来的类型安全。
唯一的例外是那些必须设置但又无法在初始化时设置的属性。比如Interface Builder的outlets或必要的代理(delegate)属性等。在这些情况下,使用隐式解包的可选值符合“尽早崩溃”原则:我们会立刻知晓这些必须要设置而又没有正确设置的属性。
错误处理的约定
Core Data中许多方法会抛出错误。基于它们是不同类型的错误这一基本事实,我们可以分类处理这些错误。我们将区分逻辑错误和其他错误。
逻辑错误是指程序员犯错的结果。它们应该从代码层面上修复而不应该尝试动态恢复程序的运行。
举一个例子,当你尝试读取应用程序包里的一个文件时,因为应用程序包是只读的,那么一个文件要么存在,要么不存在,而且它的内容永远不会变。所以如果我们无法打开或者解析应用程序包里的文件,那么这就是一个逻辑错误。
对于这些类型的错误,我们使用Swift的try! 或fatalError()来尽可能早地让应用程序崩溃。
同样的思想可以适用于as! 操作符的强制类型转换:如果我们知道一个对象必须是某种类型,转换失败的唯一原因会是逻辑错误,那么在这种时候我们实际上是希望应用程序崩溃的。
很多时候我们用Swift的guard关键字来更好地表达哪些地方出错了。例如fetched results controller返回的类型是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
}
对于可恢复的非逻辑性错误,我们使用Swift的错误传递方法:抛出(throw)或者重新抛出(rethrow)这些错误。
Florian
Daniel