Qt源码剖析之信号槽机制(二)—连接的性能优化
2025-3-18
| 2025-4-14
Words 2441Read Time 7 min
type
status
date
slug
summary
tags
category
icon
password
在 Qt 的信号槽机制的实现中,对于信号的所有者一般叫 Sender,槽的所有者一般叫 Receiver,信号和槽可通过 QObject::connect() 创建连接。信号发射后,对应被连接的槽被执行。信号可以连接多个槽,也可以连接多个信号。信号发射后,对应的所有被连接的槽(或信号)需要被处理,Receiver 的槽被执行期间也有可能发射信号。同时一个类如果有多个信号,那么发射信号时,怎么高效地找到对应的连接,如何高效地匹配槽?本文基于 Qt6.8,分析 Qt 信号槽机制所依赖的数据结构,分析其中的一些性能优化技巧。

主要数据结构

ConnectionOrSignalVector (最低位标记法实现多态)

简介ConnectionOrSignalVector 为信号向量类 SignalVector 和 维护单个连接的类 Connection 的基类。
SignalVector 顾名思义,维护不同信号的连接,因为一个信号可以连接多个槽/信号。
Connection 则是维护单个连接,这里的连接是指一个信号对应一个槽/信号的连接。
功能:利用最低位标记法实现运行时多态
定义
ConnectionOrSignalVector 利用 union 仅维护一个指针,可以表示待清理的孤立连接链表的 nextInOrphanList ,以及连接到槽的连接链表 next

关键优化

该类有两个静态方法,利用了地址低位标记以最小的性能开销实现多态。
首先,SignalVectorConnection 继承自 ConnectionOrSignalVector 。在C++中实现运行时多态,依赖虚表指针和虚表,虚表指针大小为一个指针,而虚表为若干指针的数组。
Qt 在此没有使用标准的 C++ 运行时多态,而是利用地址字节对齐的特性,通常地址都是 4 字节或 8 字节对齐,因此地址的最低 3 位必然是 0,Qt 将地址的最低位作为标志位,用于区分 SignalVectorConnection ,并提供两个静态方法用于转换。
  • asSignalVector() 方法:接受一个基类 ConnectionOrSignalVector 指针参数,如果参数的最低位为1,则清空标志位,将基类指针转换位继承类指针。否则返回 nullptr ,此时可以认为该基类指针实际指向继承类 Connection
  • fromSignalVector() 方法:可将 SignalVector 指针伪装成 Connection 指针,并标记最低位,在后续的使用中不丢失原有的类型信息。
 

Connetion(单个连接)

简介Connection 为单个连接,同时维护两个链表,一个是同一个 Sender 的不同连接的双向链表 ConnectionList ,一个是维护根据被触发的顺序的连接的单向链表(比如当前连接被触发,在槽执行的过程中又发射一个信号,触发下一个连接)。在附录B中,提供了一个可视化的图像。
功能:维护单个信号和单个槽/信号之间的连接。提供触发槽的接口。
定义
这个类维护了
  • prev: 指向前一个连接,用于接收者端的链表。
  • nextConnectionListprevConnectionList: 同一个信号对应的连接的双向链表
  • senderreceiver: 分别指向发送者和接收者对象,均为原子指针。
  • callFunctionslotObj: 存储槽函数,静态调用函数(由moc生成)或槽对象。
  • argumentTypes: 存储参数类型,原子指针。
  • ref_: 引用计数,初始为 2,用于内部列表和 QMetaObject::Connection
  • id, method_offset, method_relative, signal_index: 用于标识和定位方法。
  • 标志位:connectionType(0=自动,1=直接,2=队列,3=阻塞)

ConnectionList(单个信号的连接链表)

在上面介绍过,Connection 拥有nextConnectionListprevConnectionList 所以这是一个双向链表。
ConnectionList 维护了该双向链表的首和尾。
 

SignalVector(所有信号的连接向量)

简介:用于存储一个类的所有信号的所有连接。
功能:

关键优化

该类表面上仅维护一个 quintptr 类型的 allocated 。但通过观察该类的 at() 方法可以得知,在该类的实例所在的位置,存储了一个 ConnectionList 数组。 在上面提到过,ConnectionList 维护的是一个双向链表的类,这里通过数组存储,提供了 O(1) 时间复杂度访问连接链表。了解 Qt 元对象系统的都知道,存储信号用的是索引 signal_index
因此,当我们发射一个信号的时候,我们可以同元对象系统中获取当前信号的索引 signal_index ,再通过索引,以 O(1) 的方式获取该信号对应的所有连接。然后通过双向链表,依次执行每个连接。
 

ConnectionData

功能:用于管理 QObject 类的所有连接
这里比较关键的方法就是 void resizeSignalVector(uint size)
在后续的信号槽机制源码分析中,我们会看到,当我们创建一个连接的时候,都要调用 resizeSignalVector(signal_index+1) ,从这个方法实现上也能看出,它的作用就是创建一个 SignalVector 变量,并在其内存地址的下一个位置创建一个 ConnectionList 数组。
我们前面提到过,ConnectionOrSignalVector 管理一个 union,其中有一个成员就是管理孤立连接的 nextInOrphanList ,它就是在 resizeSignalVector() 的时候,如果当前数组大小不足以容纳当前信号的连接的时候,需要重新开辟一片空间,同时拷贝之前的数组空间的元素。而原位置上的连接不能简单的释放,需要用 nextInOrphanList 进行管理,在析构或 disconnect 的时候清理。
 

Sender

简介:这是一个用于跟踪信号发射时的发送者,在处理某个信号连接的时候,记录当前正在处理的连接。比如发射一个信号时,会依次处理该信号的所有连接,在处理某一个连接的时候,被连接的槽/信号会被执行,而在槽执行的过程中如果继续发射信号,就会形成信号的嵌套执行了,因此需要时刻维护当前正在处理的连接。

附录 A. 类图

notion image
 

附录 B. 连接内存布局图

notion image
Qt源码剖析之信号槽机制(一)—信号槽机制的实现QScopeGuard 源码分析
Loading...