如果您正在开发小型或简单的应用程序,则可能看不到在后台运行Core Data操作的好处。 但是,如果在第一次启动应用程序时在主线程上导入数百或数千条记录,将会发生什么情况? 结果可能是戏剧性的。 例如,您的应用程序可能由于启动时间太长而被Apple的监视程序杀死。
在本文中,我们研究了在多个线程上使用Core Data时的危险,并探索了解决该问题的几种解决方案。
1.线程安全
使用Core Data时,请务必记住Core Data不是线程安全的,这一点很重要。 Core Data期望在单个线程上运行。 这并不意味着每个Core Data操作都需要在主线程上执行,这对UIKit而言是正确的,但这确实意味着您需要注意在哪些线程上执行了哪些操作。 这也意味着您需要注意如何将一个线程的更改传播到其他线程。
从理论上讲,在多个线程上使用Core Data实际上非常简单。 NSManagedObject
, NSManagedObjectContext
和NSPersistentStoreCoordinator
不是线程安全的。 这些类的实例只能从创建它们的线程中访问。 您可以想象,这在实践中会变得更加复杂。
NSManagedObject
我们已经知道NSManagedObject
不是线程安全的,但是如何从不同的线程访问记录? NSManagedObject
实例具有objectID
属性,该属性返回NSManagedObjectID
类的实例。 NSManagedObjectID
类是线程安全的,该类的实例包含托管对象上下文获取对应的托管对象所需的所有信息。
// Object ID Managed Object
let objectID = managedObject.objectID
在以下代码片段中,我们向与objectID
对应的托管对象询问托管对象上下文。 objectWithID(_:)
和existingObjectWithID(_:)
方法返回相应托管对象的本地版本(对于当前线程而言本地)。
// Fetch Managed Object
let managedObject = managedObjectContext.objectWithID(objectID)
// OR
do {
let managedObject = try managedObjectContext.existingObjectWithID(objectID)
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
要记住的基本规则是不要将NSManagedObject
实例从一个线程传递到另一个线程。 而是传递托管对象的objectID
并向线程的托管对象上下文询问记录的本地版本。
NSManagedObjectContext
由于NSManagedObjectContext
类不是线程安全的,因此我们可以为与Core Data交互的每个线程创建一个托管对象上下文。 这种策略通常称为线程限制 。
一种常见的方法是将托管对象上下文存储在线程的字典中,该字典用于存储特定于线程的数据。 看下面的例子,看看它是如何工作的。
// Add Object to Thread Dictionary
let currentThread = NSThread.currentThread()
currentThread.threadDictionary.setObject(managedObjectContext, forKey: "managedObjectContext")
不久前,苹果公司推荐了这种方法。 即使工作正常,Apple现在推荐的还有另一个更好的选择。 我们将在稍后讨论这个选项。
NSPersistentStoreCoordinator
持久存储协调员呢? 您是否需要为每个线程创建一个单独的持久性存储协调器。 尽管这是可能的,也是Apple过去推荐的策略之一,但这不是必需的。
NSPersistentStoreCoordinator
类旨在支持多个托管对象上下文,即使这些托管对象上下文是在不同线程上创建的也是如此。 由于NSManagedObjectContext
类在访问持久性存储协调器时会对其进行锁定,因此即使这些托管对象上下文位于不同的线程中,多个托管对象上下文也可能使用相同的持久性存储协调器。 这使得多线程Core Data设置更易于管理,也更简单。
2.并发策略
到目前为止,我们已经了解到,如果您在多个线程上执行Core Data操作,则需要多个托管对象上下文。 但是,需要注意的是,托管对象上下文不知道彼此的存在。 在一个托管对象上下文中对托管对象所做的更改不会自动传播到其他托管对象上下文。 我们如何解决这个问题?
苹果公司推荐两种流行的策略,即通知和父子托管对象上下文。 让我们看看每种策略,并研究其优缺点。
我们将以NSOperation
子类为例,该子类在后台执行工作并在操作的后台线程上访问Core Data。 本示例将向您展示每种策略的差异和优势。
策略1:通知
在本系列的早期,我向您介绍了NSFetchedResultsController
类,并且您了解到托管对象上下文会发布三种类型的通知:
-
NSManagedObjectContextObjectsDidChangeNotification
:当托管对象上下文的一个托管对象已更改时,将发布此通知。 -
NSManagedObjectContextWillSaveNotification
:在托管对象上下文执行保存操作之前,将发布此通知。 -
NSManagedObjectContextDidSaveNotification
:在托管对象上下文执行保存操作后,将发布此通知。
当托管对象上下文通过持久性存储协调器将其更改保存到持久性存储时,其他托管对象上下文可能希望了解这些更改。 这很容易做到,甚至更容易将更改包含或合并到另一个托管对象上下文中。 让我们谈谈代码。
我们创建了一个非并行操作,该操作在后台执行某些工作并且需要访问Core Data。 这就是NSOperation
子类的实现的样子。
import UIKit
import CoreData
class Operation: NSOperation {
let mainManagedObjectContext: NSManagedObjectContext
var privateManagedObjectContext: NSManagedObjectContext!
init(managedObjectContext: NSManagedObjectContext) {
mainManagedObjectContext = managedObjectContext
super.init()
}
override func main() {
// Initialize Managed Object Context
privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
// Configure Managed Object Context
privateManagedObjectContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator
// Add Observer
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "managedObjectContextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: privateManagedObjectContext)
// Do Some Work
// ...
if privateManagedObjectContext.hasChanges {
do {
try privateManagedObjectContext.save()
} catch {
// Error Handling
// ...
}
}
}
}
有一些重要的细节需要澄清。 我们初始化私有管理对象上下文,并使用mainManagedObjectContext
对象设置其持久性存储协调器属性。 这非常好,因为我们不访问mainManagedObjectContext
,我们只要求它引用应用程序的持久性存储协调器。 我们不违反线程限制规则。
必须在操作的main()
方法中初始化私有管理对象上下文,因为此方法在运行该操作的后台线程上执行。 我们不能在操作的init(managedObjectContext:)
方法中初始化托管对象上下文吗? 答案是不。 该操作的init(managedObjectContext:)
方法在初始化Operation
实例的线程上运行,该线程很可能是主线程。 这将破坏私有管理对象上下文的目的。
在操作的main()
方法中,我们将Operation
实例添加为私有托管对象上下文发布的任何NSManagedObjectContextDidSaveNotification
通知的观察者。
然后,我们执行为操作创建操作并保存私有管理对象上下文的更改,这将触发NSManagedObjectContextDidSaveNotification
通知。 让我们看一下managedObjectContextDidSave(_:)
方法中发生的情况。
// MARK: -
// MARK: Notification Handling
func managedObjectContextDidSave(notification: NSNotification) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.mainManagedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
}
}
如您所见,它的实现是简短而简单的。 我们在主管理对象上下文上调用mergeChangesFromContextDidSaveNotification(_:)
,传入通知对象。 如前所述,通知包含发布通知的托管对象上下文的更改,插入,更新和删除。
在创建主管理对象上下文的线程(主线程)上调用此方法很关键。 这就是为什么我们将此调用分派到主线程队列的原因。 为了使此操作更容易且更透明,可以使用performBlock(_:)
或performBlockAndWait(_:)
来确保合并更改发生在托管对象上下文的队列中。 我们将在本文后面详细讨论这些方法。
// MARK: -
// MARK: Notification Handling
func managedObjectContextDidSave(notification: NSNotification) {
mainManagedObjectContext.performBlock { () -> Void in
self.mainManagedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
}
}
使用Operation
类就像初始化实例,传递托管对象上下文以及将操作添加到操作队列一样简单。
// Initialize Import Operation
let operation = Operation(managedObjectContext: managedObjectContext)
// Add to Operation Queue
operationQueue.addOperation(operation)
策略2:父/子受管对象上下文
从iOS 6开始,有一个更好,更优雅的策略。 让我们重新访问Operation
类并利用父/子管理对象上下文。 父/子托管对象上下文背后的概念很简单但功能强大。 让我解释一下它是如何工作的。
子托管对象上下文依赖于其父托管对象上下文来将其更改保存到相应的持久性存储中。 实际上,子托管对象上下文无法访问持久性存储协调器。 每当保存子托管对象上下文时,其包含的更改都会推送到父托管对象上下文。 无需使用通知将更改手动合并到主或父托管对象上下文中。
另一个好处是性能。 由于子托管对象上下文无权访问持久性存储协调器,因此在保存子托管对象上下文时,更改不会推送到后者。 而是将更改推送到父托管对象上下文,使其变脏。 所做的更改不会自动传播到持久性存储协调器。
托管对象上下文可以嵌套。 子托管对象上下文可以具有自己的子托管对象上下文。 相同的规则适用。 但是,请务必记住,推送到父托管对象上下文的更改不会推送到任何其他子托管对象上下文。 如果子项A将其更改推送给其父项,则子项B不知道这些更改。
创建子托管对象上下文与到目前为止所看到的仅稍有不同。 我们通过调用init(concurrencyType:)
初始化一个子托管对象上下文。 初始化程序接受的并发类型定义了托管对象上下文的线程模型。 让我们看一下每种并发类型。
-
MainQueueConcurrencyType
:只能从主线程访问托管对象上下文。 如果尝试从任何其他线程访问它,则会引发异常。 -
PrivateQueueConcurrencyType
:当创建并发类型为PrivateQueueConcurrencyType
的托管对象上下文时,托管对象上下文与私有队列相关联,并且只能从该私有队列中访问它。
-
ConfinementConcurrencyType
:这是与我们之前探讨的线程限制概念相对应的并发类型。 如果使用init()
创建托管对象上下文,则该托管对象上下文的并发类型为ConfinementConcurrencyType
。 Apple从iOS 9开始不赞成使用这种并发类型。这也意味着从iOS 9开始不建议使用init()
。
当Apple引入父/子托管对象上下文时,Core Data框架中添加了两个关键方法, performBlock(_:)
和performBlockAndWait(_:)
。 两种方法都会使您的生活更加轻松。 当您在托管对象上下文上调用performBlock(_:)
并传入要执行的代码块时,Core Data确保该块在正确的线程上执行。 对于PrivateQueueConcurrencyType
并发类型,这意味着将在该受管对象上下文的专用队列上执行该块。
performBlock(_:)
和performBlockAndWait(_:)
之间的区别很简单。 performBlock(_:)
方法不会阻止当前线程。 它接受该块,安排它在正确的队列上执行,然后继续执行下一条语句。
但是, performBlockAndWait(_:)
方法正在阻止。 从其调用performBlockAndWait(_:)
的线程在执行下一条语句之前,等待传递给该方法的块完成。 优点是对performBlockAndWait(_:)
嵌套调用是按顺序执行的。
结束本文,我想重构Operation
类以利用父/子管理对象上下文。 您会很快注意到,它大大简化了我们创建的NSOperation
子类。 main()
方法的变化很大。 在下面查看其更新的实现。
override func main() {
// Initialize Managed Object Context
privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
// Configure Managed Object Context
privateManagedObjectContext.parentContext = mainManagedObjectContext
// Do Some Work
// ...
if privateManagedObjectContext.hasChanges {
do {
try privateManagedObjectContext.save()
} catch {
// Error Handling
// ...
}
}
}
而已。 主管理对象上下文是专用管理对象上下文的父级。 请注意,我们没有设置私有管理对象上下文的persistentStoreCoordinator
属性,也没有将操作添加为NSManagedObjectContextDidSaveNotification
通知的观察者。 保存私有管理对象上下文后,更改将自动推送到其父管理对象上下文。 核心数据可确保此操作在正确的线程上进行。 由主托管对象上下文(父托管对象上下文)决定将更改推送到持久性存储协调器。
结论
并发并不容易掌握或实现,但是天真地认为您永远不会遇到需要在后台线程上执行Core Data操作的情况。
翻译自: https://code.tutsplus.com/tutorials/core-data-and-swift-concurrency--cms-25118