BUG 场景

《写作天下》新出了名片库功能,所有的操作都在侧边栏中,包括添加名片、修改名片等。

原图
原图

在侧边栏的按钮中添加名片、双击卡片弹出编辑窗口。

为了方便,将之转移到了编辑框中,在编辑框的菜单中弹出名片操作。

名片图
名片图

本以为只是极度简单的添加菜单、信号槽连接,第一次测试并没有问题,心中侥幸,但在接下来的使用过程中却出现了偶尔的卡死崩溃问题。

BUG 复现

编辑框的右键菜单中添加名片,通过信号槽连接到主窗口,弹出名片编辑弹窗。实测有几率崩溃。

之后重复了上百遍的尝试添加打开弹窗,然而只有3次崩溃,其余并没有任何问题。

换了个思路,点击“创建名片”按钮,并无异样,名片也顺利创建了。然而在第二次点击创建的时候,程序直接卡死,崩溃。

找到了百分百复现的地方,接下来就开始了探索。

BUG 探索

从“添加名片”的按钮点击事件开始,打LOG。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CardEditor::slotCreateCard() // “创建”按钮的槽
{
log("创建名片");
if (gd->clm.currentLib() == nullptr)
return;

card->name = name_edit->text();
/* ...省略一部分... */

log(" 添加到全局名片");
gd->clm.currentLib()->addCard(card); // 添加到全局名片中
log(" 名片创建完毕信号");
emit signalCardCreated(card);
log("创建名片 结束");
}

因为行数较少,所以在每一小步的前后都加上log。

接着一步步深入,这是信号槽顺序:

名片编辑窗口.创建名片 >> 名片库.添加名片 >> 名片库.触发添加信号 >> 名片库管理器.接收槽 >> 名片列表控件管理器.接收槽 >> 2个名片列表.添加新名片 >> 名片列表控件管理器.触发刷新章节名片信号 >> 开线程异步刷新 >> 刷新当前章节的名片 >> 刷新章节名片列表控件 >> 关闭名片编辑窗口

这么多文件?一步步深入,写了几十行的log代码,打印出上百行。

日志

按钮 >> 名片库 >> 名片库管理器 >> 名片控件列表管理器 >> 名片控件列表 >> 章节编辑器,主要就是这四个类。

名片库 Cardlib

1
2
3
4
5
6
7
8
9
10
11
void Cardlib::addCard(CardBean *card)
{
log("Cardlib.addCard");
if (card->id.isEmpty())
card->id = getRandomId();
log(" save");
saveToFile(card);
log(" append");
appendCard(card);
log("Cardlib.AddCard finish");
}

名片库管理器 CardlibManager

1
2
3
4
5
6
7
connect(cardlib, &Cardlib::signalCardAppened, [=](CardBean *card) {
log("clm.signalCardAppend");
emit signalCardAppened(card);
log("clm.signalRehighlight");
emit signalRehighlight();
log("clm.signalRehighlight finish");
});

名片控件列表管理器 CardlibGroup

1
2
3
4
5
6
7
8
9
10
// 名片库添加名片时,添加到卡片列表末尾,并且聚焦此卡片
connect(&gd->clm, &CardlibManager::signalCardAppened, [=](CardBean *card) {
log("接收到CardlibManager的signalCardAppend");
if (card->disabled)
return;
using_list_widget->addOneCard(card);
using_list_widget->scrollToBottom();
log("发送刷新正在编辑的章节名片信号");
emit signalNeedRefreshEditingCards();
});

名片控件,由于之前在侧边栏的操作无异常,所以只简要测试。

章节编辑器 ChapterEditor

1
2
3
4
5
6
7
connect(&gd->clm, &CardlibManager::signalRehighlight, [=] {
if (!editing.isChapter() && !editing.isOutline()) return ;
log("editor.highlighter begin");
highlighter->rehighlight(); // 关键词高亮
log("editor.highlighter end");
});
});

