接上文:从零开始实现信号槽机制:一
关于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"
#define slots #define signals public // Qt5 中由 protected 改为 public 以支持更多特性 #define emit #define SLOT(a) "1"#a #define SIGNAL(a) "2"#a
QObject::connect(&btn, "2click(bool)", &tv, "1onStateChanged(bool)");
// SIGNAL 0 void Button::click(bool _t1) { void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 0, _a); }
好的,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; } } };
我们通常用来建立连接的connect()函数声明如下(Qt5支持了更多重载类型):
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
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现在还没有用过,到时候接触下再做个比较相信会更加清晰。(^_^)