ARTICLE · 998486
Qt 信号槽源码解析:emit 之后,Qt 到底做了什么
Qt 信号槽源码解析:emit 之后,Qt 到底做了什么
写下这两行代码时,表面上只是“通知一下”:
emit valueChanged(value);connect(model, &Model::valueChanged, view, &View::updateValue);但 C++ 本身没有内建的信号槽。编译器不认识 signals、slots 和 emit。Qt 能做到这一层,是因为它在正常编译前插入了一个工具:moc,Meta-Object Compiler。
源码解析最容易走偏的地方,是一上来钻进几十层私有类。先抓住一条主线就够了:
signals 声明 -> moc 生成信号函数实现 -> emit 调用信号函数 -> QMetaObject::activate() -> 遍历已连接的槽 -> 直接调用,或投递 QMetaCallEvent理解这条线后,线程、连接类型、参数复制和断开连接的行为都会有地方安放。
signals 不是 C++ 关键字
先看一个最普通的类:
class Counter : public QObject{ Q_OBJECTpublic:void setValue(int value);signals:void valueChanged(int value);};在 Qt 头文件里,signals 本质上是给 moc 和 C++ 编译器看的宏。emit 也不是神秘指令,在通常配置下它会展开为空。
所以:
emit valueChanged(value);对 C++ 编译器来说,最终仍是一次普通函数调用:
valueChanged(value);区别在于 valueChanged(int) 的函数体不是你手写的,而是 moc 根据类的元对象信息生成的。生成文件通常叫 moc_类名.cpp,构建目录里可以找到它。别直接修改它,下一次构建就会被覆盖;它是看机制时的观察窗口,不是业务代码的编辑点。
moc 生成了什么
moc 会扫描带 Q_OBJECT 的类,把信号、槽、属性、枚举等信息整理成元对象数据,并生成几类关键代码:
• 类的 staticMetaObject,里面描述方法名、参数类型、属性等元信息。• qt_metacall(),让 Qt 能按方法索引调用槽函数。• 每个信号的函数实现。
不同 Qt 版本生成代码的排版和辅助结构会变,但信号函数的核心形态大致相同:
void Counter::valueChanged(int value){ void *arguments[] = { nullptr, &value }; QMetaObject::activate(this, &staticMetaObject, signalIndex, arguments);}这里的 signalIndex 是信号在元对象方法表中的索引,arguments 是参数地址组成的数组。第一个位置预留给返回值,信号通常没有返回值,所以放 nullptr。
这就是为什么 emit 不会“广播一段字符串名字”。moc 早已经把信号和方法索引整理好了;运行时 activate() 拿到的是发送者对象、元对象、信号索引和参数地址。
connect 并不立刻调用任何槽
执行 connect() 时,Qt 做的是登记工作:把“发送者的某个信号”与“接收者的某个槽或函数对象”建立关联。
connect(counter, &Counter::valueChanged, label, &QLabel::setNum);现代函数指针写法会在编译期检查很多事情,例如信号是否存在、槽参数能否接收信号参数。检查通过后,Qt 内部仍要创建一条连接记录。实现细节会用到私有的连接数据结构,例如 QObjectPrivate::ConnectionData 和 QObjectPrivate::Connection;这些名字可能随 Qt 版本调整,不该作为应用代码依赖。
可以把这条记录想成一张表项:
connect() 返回的 QMetaObject::Connection,就是对这条内部记录的一个外部凭据。保存它以后,才能精准断开 Lambda 连接:
QMetaObject::Connection connection = connect( counter, &Counter::valueChanged, this, [this](int value) { ui->label->setNum(value); });disconnect(connection);activate 才是真正的分发入口
当 Counter::valueChanged() 被调用,moc 生成的函数会进入 QMetaObject::activate()。Qt 的核心实现位于 Qt Core 模块的 qobject.cpp 一带;不同发行版、不同 Qt 版本的函数拆分不一样,不要拿网上某一版的行号当永久坐标。
activate() 的任务很像一个调度员:
1. 根据发送者和信号索引,找到这条信号对应的连接列表。 2. 逐条检查连接是否有效、是否被断开。 3. 判断这一条连接此刻应该直接调用还是排队。 4. 按正确方式执行接收端槽函数或 Lambda。
一个信号能连多个槽,正是因为连接列表里可以有多条记录:
connect(counter, &Counter::valueChanged, label, &QLabel::setNum);connect(counter, &Counter::valueChanged, this, &MainWindow::saveRecentValue);通常按连接建立顺序调用,但业务代码不应该把关键正确性建立在多个槽的执行顺序上。若 A 必须在 B 之前完成,把这两个步骤放到一个明确的协调槽函数或流程里,比依赖连接注册顺序更稳。
直接连接:在当前栈里调用
如果连接是 Qt::DirectConnection,或者 AutoConnection 判断双方在同一个线程,Qt 会在当前调用栈内调用目标。
setValue() -> emit valueChanged() -> QMetaObject::activate() -> View::updateValue() -> 回到 setValue() 的下一行这解释了两个常见现象。
第一,槽函数慢,emit 就慢。直接连接没有把工作“后台化”,槽函数跑多久,发射信号的线程就等多久。
第二,槽函数可以重入。槽里再改状态、再发信号,控制流可能回到原来的对象。Qt 在分发时需要处理连接在调用期间被断开的情况,应用代码也需要避免把对象状态写成“绝不可能嵌套进入”。
对于成员槽,Qt 可以通过元对象调用机制定位目标;对于函数指针和 Lambda,Qt 在连接时保存对应的调用包装,分发时再执行它。你不需要为了理解原理而去手写 qt_metacall(),它是 Qt 元对象系统的基础设施。
队列连接:先做一份待办事项
当连接是 Qt::QueuedConnection,或者 AutoConnection 判断发送线程与接收者线程不同,Qt 不会直接调用接收槽。
它会创建一个元调用事件,内部常见名称是 QMetaCallEvent,把“调用哪个槽、带哪些参数”打包后投递到接收者对象所属线程的事件队列:
工作线程 emit progressChanged(60) -> activate() -> 创建元调用事件 -> 投递给主线程中的窗口对象主线程事件循环 -> 取出元调用事件 -> 执行 MainWindow::updateProgress(60)这一步必须复制能跨越当前调用栈的参数。int、QString 这类值很直接;自定义类型需要让 Qt 的元类型系统认识它。否则运行时会见到那句熟悉的警告:
Cannot queue arguments of type 'TaskResult'解决方法不是改连接类型,而是用 Q_DECLARE_METATYPE 声明,并在需要时用 qRegisterMetaType() 注册类型。
队列事件最终依赖接收者线程的事件循环。事件循环不跑,事件就只是在队列里等待,槽函数自然不会进。这正是某些重写 QThread::run() 却没有 exec() 的代码,跨线程槽看起来失灵的原因。
QObject 销毁时,连接为什么会自动失效
连接记录并不只是一个永远增长的数组。QObject 析构时,Qt 会清理它相关的连接:它作为发送者建立的连接,以及它作为接收者建立的连接。
connect(worker, &Worker::progressChanged, window, &MainWindow::updateProgress);window 销毁后,Qt 不会继续调用 updateProgress()。这也是为什么 Lambda 连接最好传 context:
connect(worker, &Worker::progressChanged, this, [this](int value) { ui->progressBar->setValue(value); });这里的 this 让 Qt 知道 Lambda 的有效期应当绑定到窗口。没有 context 时,Qt 无法从 Lambda 捕获列表里推断 this 已经失效,后续调用就可能踩到释放后的内存。
已经投递、但还没处理的队列调用也要被谨慎处理。Qt 会结合接收对象与连接的存活状态避免调用已销毁的对象;但你自己跨线程传递的裸指针和外部资源,Qt 没法替你证明生命周期,仍需要应用层设计所有权。
源码该怎么读才不迷路
真想跟一遍 Qt 源码,建议按这个顺序走:
1. 从构建目录里找到某个简单类生成的 moc_*.cpp,看信号函数怎样调用QMetaObject::activate()。2. 在 Qt Core 源码的 qobject.cpp搜索QMetaObject::activate,看它如何进入内部激活逻辑。3. 再搜索 QObject::connect和QObject::disconnect,了解连接怎样进入和离开连接数据。4. 最后搜索 QMetaCallEvent与postEvent,把队列连接的投递和事件循环串起来。
不要一开始就试图记住每个私有类名。Qt 私有实现为性能、兼容性和线程安全做了不少分层,版本之间也会改。真正值得带走的是稳定的因果关系:moc 生成信号入口,connect 保存关联,activate 决定分发,队列连接通过事件循环换线程执行。源码里的类名会变,这条路径不会变。