Ios 在不同的线程上下文中,UndoManager运行循环分组将受到怎样的影响?
TLDR:我想知道当从后台线程使用时,Ios 在不同的线程上下文中,UndoManager运行循环分组将受到怎样的影响?,ios,swift,macos,grand-central-dispatch,runloop,Ios,Swift,Macos,Grand Central Dispatch,Runloop,TLDR:我想知道当从后台线程使用时,UndoManager基于运行循环的自动撤销分组是如何实现的,我的最佳选择是什么 我正在自定义Swift框架中使用UndoManager(以前称为NSUndoManager),目标是iOS和macOS 在该框架内,大量工作在后台GCD串行队列上进行。我知道UndoManager会在每个运行循环周期自动对顶级注册的撤销操作进行分组,但我不确定不同的线程情况会如何影响这一点 我的问题: 以下情况对UndoManagers已注册撤销操作的运行循环分组有什么影响
UndoManager
基于运行循环的自动撤销分组是如何实现的,我的最佳选择是什么
我正在自定义Swift框架中使用
UndoManager
(以前称为NSUndoManager
),目标是iOS和macOS
在该框架内,大量工作在后台GCD串行队列上进行。我知道UndoManager
会在每个运行循环周期自动对顶级注册的撤销操作进行分组,但我不确定不同的线程情况会如何影响这一点
我的问题:
- 以下情况对
s已注册撤销操作的运行循环分组有什么影响(如果有)UndoManager
- 假设所有需要撤销注册的更改都发生在单一的后台串行调度队列上,那么哪种情况(情况1除外,这是不可行的)最适合提供自然分组
methodCausingUndoRegistration()
和anotherMethodCausingUndoRegistration()
都不是什么稀奇古怪的东西,从调用它们的线程调用UndoManager.registerUndo
情况1:主线程上的内联
我的理解是:UndoManager
希望这样使用。上述两个撤消注册将在同一运行循环周期中进行,因此将被置于同一撤消组中
情形2:主线程上的同步调度
我的理解是:显然,我不想在生产中使用此代码,因为在大多数情况下,同步调度不是一个好主意。然而,我怀疑这两个动作有可能基于时间考虑被放入单独的运行循环周期中
情形3:主线程上的异步调度
我的理解是:尽管我非常希望这能产生与情形1相同的效果,但我怀疑这可能会导致与情形2类似的未定义分组
情况4:后台线程上的单个异步调度
我的理解是:只要
UndoManager
仅从同一后台队列中使用,我真的希望这与情况1相同。但是,我担心可能有一些因素导致分组未定义,特别是因为我不认为GCD队列(或其托管线程)总是(如果有的话)得到运行循环。TLDR:当从后台线程使用UndoManager
时,最简单的选项是通过groupsByEvent
禁用自动分组,然后手动执行。上述任何情况都不会按预期工作。如果你真的想在后台自动分组,你需要避免GCD
我将添加一些背景来解释期望,然后根据我在Xcode操场上做的实验,讨论每种情况下实际发生的情况 自动撤消分组 苹果指南中的“撤销管理器”一章指出: NSUndoManager通常在运行循环的一个周期中自动创建撤消组。第一次要求它记录循环中的撤消操作时,它将创建一个新组。然后,在循环结束时,它关闭组。可以创建其他嵌套的撤消组 通过在
通知中心注册为nsundomanagerdidopenndogroup
和nsundomanagerdicloseundogroup
的观察者,在项目或游乐场中很容易观察到这种行为。通过观察这些通知并将结果打印到控制台(包括undoManager.levelsOfUndo
),我们可以实时准确地看到分组的情况
指南还指出:
撤消管理器收集在运行循环(如应用程序的主事件循环)的单个周期内发生的所有撤消操作
此语言将指示主运行循环不是运行循环UndoManager
能够观察的唯一运行循环。因此,UndoManager
很可能会观察代表CFRunLoop
实例发送的通知,该实例在记录第一次撤消操作并打开组时是当前实例
GCD和Run循环
尽管苹果平台上运行循环的一般规则是“每个线程一个运行循环”,但也有例外。具体地说,一般认为,Grand Central Dispatch并不总是(如果有)使用标准的CFRunLoop
s及其调度队列或相关线程。事实上,唯一似乎有关联的CFRunLoop
的调度队列似乎是主队列
苹果公司的声明:
主调度队列是一个全局可用的串行队列,它在应用程序的主线程上执行任务。此队列与应用程序的运行循环(如果存在)一起工作,以将队列任务的执行与附加到运行循环的其他事件源的执行交错
主应用程序线程并不总是有运行循环(例如命令行工具),这是有道理的,但如果有,似乎可以保证GCD将与运行循环协调。此保证似乎不适用于其他调度队列,也不存在将任意调度队列(或其底层线程之一)与CFRunLoop
关联的任何公共API或文档化方式
使用以下代码可以观察到这一点:
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
由此,我们可以推导出全局和C
// 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
// 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
// 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.
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))
undoManager.groupsByEvent = false
backgroundSerialDispatchQueue.async {
undoManager.beginUndoGrouping() // <--
methodCausingUndoRegistration()
// Other code here
anotherMethodCausingUndoRegistration()
undoManager.endUndoGrouping() // <--
}
// 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()