美滋滋的让自己的软件拥有了托盘图标,可以过一会儿图标自己消失了,这是怎么回事呢?又如何解决呢?
Electron
Electron是Github出的可以使用Node+前端技术开发跨平台桌面软件的框架。大名鼎鼎的VS Code就是用这个框架开发的。
我所开发的mHosts也是用的Electron,在开发的时候也是遇到了图标自动丢失的问题。
问题复现
如上图,mHosts软件提供了一个托盘图标,用户可以点击图标使用托盘菜单快速切换hosts规则。奇怪的是在没有关闭应用,没有强制终止进程的情况下,窗口还在,但托盘图标却会在一段时间后自动消失。这是什么原因?又如何解决呢?
原因分析
- 程序某个地方出现未处理的异常导到应用退出。
- 其他软件挤掉了我的托盘图标。
- 显示托盘图标需要权限,而我的应用没有相应权限。
- 窗口隐藏以后图标也会随之隐藏。
- 维护托盘图标的对象消失导致图标消失。
问题排查
一、程序异常退出
考虑到可能是程序其他地方有未处理的异常导到程序或进程退出,从而使图标消失。所以我把主进程和渲染进程都打开了调试器,并且打开在异常处断点。但发现在图标消失的前后并没有异常发生。所以排除了程序异常退出的可能。
二、图标补其他软件挤掉
当时发现问题的时候是在Mac上,由于MacOS上的菜单栏和托盘图标是显示在一行的,我想是不是系统有限制,在托盘图标过多情况菜单显示时会被自动清理掉。这个问题还是很好确定的,把带图标的其他所有软件暂都先关掉,只保留我的软件。最后发现图标还是会自动消失,所以这个可能也被排除了。
三、需要权限
需要权限这种情况我当时想到后立即查了Electron和MacOS的开发文档,发现并没有这方面的说法。再因为图标并不是一直不显示,而是显示一段时间以后自动消失,所以需要权限这种可能也被排除了。
四、窗口隐藏图标也会隐藏
考虑到可能托盘图标是跟随主窗口的,窗口消失图标也会消失。所以的针对这种情况也做了实验:
- 声明托盘图标,但不显示主窗口,如果托盘图标也没有显示,则有可能是这种原因。
- 声明托盘图标,显示主窗口,但主窗口保持在前台不动,如果图标不消失,则有可能是这种原因。
- 声明托盘图标,显示主窗口后立即消失,如果图标跟着窗口消失,或图标直接不显示,则有可能是这种原因。
实验结果发现图标消失和上述3种情况并没有关联,所以也排除了图标跟随窗口隐藏的可能。
五、图标对象消失导致图标消失
图标对象消失有几种可能,一是我在写代码的过程中误给图标对象设置了其他值,比如null
或undefined
。在我排查过代码确定没有手动设置为空以后,这时我突然想到,还有一种情况下对象会自动变成空。那就是在某个对象失去来自主对象的引用后会被V8的垃圾回收机制给回收掉,如果对象被回收,对象不存在了,图标消失也说的通了。
于是我又排查了一遍代码,发现我声明托盘图标的方式是下面这样的:
function initTray(){
const tray = new Tray(nativeTheme.shouldUseDarkColors ? logoPath.white16 : logoPath.black16);
tray.setToolTip(`${app.getName()}-v${app.getVersion()}`);
tray.setContextMenu(...);
}
export default function startApplication() {
app.whenReady().then(() => {
if (isMac()) {
bounceId = app.dock.bounce("critical");
}
initTray();
setApplicationMenu();
initWindow();
initMessageChannel();
});
}
常量tray
的声明和初始化都在initTray
函数里面,其生命时期也仅限于initTray
函数。这样在app.whenReady
的回调执行完毕以后,就会失去对initTray
的引用,也就会失去对常量tray
的引用。这样在GC(Garbage Collection, 垃圾回收)的下一个回收周期到来时常量tray
就会被回收,从而导致托盘图标消失。
解决问题
知道了问题所在以后就方便解决了,把代码改为下面这样,问题成功解决。
let tray: Tray; // 将tray对象声明提升到全局范围。
export default function startApplication() {
app.whenReady().then(() => {
if (isMac()) {
bounceId = app.dock.bounce("critical");
}
tray = new Tray(nativeTheme.shouldUseDarkColors ? logoPath.white16 : logoPath.black16);
tray.setToolTip(`${app.getName()}-v${app.getVersion()}`);
setApplicationMenu();
initWindow();
initMessageChannel();
});
}
Electron的主进程和渲染进程都是基于V8引擎的。
后记
其实像这种问题直接到网上搜一下应该就可以找到解决办法,但我写mHosts软件不并是为了工作,也不是为了赚钱。一方面自己需要用到这样一款软件,另一方面也是为了学习。到网上找现成的解决方案是最快速的方法,但这款软件在没有工期压力的情况下我还是喜欢一步一步自己排查到原因并解决它。这样可能会耗费我更多的时间,但在排查的过程中我收获到的是直接在网上搜索答案无法比的。