fix(mac): 点红点直接退出——真凶是模板 AppDelegate 的关窗即退开关

三层排查实锤(lldb 运行时取证):
1. 红点按钮真实 action 是 AppKit 私有 selector `_close:`(并非文档所称
   performClose:),内部直接关窗,绕过 performClose/close 重写与 delegate 拦截
2. flutter create 模板的 AppDelegate 自带
   applicationShouldTerminateAfterLastWindowClosed 返回 true——最后一个窗口
   关闭即 NSApp.terminate,这是"点红点进程退出、托盘随进程消失"的根因
3. 修复:改为返回 false。窗口即便被真关闭,进程/HTTP 服务/托盘也保持运行,
   托盘点击 makeKeyAndOrderFront 恢复窗口(releasedWhenClosed=NO,窗口对象在)

验证(红点 action 原样重放 `_close:`):窗口关闭后进程存活、服务在线、
托盘状态栏窗口存在、makeKeyAndOrderFront 可恢复窗口。
保留 MainFlutterWindow 的 performClose/close 重写(Cmd+W 等正常路径只隐藏
不关闭,体验更好)。
This commit is contained in:
dzc committed 2026-09-21 13:34:46 +08:00
1 parent fb06e629c2
commit a7cc69bb92
2 files changed
+20 -7

No files matched your search

+9 -1
View File
@@ -3,8 +3,16 @@ import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
/// 关窗不退出(托盘常驻的核心开关)。
///
/// 红点按钮的真实 action 是 AppKit 私有 `_close:`(实测 macOS 15,并非文档所说
/// 的 performClose:),会绕过 performClose/close 的重写把窗口真正关闭;窗口关闭后
/// AppKit 询问本方法——Flutter 模板默认返回 true(关窗即退出),这正是"点红点
/// 直接退出、托盘消失"的根因。返回 false 后:窗口关闭时进程/HTTP 服务/托盘保持
/// 运行,托盘点击 makeKeyAndOrderFront 恢复窗口(xib releasedWhenClosed=NO,
/// 窗口对象未释放)。真正退出只走托盘菜单 → NSApp.terminate / Cmd+Q。
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
return false
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+11 -6
View File
@@ -14,13 +14,18 @@ class MainFlutterWindow: NSWindow {
super.awakeFromNib()
}
/// 关窗不退出(双保险):红点按钮与 Cmd+W 的 target 都是 performClose:,
/// 这里直接 orderOut 隐藏窗口,进程/服务/托盘保持运行;
/// 真正退出只走托盘菜单 → window_manager.destroy() → NSApp.terminate(不经过此处)。
/// window_manager 的 setPreventClose/delegate 拦截在 xib 窗口上挂载时机不可靠,
/// 不能作为关窗拦截的唯一依赖(实测会被绕过导致整窗关闭)。
/// 关窗不退出(最终拦截点)。实测(macOS 15)红点按钮的真实 action 是 AppKit
/// 私有 selector `_close:`(并非文档所称的 performClose:),其内部直接调 close(),
/// 会绕过 performClose/windowShouldClose/delegate 拦截——因此必须在 close() 上拦。
/// 此处 orderOut 隐藏窗口,进程/HTTP 服务/托盘保持运行;NSApp.terminate 流程中
/// 的窗口关闭不受影响(进程照常结束),真正退出只走托盘菜单。
override func close() {
print("MainFlutterWindow: close() 拦截,窗口隐藏到托盘(服务继续运行)")
orderOut(nil)
}
/// Cmd+W / 菜单"关闭窗口"走 performClose:,同样只隐藏不关闭。
override func performClose(_ sender: Any?) {
print("MainFlutterWindow: performClose 拦截,窗口隐藏到托盘(服务继续运行)")
orderOut(sender)
}
}