encodeRestorableState(with:backgroundQueue:) 应该同步编码,还是使用提供的后台队列?

移动开发 2026-07-07

我在使用AppKit的状态还原(state restoration),并试图理解其预期的用法:

override func encodeRestorableState(with coder: NSCoder, backgroundQueue queue: OperationQueue)

我看到的大多数示例直接在方法体内进行编码:

override func encodeRestorableState(with coder: NSCoder, backgroundQueue queue: OperationQueue) {
    super.encodeRestorableState(with: coder, backgroundQueue: queue)

    coder.encode(value1, forKey: "value1")
    coder.encode(value2, forKey: "value2")
}

然而,我最近遇到了一段使用提供的 OperationQueue 的代码:

override func encodeRestorableState(with coder: NSCoder, backgroundQueue queue: OperationQueue) {
    super.encodeRestorableState(with: coder, backgroundQueue: queue)

    queue.addOperation {
        coder.encode(value1, forKey: "value1")
    }

    queue.addOperation {
        coder.encode(value2, forKey: "value2")
    }
}

另一种可能的版本是在一个单独的操作中完成所有编码:

override func encodeRestorableState(with coder: NSCoder, backgroundQueue queue: OperationQueue) {
    super.encodeRestorableState(with: coder, backgroundQueue: queue)

    queue.addOperation {
        coder.encode(value1, forKey: "value1")
        coder.encode(value2, forKey: "value2")
    }
}

苹果的文档指出,接收方可以同步编码状态,或使用提供的串行后台队列将异步工作排队。这让我不太确定哪种风格更可取。

我的问题是:

  1. 何时应直接在方法体中进行编码,何时应使用 queue.addOperation(_:)
  2. 如果我使用提供的队列,是否有理由将编码拆分为多个操作,而不是添加一个对所有相关值进行编码的单一操作?
  3. 背景队列的用途是否主要是为了避免在主线程上进行昂贵的状态收集或编码工作?
  4. 如果被编码的值来自AppKit/UI状态,是否应该先在主线程捕获这些值,然后再在队列上进行编码?
  5. 当向同一个 NSCoder 编码的多个操作被加入时,是否存在任何排序方面的问题?
  6. 应该在我的编码之前调用 super.encodeRestorableState(with:backgroundQueue:),还是之后,还是只要调用即可?

我在努力理解预期的模式,而不仅仅是让代码能编译。

解决方案

你已经发现的文档要点(关键部分):

This method is part of the window restoration system and is called at appropriate times to save the visual state of your responder to the specified archive.

...

AppKit calls this method on your app’s main thread. If you want to encode any state information asynchronously on a background thread, submit one or more operation objects to the provided queue. Performing long-running operations asynchronously lets you free up the main thread for other operations more quickly. The encoding operation is not considered final until all operations submitted to queue finish.

由于此方法是在对整个窗口状态进行编码的一部分时在主线程上调用,因此你在该方法中同步执行的任何工作都会继续占用该线程,阻碍后续状态编码的进行。如果窗口需要为还原编码大量复杂的状态,这最终可能会使在此过程中UI保持响应性变得困难。

然后:

  1. 背景队列的目的是否主要是为了避免在主线程上进行昂贵的状态收集或编码工作?

正是这样:如果你需要编码的状态成本高、耗时,那么同步完成会使线程被阻塞。将这部分工作排队到后台线程执行,可以让你更早地释放主线程去处理UI工作。

  1. 何时应直接在方法体内进行编码,何时应使用 queue.addOperation(_:)

没有严格的规则来界定何时编码成本足以值得将其移至后台线程,但是一般而言:对于“便宜”的、快速的、可直接获取的状态,直接同步编码可以避免将工作排队所需的记录工作;如果你需要对用于编码的值进行非平凡的计算,那么很可能应该放在后台完成。

通常,关注性能会是你的朋友。

  1. 如果被编码的值来自AppKit/UI状态,是否应该先在主线程捕获这些值,然后再在队列上进行编码?

是的:

  1. 由于后台工作何时执行没有保证,到时候UI状态可能已经改变;
  2. 通常在主线程之外访问UI状态是不安全的。
  1. 如果我使用提供的队列,是否有理由将编码拆分为多个操作,而不是添加一个对所有相关值进行编码的单一操作?
  2. 是否在向同一个NSCoder编码的多个操作中存在排序问题?

后台队列被描述为一个串行队列:

A serial background operation queue on which to encode additional state asynchronously.

因此,在为编码排队多个操作时不应存在任何排序问题——但也不会带来性能提升(例如,工作不能并发执行)。

状态还原无论如何都要等到所有操作完成,因此无论你编码一个操作还是十个,最终都会执行。将工作拆分成多个操作的主要潜在好处在于让代码保持整洁:如果你需要编码的状态涉及大量不同的工作,将这些工作拆分到不同的块中可能会让代码更整洁。

  1. 应该在我的编码之前调用 super.encodeRestorableState(with:backgroundQueue:),还是之后,还是只要调用即可?

通常,这也适用于常规的带键归档 encode(with:) 调用,但:使用同一个键的多次 encode 调用会把编码的值覆盖为最后提供的一个。这意味着如果你的类及其超类都尝试使用同一个键,先调用 super.encode... 将确保你有机会覆盖超类的值,反之亦然。

由于该操作在同步性方面混合使用,存在一些细微的差别:如果你调用 super.encodeRestorableState(with:backgroundQueue:),且超类异步编码了一个值;而你的类对同一键进行同步编码,超类的调用几乎肯定会被重新排序至在你的类之后,从而导致超类的值获得胜出。这是一个相当小众的场景,通常只有在你有意覆盖超类的键时才会遇到,在这种情况下你要确保先调用 super.encode...,然后再异步编码该值(以确保如果有超类操作,它会在这些操作之后被调度)。


简要结论:对于较小、简单的状态,应同步编码以避免捕获状态和排队异步操作的开销;对于较大、复杂的状态,应异步编码以尽早释放主线程。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章