Mat*_*man 2 macos grand-central-dispatch runloop ios swift
TLDR:我想知道UndoManager从后台线程使用时如何基于运行循环自动撤消分组,而我的最佳选择是什么。
我使用UndoManager(以前NSUndoManager在与iOS和MacOS的目标定制的斯威夫特框架)。
在框架中,工作体面量呈现背景GCD串行队列的地方。我了解UndoManager每个运行循环周期会自动将顶级注册的撤消操作分组,但是我不确定不同的线程情况将如何影响这种情况。
我的问题:
UndoManager的run环路注册撤消操作的分组?在下面的所有情况下,假定methodCausingUndoRegistration()并且anotherMethodCausingUndoRegistration()没有幻想,可以UndoManager.registerUndo在没有任何分派的情况下从调用它们的线程进行调用。
// 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
Run Code Online (Sandbox Code Playgroud)
我的理解:这就是UndoManager预期的用法。上面的两个撤消注册都将在同一运行循环周期中进行,因此将被放置在同一撤消组中。
// 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
Run Code Online (Sandbox Code Playgroud)
我的理解:显然,我不想在生产中使用此代码,因为在大多数情况下,同步调度并不是一个好主意。但是,我怀疑这两种操作是否有可能基于时序考虑放在单独的运行循环周期中。
// 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
Run Code Online (Sandbox Code Playgroud)
我的理解:尽管我希望这能产生与情况1相同的效果,但我怀疑这可能导致与情况2类似的未定义分组。
// 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.
Run Code Online (Sandbox Code Playgroud)
我的理解:我真的希望这将与情况1相同,只要UndoManager专门用于同一后台队列即可。但是,我担心可能会有一些因素使分组未定义,特别是因为我认为GCD队列(或其托管线程)并不总是(如果有的话)会得到运行循环。
TLDR:当工作UndoManager从后台线程,复杂度最低的选项是通过简单地禁用自动分组groupsByEvent和做手工。上述情况均无法按预期工作。如果您确实想在后台进行自动分组,则需要避免使用GCD。
我将添加一些背景来解释期望,然后根据我在Xcode Playground中所做的实验,讨论每种情况下实际发生的情况。
Apple的iOS版Cocoa应用程序功能的“撤消管理器”一章指出:
NSUndoManager通常在运行循环的周期内自动创建撤消组。第一次要求在循环中记录撤消操作时,它将创建一个新组。然后,在循环结束时,它关闭组。您可以创建其他嵌套的撤消组。
此行为是在一个项目或者游乐场与注册我们自己容易观察到NotificationCenter作为观察员NSUndoManagerDidOpenUndoGroup和NSUndoManagerDidCloseUndoGroup。通过观察这些通知并将结果打印到控制台(包括)undoManager.levelsOfUndo,我们可以实时准确地查看分组的情况。
该指南还指出:
撤消管理器收集在运行循环(例如应用程序的主事件循环)的单个周期内发生的所有撤消操作。
这种语言将表明主运行循环不是唯一UndoManager可以观察到的运行循环。然后,最有可能UndoManager观察到代表CFRunLoop第一个撤消操作被记录并打开组时当前实例发送的通知。
即使Apple平台上运行循环的一般规则是“每个线程一个运行循环”,但该规则也有例外。具体而言,通常公认的是,Grand Central Dispatch不会总是(如果有的话)将标准CFRunLoops与它的调度队列或其关联线程一起使用。实际上,唯一具有关联的调度队列CFRunLoop似乎是主队列。
Apple的并发编程指南指出:
主调度队列是全局可用的串行队列,可在应用程序的主线程上执行任务。该队列与应用程序的运行循环(如果存在)一起工作,以使排队任务的执行与附加到运行循环的其他事件源的执行交织在一起。
可以理解的是,主应用程序线程不一定总是具有运行循环(例如命令行工具),但是如果有,似乎可以保证GCD将与运行循环协调。对于其他调度队列似乎不存在这种保证,并且似乎没有任何公共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))
Run Code Online (Sandbox Code Playgroud)
RunLoop.currentMode状态文档:
此方法仅在接收器运行时返回当前输入模式。否则,返回nil。
由此,我们可以推断出全局和自定义调度队列并非总是(如果有的话)拥有自己的队列CFRunLoop(这是背后的基本机制RunLoop)。因此,除非我们要分派到主队列,UndoManager否则就不会主动RunLoop观察。这对于情况4及以后的情况非常重要。
现在,让我们使用PlaygroundPage.current.needsIndefiniteExecution = true上面讨论过的Playground(带有)和通知观察机制来观察每种情况。
这正是UndoManager期望使用的方式(基于文档)。观察撤消通知会显示一个内部有两个撤消的撤消组。
在使用这种情况的简单测试中,我们将每个撤销注册归入其各自的组。因此,我们可以得出结论,这两个同步分派的块分别在各自的运行循环周期中发生。这似乎始终是在主队列上产生调度同步的行为。
但是,当async使用代替时,一个简单的测试将显示与情况1相同的行为。似乎是因为两个块都在有机会被运行循环实际运行之前被分派到了主线程,所以运行循环执行了两个块在同一周期内。因此,两个撤消注册都放在同一组中。
纯粹基于观察,这似乎在sync和中引入了细微的差异async。因为sync阻塞当前线程直到完成,所以运行循环必须在返回之前开始(和结束)一个循环。当然,然后,运行循环将无法在同一周期中运行其他块,因为在运行循环开始并查找消息时,它们不会在那儿。async但是,使用时,运行循环可能直到两个块都已排队后才开始发生,因为async在完成工作之前就返回了。
基于此观察,我们可以通过sleep(1)在两个async调用之间插入一个调用来模拟情况3中的情况2 。这样,运行循环就有机会在发送第二个块之前开始其循环。实际上,这会导致创建两个撤消组。
这就是事情变得有趣的地方。假设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() // <--
}
Run Code Online (Sandbox Code Playgroud)
这完全符合预期,将两个注册都放在同一组中。
在生产代码中,我打算简单地关闭自动撤消分组功能并手动进行操作,但是在调查的行为时确实找到了一种替代方法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()
Run Code Online (Sandbox Code Playgroud)
如预期的那样,输出指示两个撤消都放置在同一组中。UndoManager之所以能够观察到周期,Thread是因为RunLoopG与GCD不同。
不过,老实说,坚持使用GCD并使用手动撤消分组可能更容易。
| 归档时间: |
|
| 查看次数: |
228 次 |
| 最近记录: |