在不影响主线程的情况下保存NSManagedObjectContext

6

我想要做的事情:

  • 使用Web API进行后台同步而不冻结用户界面。我正在使用MagicalRecord,但它并不是特别针对此项任务。
  • 确保我正确地使用了上下文等内容

我的真正问题是:我的理解正确吗?最后还有几个问题。

因此,MagicalRecord提供的上下文包括:

  • MR_rootSavingContext,类型为PrivateQueueConcurrencyType,用于将数据保存到存储中,这是一个缓慢的过程
  • MR_defaultContext,类型为MainQueueConcurrencyType
  • 对于后台操作,您需要使用由MR_context()生成的上下文,它是MR_defaultContext的子级,类型为PrivateQueueConcurrencyType

现在,对于异步保存,我们有两个选项:

  • MR_saveToPersistentStoreWithCompletion():将保存所有到MR_rootSavingContext的内容并写入磁盘。
  • MR_saveOnlySelfWithCompletion():仅保存到父级上下文(即使用MR_context创建的上下文的MR_defaultContext)。

从那里开始,我认为我可以尝试以下操作(我们称之为Attempt#1),而不会冻结用户界面:

let context = NSManagedObjectContext.MR_context()
for i in 1...1_000 {
    let user = User.MR_createInContext(context) as User
    context.MR_saveOnlySelfWithCompletion(nil)
}
// I would normally call MR_saveOnlySelfWithCompletion here, but calling it inside the loop makes any UI block easier to spot

但是,我的假设是错误的。我查看了一下 MR_saveOnlySelfWithCompletion,并发现它依赖于

[self performBlock:saveBlock];

根据苹果文档,此方法在接收者队列上异步执行给定的块。

在接收者队列上异步执行给定的块。

所以我有点困惑,因为我期望它不会阻塞UI。

然后我尝试了(让我们称其为Attempt#2)

let context = NSManagedObjectContext.MR_context()
for i in 1...1_000 {
    let user = User.MR_createInContext(context) as User
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
        context.MR_saveOnlySelfWithCompletion(nil)
    }
}

这个方法可以完成任务,但感觉不太正确。

然后我在iOS 5.0的发布说明中发现了一些内容

当向与队列关联的上下文发送消息时,如果您的代码没有在该队列上执行(对于主队列类型)或者在performBlock... 调用范围内(对于私有队列类型),则必须使用performBlock: 或 performBlockAndWait: 方法。在传递给这些方法的块中,您可以自由地使用NSManagedObjectContext 的方法。

所以,我认为:

  • 尝试#1会冻结UI,因为我实际上是从主队列调用它,而不是在performBlock的范围内
  • 尝试#2虽然有效,但在上下文已经有自己的后台线程的情况下,我又创建了另一个线程

所以当然我应该使用saveWithBlock:

MagicalRecord.saveWithBlock { (localContext) -> Void in
    for i in 1...1_000 {
        User.MR_createInContext(context)
    }
}

这执行操作在MR_rootSavingContext的直接子级上,其类型为PrivateQueueConcurrencyType。由于rootContextChanged,任何传递到MR_rootSavingContext的更改都将对MR_defaultContext可用。
因此,似乎:
- MR_defaultContext是显示数据的完美上下文 - 最好使用MR_context(MR_defaultContext的子级)进行编辑 - 最好使用saveWithBlock执行长时间运行的任务,例如服务器同步
我仍然不明白如何使用MR_save[…]WithCompletion()。 我会在MR_context上使用它,但由于在我的测试用例中它阻塞了主线程,所以我不知道它何时变得相关(或者我错过了什么...)。
感谢您的时间 :)
1个回答

6

好的,我很少使用神奇记录,但是由于您说您的问题更普遍,我将尝试回答。

一些理论:创建上下文时,您需要传递一个指示器,以确定您是否希望将其绑定在主线程或后台线程上。

let context = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)

“Bound”指的是一个线程在上下文内部被引用。在上面的例子中,一个新的线程被创建并由上下文拥有。这个线程不会自动使用,必须显式地调用,如下所示:

context.performBlock({ () -> Void in
   context.save(nil)
   return
});

你使用 'dispatch_async' 的代码是错误的,因为上下文绑定的线程只能从上下文本身引用(它是一个私有线程)。

从上面可以得出的结论是,如果上下文绑定到主线程,从主线程调用 performBlock 不会与直接调用上下文方法有任何不同。

对于您在末尾的要点进行评论:

  • MR_defaultContext 是显示数据的完美上下文:NSManagedObject 必须从创建它的上下文访问,所以这实际上是您唯一可以从中提取 UI 的上下文。

  • 最好在 MR_context 中进行编辑(MR_defaultContext 的子节点):编辑不昂贵,您应该遵循上述规则。如果您从主线程调用编辑 NSManagedObject 属性的函数(例如点击按钮),则应更新主上下文。另一方面,保存操作是昂贵的,这就是为什么您的主上下文不应直接链接到持久存储,而只需将其编辑推送到具有后台并发性和拥有持久存储的根上下文中。

  • 长时间运行的任务,例如服务器同步,最好使用 saveWithBlock 进行:是的。

现在,在尝试 1 中

for i in 1...1_000 {
    let user = User.MR_createInContext(context) as User
}
context.MR_saveOnlySelfWithCompletion(nil)

不需要为每个对象创建保存。即使UI没有被阻塞,这也是浪费的。

关于MR_context。在神奇记录的文档中,我看不到'MR_context',所以我想知道它是否是访问主上下文的快速方法。如果是这样,它会被阻塞。


谢谢,回答很有趣。“没有必要为每个对象创建保存。”-确实,我这样做是为了确保在主线程上的任何阻塞都很容易被发现,我应该提到这一点。MR_saveOnlySelfWithCompletion在内部调用performBlock,这就是我不明白为什么UI会被阻塞的原因。 - Arnaud
performBlock并不保证它会在后台线程上执行,而是在上下文绑定的线程上执行。如果这是主要上下文,它将正常阻塞。基本上,认为在主线程上,在performBlock内外调用上下文方法是相同的!因此,请确保您的上下文不是主要的。 - Mike M
1
虽然您的回答很棒,但我不同意“没有必要为每个对象创建保存”。在大多数情况下,这可能是正确的,但如果您正在编辑的特定上下文需要频繁地将更改推送到其他上下文中,则可能需要保存。还有其他情况,您无法预见 :) - Tim
1
执行保存的问题在于它会阻塞上下文链(父子关系),直到保存完成。由于主要上下文可能在此上下文链中,这可能会导致阻塞。 - Mike M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接