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
。关键优化
该类有两个静态方法,利用了地址低位标记以最小的性能开销实现多态。
首先,
SignalVector
和 Connection
继承自 ConnectionOrSignalVector
。在C++中实现运行时多态,依赖虚表指针和虚表,虚表指针大小为一个指针,而虚表为若干指针的数组。Qt 在此没有使用标准的 C++ 运行时多态,而是利用地址字节对齐的特性,通常地址都是 4 字节或 8 字节对齐,因此地址的最低 3 位必然是 0,Qt 将地址的最低位作为标志位,用于区分
SignalVector
和 Connection
,并提供两个静态方法用于转换。-
asSignalVector()
方法:接受一个基类ConnectionOrSignalVector
指针参数,如果参数的最低位为1,则清空标志位,将基类指针转换位继承类指针。否则返回nullptr
,此时可以认为该基类指针实际指向继承类Connection
。
-
fromSignalVector()
方法:可将SignalVector
指针伪装成Connection
指针,并标记最低位,在后续的使用中不丢失原有的类型信息。
Connetion(单个连接)
简介:
Connection
为单个连接,同时维护两个链表,一个是同一个 Sender
的不同连接的双向链表 ConnectionList
,一个是维护根据被触发的顺序的连接的单向链表(比如当前连接被触发,在槽执行的过程中又发射一个信号,触发下一个连接)。在附录B中,提供了一个可视化的图像。功能:维护单个信号和单个槽/信号之间的连接。提供触发槽的接口。
定义:
这个类维护了
prev
: 指向前一个连接,用于接收者端的链表。
nextConnectionList
和prevConnectionList
: 同一个信号对应的连接的双向链表
sender
和receiver
: 分别指向发送者和接收者对象,均为原子指针。
callFunction
或slotObj
: 存储槽函数,静态调用函数(由moc生成)或槽对象。
argumentTypes
: 存储参数类型,原子指针。
ref_
: 引用计数,初始为 2,用于内部列表和QMetaObject::Connection
。
id, method_offset, method_relative, signal_index
: 用于标识和定位方法。
- 标志位:
connectionType
(0=自动,1=直接,2=队列,3=阻塞)
ConnectionList(单个信号的连接链表)
在上面介绍过,
Connection
拥有nextConnectionList
和 prevConnectionList
所以这是一个双向链表。而
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. 类图
附录 B. 连接内存布局图
