信号槽机制是 Qt 元对象系统主要提供的一项功能,其本质上是一个实现观察者模式的框架,使得用户能够通过 Qt 提供的基础构件和工具 快速实现一个观察者模式,并且这个观察者模式能够在多线程下工作。
对于 Qt5 之前的实现,在进行信号连接时,Qt 首先会根据客户代码传入的信号名和槽函数名字符串来遍历 staticMetaObject 成员的 data 和 stringdata 来查找匹配的项目,当找到匹配项目后,Qt 将新建一个 Connect 结构以记录本次连接的相关信息。它包含信号发送者和信号接收者对象, 以及信号和槽函数相对应的索引值等信息以方便之后使用。
对于信号,MOC 将它们实现为简单的函数,这个函数只是调用 QMetaObject::activate 函数并将信号参数和返回值作为一个指针数组传递给它。 也就是说,当我们发射信号时其实际上只是在调用一个函数,并且该函数始终是在调用线程的上下文中执行的(信号在调用线程发出,而不是发送者对象的所属线程)。
在 activate 函数内,Qt 将根据信号索引值取出当前信号的连接链表并对其进行遍历。
对于 QueuedConnection 类型的连接,Qt 将创建一个 QMetaCallEvent 并通过 QCoreApplication::postEvent 来将其发送到接收者对象所在线程的事件队列,这个 QMetaCallEvent 包含信号发送者、槽函数索引值、实参的副本、实参的类型副本等必要的信息。
对于 BlockingQueuedConnection 类型的连接,Qt 同样创建一个 QMetaCallEvent 并通过 QCoreApplication::postEvent 来将其发送到接收者对象的事件队列中, 只不过这次直接传递实参指针而无需拷贝,同时它额外传递一个信号量对象以实现阻塞等待。
对于 DirectConnection 类型的连接,Qt 将直接在当前线程调用 MOC 为类生成的 qt_static_metacall 函数,在该函数中将根据索引值调用相应的槽函数。
索引值是 Qt 为了确保快速调用信号和槽函数以及可调用函数的产物,Qt 通过一个索引值来匹配所要调用的信号、槽函数以及可调用函数。
信号和槽函数、可调用函数所对应的索引值在编译时就被确定下来,它们的索引值就是它们在 data 中的存储顺序,其值相对于当前类型从 0 开始 (最终使用时则是要转换成绝对索引,其算法为相对索引 + 所有父类中的索引数之和)。
而在 Qt5 之后,Qt 添加了新的特性,该特性支持通过成员函数指针来建立信号槽连接,并且其还可以将任意函数作为槽函数连接至信号上。
为了支持这一点,Qt 利用模板成员函数来实现对函数指针的接收,然后 Qt 将函数指针存储在相应的适配器对象之中,这个适配器对象将存储 在 conection 结构之中,在之后触发信号时 Qt 将对其进行调用。对于信号成员函数指针,Qt 还是将其转换为对应的索引值来存储。
一些补充:
- signals, slots 都只是空宏(signals 包含 public 声明),它们只是用来给 MOC 识别的标记。
- emit 也是空宏,并且它不被 MOC 解析,它只是一个给开发人员看的提示符。
- Q_OBJECT 宏则是定义了实现元对象系统所需要的接口,这些接口在 MOC 生成的文件中被实现。
- SLOT 和 SIGNAL 宏则是将传入的函数名转换为字符串,并添加上各自的字符串前缀(“1” 和 “2”)以进行区分。如果 Debug 版本,它们还会在字符串中包含代码的所在文件和行号信息。
- 根据官方文档的说法,Qt 发出一个信号的开销大约为 10 个函数的调用开销。
- Qt5 之前的 connect 语法在运行时通过比较字符串来实现连接的匹配建立,这要求开发人员正确书写信号槽的名称以确保匹配成功。此外, 如果信号或槽函数名不规范,那么将会导致 Qt 执行两次函数匹配,第一次由于名称不规范而失败,第二次则是在尝试矫正名称后再进行重试。
- 而在 Qt5,Qt 的 connect 语法支持通过成员函数指针,甚至将任意函数作为槽函数来连接至信号上,并且其还支持参数的隐式类型转换。 这为信号槽机制引入了更大的灵活性,并且使得一些硬编码上的错误能够在编译期就被发现。
- Qt 在确保信号槽的线程安全时,其使用的互斥锁是根据发送对象或接收对象的地址来从一个固定大小的静态锁池(131大小)中获取的。 因此,如果有多个线程同时访问信号槽系统,而它们刚好获取到了同一个互斥锁,那么其中一个线程将会被另一个线程所阻塞, 并且如果占有锁的那个线程在执行过程中又去等待阻塞线程的话,那么将会出现死锁。(个人推测,待验证)
- 当使用非成员函数指针作为槽函数连接信号时,要留意槽函数内使用的对象的生命周期。因为除非显式调用 disconnect,否则这个连接 将只在发送者被销毁时才会断开。