代码创建的NSWindow关闭时崩溃的原因及相关问题咨询
我正在学习NSWindow的正确生命周期及处理方式,使用默认macOS应用模板(包含Window Controller管理的ViewController),同时在applicationDidFinishLaunching中通过代码创建窗口:
let dummyWindow = CustomWindow(contentRect: .init(origin: .zero, size: .init(width: 200, height: 100)), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: true) dummyWindow.title = "Code window" dummyWindow.makeKeyAndOrderFront(nil)
CustomWindow类定义:
class CustomWindow: NSWindow { deinit { print("Deinitializing window...") } }
关闭该代码创建的窗口时,应用会触发EXC_BAD_ACCESS崩溃,即使没有主动访问该窗口。排除ARC问题的两个依据:
applicationDidFinishLaunching局部作用域结束后,窗口仍被NSApplication.shared.windows强引用"Deinitializing window..."仅在窗口关闭后才打印
对比Interface Builder(IB)创建的窗口:关闭不会崩溃,修改isReleasedWhenClosed属性对其无影响;但将代码创建的窗口isReleasedWhenClosed设为false可阻止崩溃。
由此产生三个疑问:
- 若不是我的代码,那是什么在窗口关闭后访问它,导致因NSWindow默认释放行为而崩溃?
- Interface Builder创建的窗口与Window Controller管理的窗口在底层有何区别,从而避免崩溃?
- 若代码创建窗口的推荐做法是始终设置
isReleasedWhenClosed = false,那么如何正确释放该窗口以避免内存泄漏?
问题解答
1. 崩溃原因:AppKit内部的延迟访问
当isReleasedWhenClosed为默认值true时,窗口关闭后会立即被ARC释放,但AppKit内部可能存在尚未完成的异步操作(比如窗口关闭动画的收尾逻辑、事件队列中的残留回调),这些操作会尝试访问已释放的窗口对象,从而触发EXC_BAD_ACCESS野指针崩溃。你看到deinit打印在崩溃前,说明窗口确实已经被销毁,后续AppKit的内部代码仍在引用这块已回收的内存地址。
2. IB窗口与代码创建窗口的核心区别:Window Controller的持有管理
IB创建的窗口默认由NSWindowController持有管理,这是两者的核心差异:
- Window Controller会强引用其
window属性,即使窗口关闭,只要Window Controller未被释放,窗口对象就不会被销毁 - Window Controller会接管窗口的生命周期逻辑,包括处理关闭后的资源清理,避免AppKit内部出现野指针访问
- 此外,IB创建窗口时,AppKit会自动设置内部关联标记(绑定到Window Controller),使其关闭时的释放逻辑被改写,不受
isReleasedWhenClosed属性的直接影响
3. 代码创建窗口的正确释放方式
如果设置isReleasedWhenClosed = false,需要手动管理窗口生命周期以避免内存泄漏,推荐两种可靠方案:
方案一:用Window Controller托管
代码创建窗口时,将其关联到NSWindowController,由Window Controller持有窗口引用。当窗口不再需要时,释放Window Controller即可自动回收窗口:
func applicationDidFinishLaunching(_ aNotification: Notification) { let dummyWindow = CustomWindow(contentRect: .init(origin: .zero, size: .init(width: 200, height: 100)), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: true) dummyWindow.title = "Code window" dummyWindow.isReleasedWhenClosed = false let windowController = NSWindowController(window: dummyWindow) windowController.showWindow(nil) // 若需长期持有,可将windowController存储为类属性;无需时置为nil即可释放窗口 }
方案二:监听关闭事件手动释放
给窗口添加NSWindow.willCloseNotification监听,在窗口关闭后将自身持有的窗口引用置为nil:
class AppDelegate: NSObject, NSApplicationDelegate { var dummyWindow: CustomWindow? func applicationDidFinishLaunching(_ aNotification: Notification) { let window = CustomWindow(contentRect: .init(origin: .zero, size: .init(width: 200, height: 100)), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: true) window.title = "Code window" window.isReleasedWhenClosed = false window.makeKeyAndOrderFront(nil) dummyWindow = window NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose(_:)), name: NSWindow.willCloseNotification, object: window) } @objc func windowWillClose(_ notification: Notification) { dummyWindow = nil NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: notification.object) } }
这种方式需确保你持有窗口的唯一强引用,置为nil后ARC会自动回收窗口对象。
内容的提问来源于stack exchange,提问作者Vojta Böhm

