Qt 信号槽的实现 - DevBean Tech World
Qt 的信号槽和属性系统基于在运行时进行内省的能力,内省意味着,我们可以列出对象的方法和属性列表,并且能够获取有关它们的所有信息,例如其参数类型。C++ 原生并没有提供内省,所以 Qt 提供了一个工具MOC(本质:代码生成器)来支持它,它处理头文件,生成额外的 C++ 文件,这些文件将同程序剩下的部分一起编译。这些生成的 C++ 文件包含了内省所需要的所有信息。
对于信号和槽来说,本身就是普通的函数,用signals(Qt5前被扩展为protected,qt5后为public)、slots、Q_OBJECT、emit、SIGNAL 和 SLOT等进行扩展声明,是为了MOC认识信号和槽。
#define signals public
#define slots /* nothing */
Q_OBJECT 定义了一系列函数和一个静态的 QMetaObject 对象。这些函数由 MOC 在生成的文件中实现。
emit 是一个空的宏。甚至 MOC 也不会处理它。换句话说,emit 其实是可选的,没有什么含义(除了提醒开发者)。
宏仅由预处理器使用,将参数转换成字符串,并且在之前添加一个代码。
内省表:
static const uint qt_meta_data_Counter[] = {
// content:
7, // revision
0, // classname
0, 0, // classinfo
2, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
// signals: name, argc, parameters, tag, flags
1, 1, 24, 2, 0x05,
// slots: name, argc, parameters, tag, flags
4, 1, 27, 2, 0x0a,
// signals: parameters
QMetaType::Void, QMetaType::Int, 3,
// slots: parameters
QMetaType::Void, QMetaType::Int, 5,
0 // eod
};
qt_meta_data_Counter 是一个 uint 数组,生成代码的时候已经为我们分为五个部分:第一部分 content,也就是内容,分为 9 行。第一行 revision,指明 moc 生成代码的版本号(Qt4 的 moc 生成的代码,该值是 6,也就是相当于 moc v6;Qt5 则是 7)。第二个 classname,也就是类名。这是一个索引,指向字符串表的某一个位置(本例中就是第 0 位)。后面便是类信息 classinfo、函数位置等的信息。总体来说,这个表就是一个索引表。
该表中,我们的表格有两列,第一列是总数,第二列是在这个数组中描述开始的索引,可看出定义了两个函数,函数描述的开始位置是索引 14。
函数描述由 5 个 int 组成。第一个是名字,这实际是其在字符串表(我们会在后面看到字符串表的细节)的索引位置。第二个整型是参数的个数,接下来是一个索引,表明在哪里可以找到这个参数的描述。现在我们先忽略 tag 和 flag 两个数据。对每一个函数,moc 还会保存每一个参数的返回类型、类型以及名字的索引。
MOC 同时实现了信号。它们就是普通的函数,创建了一个指向参数的指针的数组,并将这些传给 QMetaObject::activate 函数。数组的第一个元素是返回值。在我们的例子中,这个值是 0,因为返回值是 void。传给 activate 的第三个参数是信号的索引(本例中是 0)。
// SIGNAL 0
void Counter::valueChanged(int _t1)
{
void *_a[] = { 0, const_cast(reinterpret_cast(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
我们可以利用某个槽在 qt_static_metacall 函数的索引位置来调用这个
void Counter::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
Counter *_t = static_cast(_o);
switch (_id) {
case 0: _t->valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: _t->setValue((*reinterpret_cast< int(*)>(_a[1]))); break;
default: ;
}
}
在每一个 QMetaObject 中,槽、信号以及其它该对象可调用的函数都会分配一个从 0 开始的索引。它们是有顺序的,信号在第一位,然后是槽,最后是其它函数。这个索引在内部被称为相对索引。它们不包含父对象的索引位。
一般而言,我们并不想知道一个比特定类更一般的索引,但是却想包含在继承链中其它函数的索引。为了实现这一点,我们在相对索引的基础上添加一个偏移量,得到绝对索引。
连接机制使用以信号为索引的向量。
Qt 会去查找元对象的字符串表来找出相应的索引。
创建一个 QObjectPrivate::Connection 对象,将其添加到内部的链表中。
为每一个信号添加一个已连接的槽的列表。每一个连接都必须包含接收对象和槽的索引。这是双向链表,连接使用正向连接链表,断开连接使用反向连接链表。
当我们调用信号时,实际是调用 MOC 生成的代码,而这部分代码是调用了 QMetaObject::activate。