接上文:从零开始实现信号槽机制:一
关于Qt的信号槽机制,How Qt Signals and Slots Work是一篇特别好的文章,在此对作者表示感谢。
好了,是时候写段Qt代码看看了,这是一段典型的使用Qt信号槽的代码,因为我们这段代码直接写在main.cpp里面,所以在最后记得加上#include "main.moc":
#include <iostream>
#include <QApplication>
using namespace std;
class Button : public QObject
{
Q_OBJECT
public:
void nowClick(bool b) { emit click(b); }
signals:
void click(bool);
};
class Tv : public QObject
{
Q_OBJECT
public:
Tv(int b, int t) : bootTime(b), offTime(t){}
protected slots:
void onStateChanged(bool b)
{
if ( b == true )
cout << "Tv is being turned on. bootTime is " << bootTime << endl;
else
cout << "Tv is being turned off. offTime is " << offTime << endl;
}
private:
int bootTime;
int offTime;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Button btn;
Tv tv(10, 20);
QObject::connect(&btn, SIGNAL(click(bool)), &tv, SLOT(onStateChanged(bool)));
btn.nowClick(true);
return a.exec();
}
#include "main.moc"
我们知道Qt源代码在make之前需要先进行一道qmake,而qmake会调用moc.exe这个“元对象编译器”来对包含Q_OBJECT宏的文件进行一个预处理,要弄清Qt的信号槽如何运作,首先我们得知道signals,slots这种不符合C++规范的东西到底被处理成了什么鬼,ok,我们在qobjectdefs.h里面找到它们了:
#define slots
#define signals public // Qt5 中由 protected 改为 public 以支持更多特性
#define emit
#define SLOT(a) "1"#a
#define SIGNAL(a) "2"#a
当然这些定义在某些情况下,比如定义了QT_NO_EMIT时会不同,不过这超出了本文的讨论范围,大家有兴趣可以去读下源码。好的,现在我们知道了,“slots”和“emit”根本就是两个空宏,而signals仅仅是一个public,这样看来,Qt中的信号是个真正的函数无疑,而不是像sigslot中以functor的方式实现。connect函数则是借助SIGNAL和SLOT两个宏为信号和槽函数添加了一个数字并将其转化成字符串,也就是说,上面的main()中的connect实际等价于:
QObject::connect(&btn, "2click(bool)", &tv, "1onStateChanged(bool)");
这种链接方式是可以通过编译并得到正确结果的,但我们现在还不能删掉signals,和slots关键字——它们不是定义为空吗,为什么不能删呢?很简单,signals定义的函数我们根本没有实现,它只有一个声明。既然能通过编译,说明Qt在把signals改为空之前必定还做了些其他的事情。翻看main.moc,我们果然发现了这个信号函数的实现:
// SIGNAL 0
void Button::click(bool _t1)
{
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
Qt不管该信号的参数类型与参数个数,将其统一转成了void*并存在数组中,传递给QMetaObject::activate()进行调用。到这里你也许会疑惑了,我们在前面的设计中被参数问题弄到头大,槽函数收到这堆二进制数据以后如何知道该用哪种格式来解释它们呢?
好的,active()这个函数后面再说,我们先弄清楚QMetaObject是个神马,它是理解Qt信号槽机制的关键所在。贴段官方介绍吧:
“QMetaObject类包含Qt对象的元信息。Qt的元对象系统负责信号和槽的通信机制,运行时类型信息,和Qt的属性系统。每个QObject的子类将被创建一个QMeteObject实例,并被应用于应用程序中,这个实例存储了该QObject子类所有的元信息,通过QObject::metaobject()可以获取它。”
也就是说,基于QMetaObject,我们可以获取QObject子类对象的类名、父类名、元方法(信号、槽和其他声明为INVOKABLE的方法)、枚举、属性、构造函数等诸多信息。而QMetaObject中的数据则是来自于moc对源文件所进行的词法分析。看看我们main.moc中Qt为我们的Button类生成的整型数组:
static const uint qt_meta_data_Button[] = {
// content:
7, // revision
0, // classname
0, 0, // classinfo
1, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
// signals: name, argc, parameters, tag, flags
1, 1, 19, 2, 0x06 /* Public */,
// signals: parameters
QMetaType::Void, QMetaType::Bool, 2,
0 // eod
};
// content栏目中的13个整型数表示的信息已由注释给出,对于有两列的数据,第一列表示该类项目的个数,第二列表示这一类项目的描述信息开始于这个数组中的哪个位置(索引值)。可以看到Button类包含一个方法信息(nowClick()非INVOKABLE方法不被记录),就是我们的信号了,并且该方法的描述信息开始于第14个int数据。
// signals注释下那个“1”即为qt_meta_data_Button[14],注释写得更清楚,表明这里开始记录的是(信号方法)信息,每个方法的描述信息由5个int型数据组成。分别代表方法名、该方法所需参数的个数、关于参数的描述(表示与参数相关的描述信息开始于本数组中的哪个位置,也是个索引)、以及tag和flags。最后,该数组存放了方法的返回类型、每个参数的类型、以及参数的名称。也就是说,任何一个可以拿到Button类的父类指针(QObjcet*)的对象都可以清楚地了解其signal的所有信息。
除了这个整形数组,moc还为我们生成了metaObject(),qt_metacall()等函数,前者用来获取元对象,后者十分关键,我们先将连接建立起来再来看它。
是时候建立连接了
我们现在已经知道,基于元对象系统,Qt可以通过名称很快地找到对应的方法的索引,然后,我们还需要一个用来管理连接的类,由于Qt中的QObject类即可以作为接收者也可以作为发送者,因此这个Connection需要同时包含发送对象与接收对象的指针,以及对应信号与槽函数的索引。Qt在QObjectPrivate中定义了这个Connection,位于qobject_p.h中:
struct Connection
{
QObject *sender;
QObject *receiver;
union {
StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase *slotObj;
};
// The next pointer for the singly-linked ConnectionList
Connection *nextConnectionList;
//senders linked list
Connection *next;
Connection **prev;
QAtomicPointer<const int> argumentTypes;
QAtomicInt ref_;
ushort method_offset;
ushort method_relative;
uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
ushort isSlotObject : 1;
ushort ownArgumentTypes : 1;
Connection() : nextConnectionList(0), ref_(2), ownArgumentTypes(true) {
//ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection
}
~Connection();
int method() const { return method_offset + method_relative; }
void ref() { ref_.ref(); }
void deref() {
if (!ref_.deref()) {
Q_ASSERT(!receiver);
delete this;
}
}
};
每个Sender需要维护一个QVector,其长度是其signal的个数,QVector内每个单元存放着一个Connection链表的头结点,存储该信号的每个链接;而Receiver则相对简单一些,它直接维护一个Connection链表,表示所有链接到它身上的链接。当然,一个QObjcet可能即是Sender又是Receiver,因此一个QObject可能同时在维护这两个数据结构,并且,如果是该QObjcet内部的信号槽调用,两个Connection对象将会重叠,贴张国际友人的图像像下面这样:
我们通常用来建立连接的connect()函数声明如下(Qt5支持了更多重载类型):
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type)
可以看到,Qt在建立信号槽时不需要函数指针,两个字符型的函数名即可。链接建立后,当信号发出,实际调用了 QMetaObject::activate(),该函数位于qobject.cpp:
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
int signal_index = signalOffset + local_signal_index;
/* 第一件要做的事就是快速检查64位的掩码. 如果是0,
* 则可以肯定这个信号没有被槽函数链接,可以直接返回,
* 这意味着发射一个没有链接到任何槽的信号是及其快速的 */
if (!sender->d_func()->isSignalConnected(signal_index))
return;
/* ... 跳过一些Debug和QML钩子、正常检测代码 ... */
/* 锁定互斥对象, 保证对容器的所有操作都是线程安全的 */
QMutexLocker locker(signalSlotLock(sender));
/* 获取该signal的ConnectionList. 简化了一些检测性代码 */
QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;
const QObjectPrivate::ConnectionList *list =
&connectionLists->at(signal_index);
QObjectPrivate::Connection *c = list->first;
if (!c) continue;
// 检查这段期间有没有新添加而在发射过程中没有发射的信号
QObjectPrivate::Connection *last = list->last;
/* 对每个槽函数进行迭代 */
do {
if (!c->receiver)
continue;
QObject * const receiver = c->receiver;
const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;
// 决定该链接是立即发送还是放入事件队列
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {
/* 拷贝参数然后放入事件 */
queued_activate(sender, signal_index, c, argv);
continue;
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
/* ... 跳过 ... */
continue;
}
/* Helper struct that sets the sender() (and reset it backs when it
* goes out of scope */
QConnectionSenderSwitcher sw;
if (receiverInSameThread)
sw.switchSender(receiver, sender, signal_index);
const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
const int method_relative = c->method_relative;
if (c->isSlotObject) {
/* ... 跳过.... Qt5-style 链接至函数指针 */
} else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
/* 如果我们有一个 callFunction (由moc生成指向 qt_static_metacall的指针)
* 我们可以直接调用它. 我们同样需要检查保存的 metodOffset是否仍然有效
* (可能在此之前被析构) */
locker.unlock(); // 调用该函数时我们不能保持锁定
callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
locker.relock();
} else {
/* 动态对象的反馈 */
const int method = method_relative + c->method_offset;
locker.unlock();
metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
locker.relock();
}
// 检查该对象是否未被槽删除
if (connectionLists->orphaned) break;
} while (c != last && (c = c->nextConnectionList) != 0);
}
一路追踪这里的callFunction()和metacall(),结果发现它们都调用了QObject::qt_metacall(),而在qobjectdefs.h文件中我们看到:
virtual int qt_metacall(QMetaObject::Call, int, void **);
恩,这是个虚函数,也就是说,最后的调用都回到了moc为我们创建的那个qt_metacall()函数。
因为我们的Tv类写得很简单,所以生成的qt_metacall()也很简短,qt_metacall()则调用了qt_static_metacall()来触发我们声明的槽函数onStateChanged():
void Tv::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
Tv *_t = static_cast<Tv *>(_o);
switch (_id) {
case 0: _t->onStateChanged((*reinterpret_cast< bool(*)>(_a[1]))); break;
default: ;
}
}
}
int Tv::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 1)
qt_static_metacall(this, _c, _id, _a);
_id -= 1;
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 1)
*reinterpret_cast<int*>(_a[0]) = -1;
_id -= 1;
}
return _id;
}
到这里应该差不多了,总结一下。我们在上篇博文中实现的sigslot机制已经能够比较好地实现两个组件之间的解耦,但是缺点是设计库时需要针对不同参数数量的信号与链接需要重复编码,槽函数必须继承一个共同的基类等。
而Qt的信号槽机制建立在其庞大的元对象体系之上,由于其信号与槽函数的参数类型可以随时随地查到,因此在传参时可以仅仅传递一个void*类型的指针,然后通过虚函数机制调用为被调类写好的qt_matecall(),就很容易对参数反向解析从而调用相应的槽函数了。基本上是以一定的性能损失换来了更高的灵活性,也算是各有千秋吧。Boost.signal现在还没有用过,到时候接触下再做个比较相信会更加清晰。(^_^)