如何正确杀死 NSWindow?
How to kill NSWindow properly?
最近,我发现 NSWindow
的惊人行为。无论是否存在对它的引用,它在屏幕上显示时都不会消失。
import Foundation
import AppKit
final class W1: NSWindow {
deinit {
print("W1.deinit")
}
}
print("start")
autoreleasepool {
var w1:W1? = W1()
w1?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w1?.orderFront(nil)
w1 = nil
}
print("finish")
RunLoop.main.run()
上面的代码打印 start
和 finish
但没有 W1.deinit
。
我在这些平台上进行了测试。
- Xcode 莫哈韦沙漠 10
- Xcode Catalina Beta 上的 11 GM(第一位)
并在两个平台上确认了相同的结果。
这是我的问题。
- 为什么
NSWindow
不死?
- 我应该如何管理
NSWindow
?
- 如何正确杀掉它?
由于对 NSWindow
的最后引用已被删除,它应该会立即消失。但事实并非如此。
如果它没有死,则意味着有另一个 "hidden" 引用它或 AppKit 在 windows 上有 "special" 行为。什么原因?
Window 如果我 close()
它在删除对它的最后一个引用之前就死了。但我不确定这是否真的是 proper/designed 杀死它的方法,因为它超出了 Cocoa/Swift 生命周期管理规则。
不违反Cocoa/Swift生命周期管理规则。生命周期管理规则始终与局部正确性有关。您确保代码的相关部分(单个 function/method、单个 class 等)做正确的事情和其他代码(在您自己的项目中、系统库中、第三方库中) , 等等) 被期望做正确的事情来满足它的需要。正确的事情通常包括维护一个强引用,可能超出您的代码保留一个的程度。
例如,Cocoa 可以 将显示的每个 window 放入屏幕 windows 的 Array
中,并且关闭时将其删除。 (我不知道它是否这样做,但它完全有效。)
无论如何,是的,调用close()
是正确的。 (还有其他方法,比如performClose()
,或者NSWindowController
的close()
方法。)
Why NSWindow don't die?
为什么会这样?你没有关闭它。
How am I supposed to manage NSWindow?
通过在您希望它关闭时关闭它,并在您不再关心它时释放您对它的任何强引用。这两个事件并不等同。保持强引用意味着"I care about this object."调用关闭意味着"I want this window to close."做你想做的事。
How to kill it properly?
相同。
我使用的是 macOS 10.15 Catalina Beta,所以这个问题可能只是这个 Beta 版本的问题。
只需调用 close()
就可以解决几个问题。
如果您的 NSWindow
实例尚未打开,调用 close()
会导致向释放对象发送 release
消息的异常。你可以用 Zombies-on 查看一下。
看这个例子。这没有问题。
final class TestWindow: NSWindow {
deinit { print("TestWindow.deinit!") }
}
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
// Okay.
TestWindow.deinit!
但是如果您还没有打开 window,这会导致 EXC_BAD_INSTRUCTION
。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
// w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
// An exception!
TestWindow.deinit!
2019-09-21 14:08:06.192106+0700 NSWindowLifetime1[73485:5304415] *** -[NSWindowLifetime1.TestWindow release]: message sent to deallocated instance 0x1005aafc0
调用setIsVisible(false)
或orderOut(nil)
不会释放window。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.orderOut(nil)
w?.setIsVisible(false)
w = nil
})
RunLoop.main.run()
// Window disppears but doesn't get released.
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
//w?.setIsVisible(true)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.setIsVisible(false)
w = nil
})
RunLoop.main.run()
// Works. No exception.
调用 close()
是目前我能找到的唯一可行的解决方案。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
// Okay.
TestWindow.deinit!
但如果 window 尚未打开,则会引发异常。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
//w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
TestWindow.deinit!
2019-09-21 14:26:06.410662+0700 NSWindowLifetime1[73801:5322002] *** -[NSWindowLifetime1.TestWindow release]: message sent to deallocated instance 0x1005c2c80
更有趣的是,您可以通过调用 setIsVisible(false)
将 window 设置为隐藏,但这不会使 window 一旦可见就被释放。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.setIsVisible(true)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.setIsVisible(false)
w = nil
})
RunLoop.main.run()
// No release.
由于它是不可见的,因此您不能通过可见性检查有条件地调用 close()
。
IMO 没有简单可靠的方法来安全地关闭 window。
- 如果你不关闭 window,它将被泄露直到应用进程结束。
- 如果您只是无条件地关闭 window,如果 window 不可见,它会抛出异常。
到目前为止,处理此问题的最佳方法是
- 总是打开 window 如果你曾经做过。
- 总是关闭它。
只是不要隐藏任何永远不会打开的 window。
如果您一开始必须将其隐藏,只需在 window 创建后立即调用 orderFront
和 orderOur
,使其在不显示的情况下订购一次。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
w?.orderOut(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
这可能是一个仅限于测试版的问题。我正在使用这些测试版。
- macOS 10.15 卡特琳娜测试版
- Xcode 11 通用 2
最近,我发现 NSWindow
的惊人行为。无论是否存在对它的引用,它在屏幕上显示时都不会消失。
import Foundation
import AppKit
final class W1: NSWindow {
deinit {
print("W1.deinit")
}
}
print("start")
autoreleasepool {
var w1:W1? = W1()
w1?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w1?.orderFront(nil)
w1 = nil
}
print("finish")
RunLoop.main.run()
上面的代码打印 start
和 finish
但没有 W1.deinit
。
我在这些平台上进行了测试。
- Xcode 莫哈韦沙漠 10
- Xcode Catalina Beta 上的 11 GM(第一位)
并在两个平台上确认了相同的结果。
这是我的问题。
- 为什么
NSWindow
不死? - 我应该如何管理
NSWindow
? - 如何正确杀掉它?
由于对 NSWindow
的最后引用已被删除,它应该会立即消失。但事实并非如此。
如果它没有死,则意味着有另一个 "hidden" 引用它或 AppKit 在 windows 上有 "special" 行为。什么原因?
Window 如果我 close()
它在删除对它的最后一个引用之前就死了。但我不确定这是否真的是 proper/designed 杀死它的方法,因为它超出了 Cocoa/Swift 生命周期管理规则。
不违反Cocoa/Swift生命周期管理规则。生命周期管理规则始终与局部正确性有关。您确保代码的相关部分(单个 function/method、单个 class 等)做正确的事情和其他代码(在您自己的项目中、系统库中、第三方库中) , 等等) 被期望做正确的事情来满足它的需要。正确的事情通常包括维护一个强引用,可能超出您的代码保留一个的程度。
例如,Cocoa 可以 将显示的每个 window 放入屏幕 windows 的 Array
中,并且关闭时将其删除。 (我不知道它是否这样做,但它完全有效。)
无论如何,是的,调用close()
是正确的。 (还有其他方法,比如performClose()
,或者NSWindowController
的close()
方法。)
Why NSWindow don't die?
为什么会这样?你没有关闭它。
How am I supposed to manage NSWindow?
通过在您希望它关闭时关闭它,并在您不再关心它时释放您对它的任何强引用。这两个事件并不等同。保持强引用意味着"I care about this object."调用关闭意味着"I want this window to close."做你想做的事。
How to kill it properly?
相同。
我使用的是 macOS 10.15 Catalina Beta,所以这个问题可能只是这个 Beta 版本的问题。
只需调用 close()
就可以解决几个问题。
如果您的 NSWindow
实例尚未打开,调用 close()
会导致向释放对象发送 release
消息的异常。你可以用 Zombies-on 查看一下。
看这个例子。这没有问题。
final class TestWindow: NSWindow {
deinit { print("TestWindow.deinit!") }
}
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
// Okay.
TestWindow.deinit!
但是如果您还没有打开 window,这会导致 EXC_BAD_INSTRUCTION
。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
// w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
// An exception!
TestWindow.deinit!
2019-09-21 14:08:06.192106+0700 NSWindowLifetime1[73485:5304415] *** -[NSWindowLifetime1.TestWindow release]: message sent to deallocated instance 0x1005aafc0
调用setIsVisible(false)
或orderOut(nil)
不会释放window。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.orderOut(nil)
w?.setIsVisible(false)
w = nil
})
RunLoop.main.run()
// Window disppears but doesn't get released.
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
//w?.setIsVisible(true)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.setIsVisible(false)
w = nil
})
RunLoop.main.run()
// Works. No exception.
调用 close()
是目前我能找到的唯一可行的解决方案。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
// Okay.
TestWindow.deinit!
但如果 window 尚未打开,则会引发异常。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
//w?.orderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
TestWindow.deinit!
2019-09-21 14:26:06.410662+0700 NSWindowLifetime1[73801:5322002] *** -[NSWindowLifetime1.TestWindow release]: message sent to deallocated instance 0x1005c2c80
更有趣的是,您可以通过调用 setIsVisible(false)
将 window 设置为隐藏,但这不会使 window 一旦可见就被释放。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.setIsVisible(true)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.setIsVisible(false)
w = nil
})
RunLoop.main.run()
// No release.
由于它是不可见的,因此您不能通过可见性检查有条件地调用 close()
。
IMO 没有简单可靠的方法来安全地关闭 window。
- 如果你不关闭 window,它将被泄露直到应用进程结束。
- 如果您只是无条件地关闭 window,如果 window 不可见,它会抛出异常。
到目前为止,处理此问题的最佳方法是
- 总是打开 window 如果你曾经做过。
- 总是关闭它。
只是不要隐藏任何永远不会打开的 window。
如果您一开始必须将其隐藏,只需在 window 创建后立即调用 orderFront
和 orderOur
,使其在不显示的情况下订购一次。
var w: NSWindow? = TestWindow()
w?.setFrame(NSRect(x: 0, y: 0, width: 100, height: 100), display: true)
w?.orderFront(nil)
w?.orderOut(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
w?.close()
w = nil
})
RunLoop.main.run()
这可能是一个仅限于测试版的问题。我正在使用这些测试版。
- macOS 10.15 卡特琳娜测试版
- Xcode 11 通用 2