查看日志,最后卡在了 editor.highlighter begin 这一行,说明是 highlighter 这一行出问题了,高亮显示名片内容时崩溃。

进入 highlighter,重绘高亮的 highlightBlock 的方法中在名片部分前后打上 log,输出,发现有头有尾,应该是正常运行,无法发现在哪里停止运行。

1
2
3
4
5
6
7
8
log("highlightBlock begin");
if (gd->clm.currentLib() != nullptr)
{
CardList& cards = gd->clm.currentLib()->using_cards;
foreach (CardBean* card, cards)
{ /* ...省略... */ }
}
log("highlightBlock end");

甚至注释掉名片部分,也无法阻止在第二次创建名片的时候停止运行。

日志是这样的:

1
2
3
4
5
6
7
8
9
10
11
clm.signalRehighlight
editor.highlighter begin
highlightBlock begin
highlightBlock end
highlightBlock begin
highlightBlock end
...
editor.highlighter end
...
editor.highlighter begin
(第二次创建名片,崩溃)

问题肯定就出在了 highlighter->rehighlight(); 这一行。将它延时100毫秒:

1
2
3
4
5
6
7
8
connect(&gd->clm, &CardlibManager::signalRehighlight, [=] {
log("editor.slot.rehighter");
QTimer::singleShot(100, [=]{
log("editor.rehighter begin");
highlighter->rehighlight(); // 关键词高亮
log("editor.rehighter end");
});
});

继续崩溃。

注释掉 highlighter->rehighlight(); ,这下子倒是没有问题了,但是也彻底丧失了高亮的功能,这是我不想的,日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
clm.signalRehighlight
editor.slot.rehighter
editor.slot.rehighter
editor.slot.rehighter
editor.slot.rehighter
...
(延迟100毫秒后)
editor.highlighter begin
editor.highlighter end
editor.highlighter begin
editor.highlighter end
editor.highlighter begin
editor.highlighter end
editor.highlighter begin
editor.highlighter end

思考为什么会导致崩溃。最可能的原因就是空指针了。

再研究日志,通过全局的名片库管理器发送的信号,只发送了一次,但是为什么接收到了4次,即高亮四次?

这才想到,目前一共打开了2个能接收信号的自定义编辑框,分别是章节、大纲,但还有两次呢?

突然意识过来,名片编辑窗口中,显示名片自己信息的编辑框,就是和章节编辑一模一样的自定义编辑框!而且还是两个!这就解释了为什么会多出两个接收的槽。

BUG 原因

名片编辑窗口,为了显示简介和详情的高亮,它内部本身就用了略微修改的章节编辑框ChapterEditor类,由其派生出一个通用的GeneralEditor,后期可添加适配多个场景,例如大纲。而在构造函数中,连接了全局的名片管理器,统一刷新名片高亮。

除了两个特殊的编辑框,还有一个“创建名片”按钮,点击后创建名片,并且关闭弹窗本身。为了防止内存溢出,又设置了自动 delete 的属性setAttribute(Qt::WA_DeleteOnClose);。创建名片之后,立即关闭自己,触发delete,同时由于控件的父子关系,自动析构子对象,包括两个正在刷新名片高亮的编辑框。

原因也正是如此,可能 Qt 在高亮时没有阻塞主线程,正在高亮的同时又 delete 掉编辑框控件,导致没有高亮的对象,野指针导致停止运行。

BUG 解决

自定义编辑框连接信号槽时,判断编辑的对象类型,如果是名片编辑窗口的那两个,则不刷新高亮。

1
2
3
4
5
6
if (!editing.isBrief() && !editing.isDetail()) // 如果不是名片的简介和详情
{
connect(&gd->clm, &CardlibManager::signalRehighlight, [=] {
highlighter->rehighlight();
});
}

这样的唯一问题,就是在编辑某一个名片时,这个窗口内部并不会立即修改高亮,而是需要等待后期刷新。

BUG 总结

多线程关键词高亮,嗯,这个我真没想到。

还有编辑框析构后没有删除控制高亮的对象,也没有想到。