swift 尽管有@MainActor注解,但未在主线程上触发UI更新

yqhsw0fo  于 2023-04-28  发布在  Swift
关注(0)|答案(1)|浏览(254)

我用@MainActor注解我的函数,以确保它可以从任何异步位置安全地调用,以触发UI更新。尽管如此,我遇到了一个错误,不知何故,UI更新似乎是在后台线程上尝试的,即使(据我理解)该函数严格绑定到@MainActor
这是我的代码:

/// Dismisses the popup from its presenting view controller.
@MainActor public func dismiss() {
    presentingViewController?.dismiss(self)
}

它是从NSViewController内部调用的,NotificationCenter监听某个事件,然后在下面的objc函数中启动解除:

class MainWindowControllerVC: NSWindowController, NSWindowDelegate {
  override func windowDidLoad() {
      NotificationCenter.default.addObserver(self, selector: #selector(self.dismissVCastSharePopup), name: .NOTIF_DISMISS_POPUP, object: nil)
  }

  @objc private func dismissPopup() {
      // some other cleanup is happening here
      popup?.dismiss()
  }
}

我得到以下错误:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'

以及以下警告:

有人能解释一下这是怎么可能的吗?我误会什么了?如果我将相同的代码 Package 成DispatchQueue.main.async甚至Task { @MainActor () -> Void in ... },我不会得到这个错误。它特别地与函数之前的注解绑定。

atmip9wb

atmip9wb1#

博士
当你将一个函数隔离到@MainActor时,只有当你从Swift并发上下文调用这个方法时,这才是相关的。如果你从Swift并发系统外部调用它(比如从NotificationCenter),@MainActor限定符没有任何作用。
因此,要么从Swift并发上下文调用这个actor隔离函数,要么依赖于遗留的main队列模式。
有很多方法可以解决这个问题。首先,让我将您的示例重构为MCVE:

import Cocoa
import os.log

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ViewController")

extension Notification.Name {
    static let demo = Notification.Name(rawValue: Bundle.main.bundleIdentifier! + ".demo")
}

class ViewController: NSViewController {
    deinit {
        NotificationCenter.default.removeObserver(self, name: .demo, object: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        addObserver()
        postFromBackground()
    }

    func addObserver() {
        logger.debug(#function)

        NotificationCenter.default.addObserver(self, selector: #selector(notificationHandler(_:)), name: .demo, object: nil)
    }

    func postFromBackground() {
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            logger.debug(#function)
            NotificationCenter.default.post(name: .demo, object: nil)
        }
    }

    @objc func notificationHandler(_ notification: Notification) {
        checkQueue()
    }

    @MainActor func checkQueue() {
        logger.debug(#function)
        dispatchPrecondition(condition: .onQueue(.main))
        logger.debug("OK")
    }
}

有几种方法可以解决这个问题:
1.我的notificationHandler(在你的例子中是dismissVCastSharePopup)不在Swift并发上下文中。但我可以通过将调用 Package 在Task {…}中来桥接Swift并发:

@objc nonisolated func notificationHandler(_ notification: Notification) {
    Task { await checkQueue() }
}

注意,我不仅将调用 Package 在Task {…}中,还添加了nonisolated限定符,以让编译器知道这是从非隔离上下文调用的。(我认为它应该从@objc限定符中推断出来,但目前还没有。)
1.或者,你可以回到传统模式来确保你在主线程上,比如基于块的addObserver,它允许你指定.main队列:

private var observerToken: NSObjectProtocol?

func addObserver() {
    observerToken = notificationCenter.addObserver(forName: .demo, object: nil, queue: .main) { [weak self] _ in
        self?.checkQueue()
    }

    …
}

也许不用说,即使我们在后面的例子中不再使用@objc选择器方法,这个闭包也不会在actor隔离的上下文中被调用,因此它将忽略@MainActor属性。上面的模式之所以能够工作,完全是因为我遵循了遗留模式,并显式地指定了.main队列来处理这个观察者。

相关问题