Recently, I discovered surprising behavior of NSWindow
. It doesn't die while it is displayed on screen regardless of existence of reference to it.
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()
Code above prints start
and finish
but no W1.deinit
.
I tested this on these platforms.
And confirmed same result on both platforms.
Here are my questions.
NSWindow
don't die?NSWindow
?As last reference to NSWindow
has been removed, it is supposed to die immediately. But it doesn't.
If it do not die, it means there's another "hidden" reference to it or AppKit have "special" behavior on windows. What's the reason?
Window dies if I close()
it before removing last reference to it. But I am not sure whether this is really proper/designed way to kill it as it's out of Cocoa/Swift lifetime management rules.
I am using macOS 10.15 Catalina Beta so this issue could be only the problem of this beta release.
Just calling close()
can make several issues.
If your NSWindow
instance haven't been open, calling close()
causes an exception of sending release
message to deallocated object. You can check it with Zombies-on.
See this example. This works with no problem.
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!
But if you haven't open your window, this causes an 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
Calling setIsVisible(false)
or orderOut(nil)
doesn't let window to be released.
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.
Calling close()
is the only working solution so far I could find.
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!
But this throws an exception if the window hasn't been open.
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
More interestingly, you can set window hidden by calling setIsVisible(false)
, but this doesn't make window get released if it once became visible.
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.
And as it's been invisible, you cannot call close()
conditionally by visibility check.
IMO there's no simple and reliable way to close window safely.
So far the best way to deal with this would be
Just don't make any hidden window that never be open.
If you have to keep it hidden at first just call orderFront
and orderOur
immediately right after window creation to make it ordered once without presenting.
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()
This could be a problem limited to beta release. I am using these beta releases.