mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-15 23:42:27 +08:00
fix: recover webview lazily and stop ws leak after render process ter… (#7410)
* fix: recover webview lazily and stop ws leak after render process termination macOS 内存压力下 WKWebView 渲染进程可能被系统杀掉,导致: 1. 从托盘打开主窗口时白屏; 2. 前端无法清理 Mihomo WebSocket 订阅,孤儿订阅持续将 >1KB 的 payload 写入 tauri ChannelDataIpcQueue 且无人取走,主进程内存 以 ~150MB/h 持续增长。 注册 on_web_content_process_terminate 钩子: - 立即清理全部 Mihomo WebSocket 订阅阻断泄漏(托盘速率任务会自动重连); - 窗口可见时立即 reload 拉起渲染进程恢复页面;窗口隐藏/最小化时仅置位 待重载标记,等用户下次打开窗口时再 reload——系统正是因内存压力才杀掉 不可见窗口的渲染进程,立即重建既浪费内存也可能形成反复被杀的循环。 另修复 tray_event 配置为 tray_menu 时误报 invalid tray event 警告。 * fix: gate logging import to macos to satisfy clippy on other platforms
This commit is contained in:
@@ -933,7 +933,10 @@ fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
||||
WindowManager::show_main_window().await;
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
// tray_menu 模式下菜单由系统原生展示(见 set_show_menu_on_left_click),
|
||||
// 左键点击事件无需额外处理
|
||||
TrayAction::TrayMenu => {}
|
||||
TrayAction::Unknown => {
|
||||
logging!(warn, Type::Tray, "invalid tray event: {}", verge_tray_event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -289,6 +289,12 @@ pub fn run() {
|
||||
})
|
||||
.invoke_handler(app_init::generate_handlers());
|
||||
|
||||
// macOS 内存压力下 WKWebView 渲染进程可能被系统终止(表现为白屏),
|
||||
// 注册恢复钩子:清理孤儿 WebSocket 订阅防止内存泄漏;窗口可见时立即 reload
|
||||
// 恢复页面,不可见时延迟到用户下次打开窗口再 reload。
|
||||
#[cfg(target_os = "macos")]
|
||||
let builder = builder.on_web_content_process_terminate(resolve::window::on_web_content_process_terminated);
|
||||
|
||||
mod event_handlers {
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::module::lightweight;
|
||||
|
||||
@@ -5,6 +5,9 @@ use tauri::{Theme, WebviewWindow};
|
||||
|
||||
use crate::{config::Config, core::handle, utils::resolve::window_script::build_window_initial_script};
|
||||
use clash_verge_logging::{Type, logging_error};
|
||||
// logging 仅在 macOS 的渲染进程恢复逻辑中使用
|
||||
#[cfg(target_os = "macos")]
|
||||
use clash_verge_logging::logging;
|
||||
|
||||
const DARK_BACKGROUND_COLOR: Color = Color(46, 48, 61, 255); // #2E303D
|
||||
const LIGHT_BACKGROUND_COLOR: Color = Color(245, 245, 245, 255); // #F5F5F5
|
||||
@@ -109,12 +112,90 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
Ok(window) => {
|
||||
logging_error!(Type::Window, window.set_background_color(Some(background_color)));
|
||||
restore_default_size_if_needed(&window);
|
||||
// 全新窗口的页面即为最新状态,丢弃旧窗口遗留的待重载标记,避免多余 reload
|
||||
#[cfg(target_os = "macos")]
|
||||
take_webview_needs_reload();
|
||||
Ok(window)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染进程死亡、页面待重载标记(macOS)
|
||||
///
|
||||
/// 渲染进程在窗口不可见时被系统终止后置位,由下次激活窗口的路径取走并执行 reload。
|
||||
#[cfg(target_os = "macos")]
|
||||
static WEBVIEW_NEEDS_RELOAD: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// 取出并清除"页面待重载"标记
|
||||
///
|
||||
/// # Returns
|
||||
/// * `bool` - 渲染进程是否曾在窗口不可见时被系统终止、页面需要 reload
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn take_webview_needs_reload() -> bool {
|
||||
WEBVIEW_NEEDS_RELOAD.swap(false, std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// WebView 渲染进程被系统终止后的恢复处理(macOS)
|
||||
///
|
||||
/// macOS 在内存压力下可能杀掉 WKWebView 的 WebContent 渲染进程:
|
||||
/// 1. 页面内容层消失,窗口打开后表现为白屏;
|
||||
/// 2. 前端 JS 状态随之丢失,无法调用 `ws_disconnect` 清理 Mihomo WebSocket 订阅,
|
||||
/// 孤儿订阅持续把大于 1KB 的 payload(如 `/connections` 全量快照)塞进 tauri 的
|
||||
/// `ChannelDataIpcQueue`,且没有存活的页面来取走,导致主进程内存无限增长。
|
||||
///
|
||||
/// 恢复策略:
|
||||
/// * 窗口可见(前台被杀的罕见场景)——立即 reload 恢复页面;
|
||||
/// * 窗口隐藏/最小化(托盘常驻的常见场景)——只置位待重载标记,等用户下次打开
|
||||
/// 窗口时再 reload。系统正是因内存压力才杀掉不可见窗口的渲染进程,此时立即
|
||||
/// 重建渲染进程既浪费内存,也可能形成"系统杀→拉起→再杀"的循环。
|
||||
///
|
||||
/// 注意:应用层注册 `on_web_content_process_terminate` 后会覆盖 tauri-runtime-wry
|
||||
/// 的默认自动 reload 行为,因此页面死亡状态会一直保持到我们主动 reload。
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `webview` - 渲染进程被终止的 WebView
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn on_web_content_process_terminated(webview: &tauri::Webview) {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
"WebView 渲染进程已被系统终止(label={}),开始恢复",
|
||||
webview.label()
|
||||
);
|
||||
|
||||
// 关键步骤:清理 Rust 侧所有 Mihomo WebSocket 订阅。
|
||||
// 旧页面遗留的孤儿订阅(traffic/memory/connections/logs)自此停止向已死亡的
|
||||
// IPC Channel 推送数据,阻断 ChannelDataIpcQueue 泄漏;
|
||||
// 托盘速率任务的订阅也会被一并关闭,但其内部循环会在约 1 秒后自动重连。
|
||||
crate::process::AsyncHandler::spawn(|| 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 连接");
|
||||
}
|
||||
});
|
||||
|
||||
// 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, "窗口不可见,页面将在下次打开窗口时重载");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::restored_window_size_is_too_small;
|
||||
|
||||
@@ -218,6 +218,16 @@ impl WindowManager {
|
||||
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
|
||||
logging!(info, Type::Window, "开始激活窗口");
|
||||
|
||||
// 关键步骤:渲染进程曾在窗口不可见时被系统终止(macOS 内存压力),
|
||||
// 页面当前是死亡的白屏状态,显示窗口前先 reload 重新拉起渲染进程。
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
let mut operations_successful = true;
|
||||
|
||||
// 1. 如果窗口最小化,先取消最小化
|
||||
|
||||
Reference in New Issue
Block a user