From a7cc69bb924bfe1396f9d492a2ae647967200db5 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Mon, 21 Sep 2026 13:34:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(mac):=20=E7=82=B9=E7=BA=A2=E7=82=B9?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E9=80=80=E5=87=BA=E2=80=94=E2=80=94=E7=9C=9F?= =?UTF-8?q?=E5=87=B6=E6=98=AF=E6=A8=A1=E6=9D=BF=20AppDelegate=20=E7=9A=84?= =?UTF-8?q?=E5=85=B3=E7=AA=97=E5=8D=B3=E9=80=80=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三层排查实锤(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 等正常路径只隐藏 不关闭,体验更好)。 --- macos/Runner/AppDelegate.swift | 10 +++++++++- macos/Runner/MainFlutterWindow.swift | 17 +++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index b3c1761..237e66c 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -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 { diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 2af11c4..9b4728b 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -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) } }