Qt 信号槽的工作原理(一)

原文链接 https://woboq.com/blog/how-qt-signals-slots-work.html Olivier Goffart 于 2012年12月02日

Qt 因其 信号槽机制 而闻名。但它们究竟是如何工作的呢? 在这篇博客文章中,我们将深入探索 QObjectQMetaObject 的内部实现,看看 signals 和 slots 在底层是如何运作的。

在本文中,我会展示 Qt5 的部分代码,有时为了排版和简洁性进行了适当修改。


Signals 和 Slots

首先,我们回顾一下 signals 和 slots 的基本用法,来看一个官方示例。

头文件如下:

.cpp 文件中,我们实现 setValue()

然后可以这样使用 Counter 对象:

这是 Qt 自 1992 年诞生以来几乎没有变化的原始语法。

尽管 API 基本没变,但其底层实现经历了多次演进:增加了新特性,也在内部发生了大量变化。 其实并没有任何魔法,本文将向你展示它究竟是如何实现的。


MOC:元对象编译器(Meta Object Compiler)

Qt 的 signals/slots 以及属性系统,建立在运行时自省(introspection)能力之上。

所谓自省,就是能够在运行时:

如果没有这套机制,QtScript 和 QML 几乎不可能实现。

然而,C++ 本身并不支持自省,因此 Qt 提供了一个工具来弥补这一点:MOC

Important

MOC 是一个代码生成器(而不是预处理器)。

它会解析头文件,并生成一个额外的 moc_xxx.cpp 文件,与项目其余部分一起编译。 这个生成的文件包含了运行时自省所需的全部信息。

Qt 有时会因为引入代码生成器而受到语言纯粹主义者的批评。对此,Qt 在官方文档中这么说:

https://thinkinginqt.com/20251226_why-moc/20251226_why-moc.html

代码生成器没有任何问题,MOC 是一个非常有用的工具


魔法宏(Magic Macros)

下面这些都不是 C++ 的关键字

这些被称为 Qt 对 C++ 的扩展,实际上它们都是宏,定义在 qobjectdefs.h 中。

signals 和 slots 宏

译者注:

Qt5 中 define signals Q_SIGNALS 定义在 qobjectdefs.h

Qt6 中 define signals Q_SIGNALS 定义在 qtmetamacros.h

没错,signals 和 slots 本质上只是普通函数,编译器会像对待普通函数一样处理它们。

这些宏存在的真正意义是:让 MOC 能识别它们

Note

在 Qt4 及之前,signals 是 protected; 在 Qt5 中,它们变成了 public,以支持新的连接语法。


Q_OBJECT 宏

Q_OBJECT 定义了一系列函数以及一个静态的 QMetaObject 这些函数全部由 MOC 生成。


emit 宏

emit 是一个 空宏,MOC 甚至不会解析它。

换句话说:

emit 完全可以不写,仅用于提高代码可读性。


SIGNAL / SLOT 宏

这些宏只是:

在 Debug 模式下,还会附加文件和行号,用于在连接失败时输出更友好的警告信息。

为了知道哪些字符串包含行号信息,我们使用 qFlagLocation 函数,该函数会在一个 table 中注册该字符串的地址,并记录两项信息。


MOC 生成的代码

QMetaObject

下面我们来看 Qt5 中 MOC 生成的部分代码。

这里可以看到 Counter::metaObject()Counter::staticMetaObject 的实现,它们都在 Q_OBJECT 宏中声明。

这里使用 d 是为了表示“私有数据”,但它并未真正设为 private,以便:

QMetaObject 使用父对象的元对象(在这里是 QObject::staticMetaObject)作为 superdata 进行初始化。

stringdatadata 使用了一些数据进行初始化,这些数据将在本文后面进行说明。

static_metacall 是一个函数指针,被初始化为 Counter::qt_static_metacall


自省表(Introspection Tables)

下面是 QMetaObject 的整型数据

我们暂时忽略 tag 和 flags。对于每个函数,moc 还会将每个参数的返回类型、类型和索引保存到名称中。


字符串表(String Table)

这是一个静态的 QByteArray 数组,QT_MOC_LITERAL 宏创建一个静态 QByteArray,该对象引用下面字符串中的特定索引位置。


Signals

MOC 生成的 signal 本质上是普通函数,它们只是创建一个指向参数的指针数组,然后将该数组传递给 QMetaObject::activate 函数。

数组的第一个元素是返回值。在我们的示例中,返回值是 void,因此第一个元素为 0。

传递给 activate 函数的第三个参数是信号索引(在本例中为 0)。


调用 Slot

Slot 可以通过 qt_static_metacall 函数使用索引来调用:


关于索引的一点说明

Qt 内部存在 三种索引概念

  1. 相对索引(relative index)

    • 在单个类中,从 0 开始

    • 顺序:signals → slots → methods

  2. 绝对索引(absolute index)

    • 包含父类的方法

    • 对外 API 使用(如 QMetaObject::indexOfSignal、QMetaObject::indexOfSlot、QMetaObject::indexOfMethod

  3. 内部 signal index

    • 仅包含 signal

    • 用于优化存储(Qt 4.6 引入)

在使用 Qt 进行开发时,你通常 只需要了解绝对方法索引 即可。

但在阅读和分析 Qt 的 QObject 源码时,必须清楚这三种索引之间的区别


连接是如何建立的

QObject::connect() 时,Qt 会:

  1. 在元对象的字符串表中查找 signal 和 slot

  2. 得到它们的索引

  3. 创建一个 QObjectPrivate::Connection

  4. 加入内部的链表

每个连接需要存储哪些信息?我们需要一种方法,能够 快速访问某个信号索引对应的连接

由于同一个信号可能连接到多个槽,因此我们需要为 每个信号维护一个已连接槽的列表

每条连接必须包含 接收者对象(receiver object)以及槽的索引

我们还希望当接收者被销毁时,这些连接能够自动销毁,所以每个接收者对象也需要知道 谁连接到了它,以便它能清理这些连接。

下面是 qobject_p.h 中定义的 QObjectPrivate::Connection

然后,每个对象都会有一个 连接向量(connection vector):它是一个向量,用来把所有信号关联到一个 QObjectPrivate::Connection链表(linked list)

每个对象还有一个反向连接列表(reversed list),用于在对象销毁时自动删除连接:这是一个双向链表,记录“该对象连接到了哪些地方/被哪些连接引用”。

img

之所以使用链表,是因为链表可以 快速地插入和删除 节点。它们通过在 QObjectPrivate::Connection 内部保存指向前后节点的指针来实现。

注意:senderList 的 prev 指针是一个指向指针的指针(Connection prev),原因是它并不是直接指向前一个节点,而是指向“前一个节点里 next 指针本身”。这个指针只在销毁连接时使用,而不是用于反向遍历。这样做可以避免对链表第一个元素做特殊处理。

img


信号发射(Signal Emission)

当调用 signal 时,会进入 QMetaObject::activate

以下是其实现的注释版本,来自 qobject.cpp


总结

我们已经看到:

本文尚未涉及 Qt5 新语法(函数指针 / lambda 连接)的实现细节,这将留到下一篇文章中讨论。