gpt4 book ai didi

ios - UndoManager运行循环分组将如何在不同的线程上下文中受到影响?

转载 作者:行者123 更新时间:2023-12-03 14:13:57 25 4
gpt4 key购买 nike

TLDR:我想知道从后台线程使用时如何影响基于运行循环的UndoManager自动撤消分组,而我的最佳选择是什么。

我在具有iOS和macOS目标的自定义Swift框架中使用UndoManager(以前称为NSUndoManager)。

在该框架内,后台GCD串行队列上进行了大量工作。我知道UndoManager在每个运行循环周期自动将顶级已注册的撤消操作分组,但是我不确定不同的线程情况将如何影响这种情况。

我的问题:

  • 以下情况会对UndoManager的已注册撤消操作的运行循环分组产生什么影响(如果有)?
  • 假设需要撤消注册的所有更改都将在单个后台串行分派队列中进行,哪种情况(除了情况1,这是不可行的)最适合提供自然分组?

  • 在以下所有情况下,假定 methodCausingUndoRegistration()anotherMethodCausingUndoRegistration()都不花哨,并从调用它们的线程中调用 UndoManager.registerUndo而不进行任何调度。

    情况1:在主线程上内联
    // Assume this runs on main thread
    methodCausingUndoRegistration()
    // Other code here
    anotherMethodCausingUndoRegistration()
    // Also assume every other undo registration in this framework takes place inline on the main thread

    我的理解:期望使用 UndoManager。上面的两个撤消注册都将在相同的运行循环周期中进行,因此将被放置在同一撤消组中。

    情况2:主线程上的同步调度
    // Assume this runs on an arbitrary background thread, possibly managed by GCD.
    // It is guaranteed not to run on the main thread to prevent deadlock.
    DispatchQueue.main.sync {
    methodCausingUndoRegistration()
    }
    // Other code here
    DispatchQueue.main.sync {
    anotherMethodCausingUndoRegistration()
    }

    // Also assume every other undo registration in this framework takes place
    // by syncing on main thread first as above

    我的理解:显然,我不想在生产中使用此代码,因为在大多数情况下,同步调度并不是一个好主意。但是,我怀疑这两种动作是否有可能基于时序考虑放在单独的运行循环周期中。

    情况3:主线程上的异步调度
    // Assume this runs from an unknown context. Might be the main thread, might not.
    DispatchQueue.main.async {
    methodCausingUndoRegistration()
    }
    // Other code here
    DispatchQueue.main.async {
    anotherMethodCausingUndoRegistration()
    }

    // Also assume every other undo registration in this framework takes place
    // by asyncing on the main thread first as above

    我的理解:我想尽可能地产生与情况1相同的效果,我怀疑这可能导致与情况2类似的不确定的分组。

    情况4:后台线程上的单个异步调度
    // Assume this runs from an unknown context. Might be the main thread, might not.
    backgroundSerialDispatchQueue.async {
    methodCausingUndoRegistration()
    // Other code here
    anotherMethodCausingUndoRegistration()
    }

    // Also assume all other undo registrations take place
    // via async on this same queue, and that undo operations
    // that ought to be grouped together would be registered
    // within the same async block.

    我的理解:我真的希望这与情况1一样,只要 UndoManager仅在同一后台队列中使用即可。但是,我担心可能会有一些因素使分组未定义,特别是因为我认为GCD队列(或其托管线程)并不总是(如果有的话)会得到运行循环。

    最佳答案

    TLDR:从后台线程使用UndoManager时,最简单的选择是简单地禁用通过groupsByEvent进行自动分组并手动进行。上述情况均无法按预期工作。如果您确实要在后台进行自动分组,则需要避免使用GCD。

    我将添加一些背景来解释期望,然后根据我在Xcode Playground中所做的实验,讨论每种情况下实际发生的情况。

    自动撤消分组

    Apple的Cocoa Application Competencies for iOS指南的“撤消管理器”一章指出:

    NSUndoManager通常在运行循环的周期内自动创建撤消组。第一次要求在周期中记录撤消操作时,它将创建一个新组。然后,在循环结束时,它将关闭组。您可以创建其他嵌套的撤消组。

    通过将自己注册为NotificationCenterNSUndoManagerDidOpenUndoGroup的观察者NSUndoManagerDidCloseUndoGroup,可以轻松在项目或Playground中观察到此行为。通过观察这些通知并将结果打印到包括undoManager.levelsOfUndo的控制台,我们可以实时准确地看到分组的情况。

    该指南还指出:

    撤消管理器收集在运行循环(例如应用程序的主事件循环)的单个周期内发生的所有撤消操作。

    这种语言将表明UndoManager是唯一可以观察到的主运行循环。因此,很可能UndoManager观察到代表CFRunLoop实例发送的通知,该实例在记录第一个撤消操作并打开组时是当前的。

    GCD和运行循环

    即使Apple平台上运行循环的一般规则是“每个线程一个运行循环”,但该规则也有例外。具体来说,通常公认的是,Grand Central Dispatch不会总是(如果有的话)将标准CFRunLoop与它的调度队列或其关联线程一起使用。实际上,似乎唯一具有相关联的CFRunLoop的调度队列似乎是主队列。

    苹果的Concurrency Programming Guide指出:

    主调度队列是全局可用的串行队列,可在应用程序的主线程上执行任务。此队列与应用程序的运行循环(如果存在)一起工作,以使排队任务的执行与附加到运行循环的其他事件源的执行交织在一起。

    主应用程序线程不一定总会有一个运行循环(例如命令行工具)是有道理的,但是如果有,似乎可以保证GCD将与运行循环协调。对于其他调度队列似乎不存在这种保证,并且似乎没有任何 public API或文档记录的方式将任意调度队列(或其基础线程之一)与CFRunLoop关联。

    通过使用以下代码可以观察到这一点:

    DispatchQueue.main.async {
    print("Main", RunLoop.current.currentMode)
    }

    DispatchQueue.global().async {
    print("Global", RunLoop.current.currentMode)
    }

    DispatchQueue(label: "").async {
    print("Custom", RunLoop.current.currentMode)
    }

    // Outputs:
    // Custom nil
    // Global nil
    // Main Optional(__C.RunLoopMode(_rawValue: kCFRunLoopDefaultMode))
    RunLoop.currentMode的文档指出:

    此方法仅在接收器运行时返回当前输入模式。否则,返回nil。

    由此,我们可以推断出全局和自定义调度队列并不总是(如果有的话)拥有自己的 CFRunLoop(这是 RunLoop背后的基础机制)。因此,除非我们要调度到主队列,否则 UndoManager将没有 Activity 的 RunLoop可供观察。这对于情况4及以后的情况非常重要。

    现在,让我们使用上面讨论过的Playground(带有 PlaygroundPage.current.needsIndefiniteExecution = true)和通知观察机制来观察每种情况。

    情况1:在主线程上内联

    这正是 UndoManager期望使用的方式(基于文档)。观察撤消通知显示,正在创建一个撤消组,其中包含两个撤消。

    情况2:主线程上的同步调度

    在使用这种情况的简单测试中,我们将每个撤销注册归入其各自的组。因此,我们可以得出结论,这两个同步分派的块分别在各自的运行循环周期中发生。这似乎总是在主队列上发生调度同步产生的行为。

    情况3:主线程上的异步调度

    但是,当改为使用 async时,一个简单的测试揭示了与情况1相同的行为。似乎是因为两个块都在有机会被run循环实际运行之前被分派到了主线程,所以run循环同时执行同一周期中的块。因此,两个撤消注册都位于同一组中。

    纯粹基于观察,这似乎在 syncasync中引入了细微的差别。因为 sync会阻塞当前线程直到完成,所以运行循环必须在返回之前开始(和结束)一个循环。当然,然后,运行循环将无法在同一周期中运行另一个块,因为在运行循环开始并查找消息时,它们不会在那儿。但是,对于 async,直到两个块都已排队后,运行循环才可能发生,因为 async在工作完成之前就返回了。

    基于此观察,我们可以通过在两个 sleep(1)调用之间插入 async调用来模拟情况3内的情况2。这样,运行循环就有机会在发送第二个块之前开始其循环。实际上,这将导致创建两个撤消组。

    情况4:后台线程上的单个异步调度

    这就是事情变得有趣的地方。假设 backgroundSerialDispatchQueue是GCD自定义串行队列,则在第一次撤销注册之前立即创建一个撤销组,但是永远不会关闭它。如果我们考虑上面关于GCD和运行循环的讨论,这是有道理的。仅仅由于我们调用了 registerUndo而创建了撤消组,所以还没有顶级组。但是,它从未关闭过,因为它从未收到有关运行循环结束其循环的通知。它从未收到该通知,因为后台GCD队列未获得与之关联的功能性 CFRunLoop,因此 UndoManager很可能甚至从未能够观察到运行循环。

    正确的方法

    如果有必要从后台线程使用 UndoManager,则以上两种情况都不是理想的(第一种情况除外,这不满足在后台触发的要求)。似乎有两个选择可行。两者都假定 UndoManager将仅在相同的后台队列/线程中使用。毕竟, UndoManager不是线程安全的。

    只是不使用自动分组

    这种基于运行循环的自动撤消分组可以通过 undoManager.groupsByEvent轻松关闭。然后可以像这样实现手动分组:
    undoManager.groupsByEvent = false

    backgroundSerialDispatchQueue.async {
    undoManager.beginUndoGrouping() // <--
    methodCausingUndoRegistration()
    // Other code here
    anotherMethodCausingUndoRegistration()
    undoManager.endUndoGrouping() // <--
    }

    这完全符合预期,将两个注册都放在同一组中。

    使用基金会而不是GCD

    在生产代码中,我打算简单地关闭自动撤消分组功能并手动进行操作,但是在调查 UndoManager的行为时确实找到了一种替代方法。

    我们先前发现 UndoManager无法观察自定义GCD队列,因为它们似乎没有关联的 CFRunLoop。但是,如果我们创建了自己的 Thread并设置了相应的 RunLoop,该怎么办。从理论上讲,这应该起作用,并且下面的代码演示:
    // Subclass NSObject so we can use performSelector to send a block to the thread
    class Worker: NSObject {

    let backgroundThread: Thread

    let undoManager: UndoManager

    override init() {
    self.undoManager = UndoManager()

    // Create a Thread to run a block
    self.backgroundThread = Thread {
    // We need to attach the run loop to at least one source so it has a reason to run.
    // This is just a dummy Mach Port
    NSMachPort().schedule(in: RunLoop.current, forMode: .commonModes) // Should be added for common or default mode
    // This will keep our thread running because this call won't return
    RunLoop.current.run()
    }

    super.init()
    // Start the thread running
    backgroundThread.start()
    // Observe undo groups
    registerForNotifications()
    }

    func registerForNotifications() {
    NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidOpenUndoGroup, object: undoManager, queue: nil) { _ in
    print("opening group at level \(self.undoManager.levelsOfUndo)")
    }

    NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidCloseUndoGroup, object: undoManager, queue: nil) { _ in
    print("closing group at level \(self.undoManager.levelsOfUndo)")
    }
    }

    func doWorkInBackground() {
    perform(#selector(Worker.doWork), on: backgroundThread, with: nil, waitUntilDone: false)
    }

    // This function needs to be visible to the Objc runtime
    @objc func doWork() {
    registerUndo()

    print("working on other things...")
    sleep(1)
    print("working on other things...")
    print("working on other things...")

    registerUndo()
    }

    func registerUndo() {
    let target = Target()
    print("registering undo")
    undoManager.registerUndo(withTarget: target) { _ in }
    }

    class Target {}
    }

    let worker = Worker()
    worker.doWorkInBackground()

    如预期的那样,输出指示两个撤消都放置在同一组中。 UndoManager之所以能够观察到周期,是因为与GCD不同, Thread使用的是 RunLoop

    不过,老实说,坚持使用GCD并使用手动撤消分组可能更容易。

    关于ios - UndoManager运行循环分组将如何在不同的线程上下文中受到影响?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47988403/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com