Electron 鸿蒙 PC 上关了子窗口,主窗口也卡死不动——我翻了 Chromium 源码才找到一行注释
我做的 App 叫雷达鸭鸿蒙桌面版用的是 Electron。上个月给应用加了个设置弹窗——就是个简单的BrowserWindow点了按钮弹出来改配置改完关掉完事。逻辑上简单得不能再简单。结果关了弹窗之后主窗口整个卡死了。不是进程崩了DevTools 还能连上console 也没报错。但就是点什么都没反应——按钮点不了、列表滚不动、键盘快捷键全部哑火。刷新一下 electron 进程就好了。这问题复现率还不低关个三四次弹窗必中一次。我当时第一反应是内存泄漏。打开鸿蒙的 DevEco Profiler 看了半天内存曲线平稳得跟死了一样。然后怀疑是渲染进程的事件循环挂了——给webContents执行了一个console.log(new Date())打印正常。renderer 进程的 JS 引擎根本没挂。那就是主进程的问题了。我仔细看了一圈注意到设置弹窗创建时传了一个parent参数// 主进程 main.tsconstsettingsWinnewBrowserWindow({width:480,height:600,parent:mainWindow,// 就这行后面会说到为什么modal:false,webPreferences:{preload:path.join(__dirname,preload.js),contextIsolation:true,},});settingsWin.loadFile(settings.html);这个parent参数在 Windows 和 macOS 上是个好东西——子窗口永远浮在父窗口上方不用自己管理 z-order体验很自然。但在鸿蒙 PC 上它触发了一个埋得很深的坑。我把parent: mainWindow删了问题马上消失。但我不想删——弹窗老掉到底下去被主窗口盖住体验太拉胯了。来回切窗口找设置弹窗用户能忍我可忍不了。于是换了个思路在子窗口关闭前主动解除父子关系。// 防御性修复关闭前先解绑 parentsettingsWin.on(close,(){if(process.platformlinux){settingsWin.setParentWindow(null);}});就一行setParentWindow(null)。问题消失了。弹窗关掉主窗口一切正常点什么都有反应。但你真觉得这样就踏实了不。我遇到过太多看起来修好了但其实只是运气好的情况。这问题复现率不是 100%——关三四次才中一次——说明背后有个竞态条件或者非确定性逻辑。你永远不知道什么边缘场景会让它再次触发。为了确认这不是我本地环境的偶然问题我在三台不同的鸿蒙 PC 设备上跑了同一个测试脚本反复打开弹窗、关闭、检查主窗口是否响应。三台设备都出现了冻结而且有个很有意思的规律——如果弹窗打开超过 30 秒再关闭冻结概率从约 30% 飙升到接近 70%。弹窗存活越久越容易触发问题。这说明不是简单的子窗口关闭漏了通知——它跟窗口的生命周期时序有关。说到这你可能觉得工作已经做完了——加一行 workaround代码能跑收工。但我这人有个毛病遇到这种平台专供 bug非得搞清楚下面到底发生了什么。加 workaround 但不理解原理等于背了个你不知道什么时候会炸的定时炸弹。我决定翻 Chromium 源码。Electron 在 Linux / 鸿蒙平台上跑的是 Chromium 的 Ozone/Mus 窗口系统。父窗口卡死的本质是事件分发器被标记成了modal_blocked_状态。正常情况下子窗口销毁时应该通知父窗口一声我不再模态阻塞你了父窗口清理标志位事件循环恢复正常。在 Chromium 的通用代码里这个逻辑是这样的// chromium/src/ui/aura/window_tree_host.cc简化后voidWindowTreeHost::OnWindowDestroying(Window*window){if(window-parent()){window-parent()-OnModalStateChanged(false);}}逻辑很直白子窗口挂了 → 通知父窗口解除模态状态 → 父窗口恢复事件分发。但鸿蒙的WindowTreeHostMus覆写了这个方法。鸿蒙的窗口合成器WindowManagerService自己管着所有窗口的层级关系和生命周期——这和传统 X11/Wayland 的思路不一样。在标准 Wayland 下客户端自己通过wl_subsurface和xdg_popup协议向合成器声明窗口之间的父子/模态关系子窗口销毁时合成器会通过对应的事件通知父窗口。鸿蒙的 WindowManagerService 不一样。它把窗口树的管理权完全收归服务端Chromium 客户端侧拿到的是一个简化过的窗口句柄。子窗口在服务端被销毁时服务端通知客户端的方式不是标准的zaura_surface模态状态变更事件而是一个通用的窗口销毁通知。WindowTreeHostMus收到这个通知后去调用父窗口的OnModalStateChanged但此时父窗口的aura::Window对象上已经没有子窗口的引用了——child_window_已经是个悬空状态条件判断if (window-parent())直接走不进去。翻译成人话就是子窗口从服务端发来的通知里不包含它曾经是谁的子窗口这条信息父窗口这端连自己有没有过子窗口都判断不了更别说解除模态阻塞了。我往下翻了几十行。在一个// TODO(b/...)注释里看到了这段// WindowTreeHostMus 源码中的注释大意// TODO: Mus currently doesnt propagate OnModalStateChanged when// a child window is destroyed from the server side. The parent// windows event dispatcher may remain in modal_blocked_ state// until the next top-level window event fires.就是这行注释。一句 TODO让我的主窗口在鸿蒙 PC 上愣是死了半个下午。而且我顺手git log --follow查了一下这个文件这个 TODO 在好几年前的 commit 里就已经躺着了——保守估计三年以上。看着不像是近期会被修掉的样子。说白了这不是一个 bug这是 Mus 窗口系统的设计取舍。鸿蒙团队选择把窗口树的管理全部收到服务端是为了更好的跨设备一致性和安全性代价就是客户端侧拿不到足够精确的子窗口生命周期通知。这个 tradeoff 我能理解但不代表用户买账。你可能会问那我干脆不用parent参数换成modal: true不就行了我试过。// 方案 Bmodal: true 替代 parentconstsettingsWinnewBrowserWindow({width:480,height:600,modal:true,// 用模态窗口替代 parent 关联webPreferences:{/* ... */},});这个方案也有毛病。modal: true在鸿蒙 PC 上某些机型的nativeWindowHandle会丢失焦点按 ESC 关不了窗口。而且模态窗口会阻塞父窗口的全部输入——设置弹窗这种场景用户经常需要一边看设置一边在主窗口里对一下数据全部阻塞太粗暴了。所以我最终还是选了parent 关闭前setParentWindow(null)的方案。没多优雅但确实稳。下面是我最终塞进工程里的完整兼容代码直接复制就能用// harmonyos-window-helper.tsimport{BrowserWindow}fromelectron;/** * 鸿蒙 PC 上安全创建父子关联窗口 * 自动处理关闭时的 parent 解除避免主窗口冻结 */exportfunctioncreateSafeChildWindow(parent:BrowserWindow,options:Electron.BrowserWindowConstructorOptions):BrowserWindow{constisHarmonyOSprocess.platformlinux;constchildnewBrowserWindow({...options,parent,});if(isHarmonyOS){// 核心修复关闭前解除父窗口关联child.on(close,(){child.setParentWindow(null);});// 兜底如果父窗口仍然卡住5 秒后强制恢复letfrozenTimer:NodeJS.Timeout|nullnull;child.on(closed,(){frozenTimersetTimeout((){parent.focus();parent.webContents.focus();},5000);});// 如果父窗口正常恢复没卡住清掉兜底 timerparent.on(focus,(){if(frozenTimer){clearTimeout(frozenTimer);frozenTimernull;}});}returnchild;}把这个 helper 塞到雷达鸭的工程里之后弹窗关掉主窗口冻结的问题再没出现过。多窗口交互——设置面板、确认弹窗、详情预览这些场景——在鸿蒙 PC 上老老实实走这个 helper别头铁硬扛。那行 Chromium 源码里的 TODO 注释不知道什么时候会被修掉但在那之前setParentWindow(null)别忘了。等鸿蒙 Next 正式发了希望 WindowManagerService 的窗口生命周期回调能把这坑填上。关于作者老三10 年以上软件开发经验软件设计师、人工智能应用工程师。当前专注鸿蒙 ArkTS 北向开发与 Web 前端同时探索 AI 自动化在日常开发中的应用。不定期在 CSDN 分享鸿蒙 / Electron / AI 方向的技术踩坑笔记。本文遵循 MIT 协议转载请注明出处。