From 4fa2f9a329b08fde6d29ace9895b8e816c631682 Mon Sep 17 00:00:00 2001 From: wonfen Date: Sat, 4 Jul 2026 09:39:44 +0800 Subject: [PATCH] fix(macos): recover webview and stop ws leak after render process terminate --- Changelog.md | 2 + src-tauri/src/lib.rs | 5 +++ src-tauri/src/utils/resolve/window.rs | 53 ++++++++++++++++++--------- src-tauri/src/utils/window_manager.rs | 31 +++++++++------- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/Changelog.md b/Changelog.md index 4096e9c4d..08fde77d8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,7 @@ - 修复部分非标准 WebDAV 服务器在备份目录已存在时的问题 - 修复 Linux 应用内更新问题 - 修复 JS 脚本验证因 console 方法调用导致的执行失败问题 +- 修复 macOS 内存压力下 WebView 渲染进程被系统终止引发的白屏与主进程内存泄漏
✨ 新增功能 @@ -33,5 +34,6 @@ - 关闭 autofill 弹出窗口 - 改进切换订阅后激活选中节点的逻辑 - 实现代理组粘性滚动列表 +- 完善了配置覆写的相关逻辑
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ed3fc7f7d..94658b80e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -458,6 +458,11 @@ pub fn run() { event_handlers::handle_window_close(&event); } tauri::WindowEvent::Focused(focused) => { + // 兜底:原生取消最小化只触发 Focused、不走 activate_window(macOS) + #[cfg(target_os = "macos")] + if focused { + crate::utils::resolve::window::reload_main_window_if_needed(); + } event_handlers::handle_window_focus(focused); } #[cfg(target_os = "macos")] diff --git a/src-tauri/src/utils/resolve/window.rs b/src-tauri/src/utils/resolve/window.rs index 3917ef13a..340f02287 100644 --- a/src-tauri/src/utils/resolve/window.rs +++ b/src-tauri/src/utils/resolve/window.rs @@ -168,31 +168,48 @@ pub fn on_web_content_process_terminated(webview: &tauri::Webview) { webview.label() ); - // 关键步骤:清理 Rust 侧所有 Mihomo WebSocket 订阅。 - // 旧页面遗留的孤儿订阅(traffic/memory/connections/logs)自此停止向已死亡的 - // IPC Channel 推送数据,阻断 ChannelDataIpcQueue 泄漏; - // 托盘速率任务的订阅也会被一并关闭,但其内部循环会在约 1 秒后自动重连。 - crate::process::AsyncHandler::spawn(|| async move { + let window = webview.window(); + let is_user_visible = window.is_visible().unwrap_or(false) && !window.is_minimized().unwrap_or(false); + + // 懒重载标记仅供主窗口;其它 webview(update-splash)无消费通道,不可见时直接 reload 兜底 + let is_main_window = webview.label() == "main"; + let reload_now = is_user_visible || !is_main_window; + + if !reload_now { + // 主窗口不可见:置标记,延迟到下次 activate_window / reload_main_window_if_needed 再 reload + WEBVIEW_NEEDS_RELOAD.store(true, std::sync::atomic::Ordering::SeqCst); + logging!(info, Type::Window, "窗口不可见,页面将在下次打开窗口时重载"); + } + + // 清理全部 Mihomo WS 订阅,阻断 ChannelDataIpcQueue 泄漏(托盘速率任务约 1s 后自重连)。 + // reload 必须与清理同任务、排在其后,否则清理可能误清重载后新页面的订阅(竞态)。 + let webview = webview.clone(); + crate::process::AsyncHandler::spawn(move || async move { if let Err(err) = handle::Handle::mihomo().await.clear_all_ws_connections().await { logging!(warn, Type::Window, "清理 Mihomo WebSocket 连接失败: {err}"); } else { logging!(info, Type::Window, "已清理全部 Mihomo WebSocket 连接"); } + if reload_now { + logging_error!(Type::Window, webview.reload()); + } }); +} - // is_user_visible:窗口是否处于用户可感知的可见状态(未隐藏且未最小化) - let window = webview.window(); - let is_user_visible = window.is_visible().unwrap_or(false) && !window.is_minimized().unwrap_or(false); - - if is_user_visible { - // 关键步骤:窗口正被用户看着,立即 reload。加载请求会让 WKWebView - // 自动重新拉起渲染进程,恢复页面内容(解决白屏)。 - logging_error!(Type::Window, webview.reload()); - } else { - // 关键步骤:窗口不可见,延迟到下次激活窗口时再 reload(见 - // WindowManager::activate_window),避免在内存压力下重建无人观看的页面。 - WEBVIEW_NEEDS_RELOAD.store(true, std::sync::atomic::Ordering::SeqCst); - logging!(info, Type::Window, "窗口不可见,页面将在下次打开窗口时重载"); +/// 消费待重载标记并 reload 主窗口(macOS)。 +/// 兜底原生取消最小化(Dock 缩略图/调度中心/窗口菜单)只触发 Focused(true)、不走 +/// activate_window 的情况。与 activate_window 共用 swap 标记,先取先得、不重复 reload。 +#[cfg(target_os = "macos")] +pub fn reload_main_window_if_needed() { + if !take_webview_needs_reload() { + return; + } + let Some(window) = crate::utils::window_manager::WindowManager::get_main_window() else { + return; + }; + logging!(info, Type::Window, "渲染进程曾被系统终止,窗口聚焦后重载页面"); + if let Err(e) = window.reload() { + logging!(warn, Type::Window, "重载页面失败: {e}"); } } diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index 21d7eee7e..f3fd72162 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -218,13 +218,16 @@ impl WindowManager { fn activate_window(window: &WebviewWindow) -> WindowOperationResult { logging!(info, Type::Window, "开始激活窗口"); - // 关键步骤:渲染进程曾在窗口不可见时被系统终止(macOS 内存压力), - // 页面当前是死亡的白屏状态,显示窗口前先 reload 重新拉起渲染进程。 + // 渲染进程曾被系统终止:先 reload,并把 show+focus 交给 on_page_load(Finished), + // 内容就绪再显示,避免白屏闪烁。reload 成功才 defer,失败则走下方直接显示。 + #[allow(unused_mut)] + let mut defer_show_to_page_load = false; #[cfg(target_os = "macos")] if crate::utils::resolve::window::take_webview_needs_reload() { logging!(info, Type::Window, "渲染进程曾被系统终止,激活窗口前重载页面"); - if let Err(e) = window.reload() { - logging!(warn, Type::Window, "重载页面失败: {}", e); + match window.reload() { + Ok(()) => defer_show_to_page_load = true, + Err(e) => logging!(warn, Type::Window, "重载页面失败,退回直接显示: {}", e), } } @@ -239,16 +242,16 @@ impl WindowManager { } } - // 2. 显示窗口 - if let Err(e) = window.show() { - logging!(warn, Type::Window, "显示窗口失败: {}", e); - operations_successful = false; - } - - // 3. 设置焦点 - if let Err(e) = window.set_focus() { - logging!(warn, Type::Window, "设置窗口焦点失败: {}", e); - operations_successful = false; + // 2/3. 显示 + 焦点(reload 分支跳过,交给 on_page_load) + if !defer_show_to_page_load { + if let Err(e) = window.show() { + logging!(warn, Type::Window, "显示窗口失败: {}", e); + operations_successful = false; + } + if let Err(e) = window.set_focus() { + logging!(warn, Type::Window, "设置窗口焦点失败: {}", e); + operations_successful = false; + } } // 4. 平台特定的激活策略