c++ Qt信号槽原理
1、说明
使用Qt已经好几年了,一直以为自己懂Qt,熟悉Qt,使用起来很是熟练,无论什么项目,都喜欢用Qt编写。但真正去看Qt的源码,去理解Qt的思想也就近两年的事。
本次就着重介绍一下Qt的核心功能--信号槽机制,相信接触过Qt的人都能很熟悉地使用,甚至,大部分人还能轻松地说出信息槽的几种用法。但是信号槽的核心可不是简单说说就能说清楚的。
那么,本次,就从Qt的源码中讲解一下信号槽的机制。
其实,直到写这篇文章,我也没有完全看明白相关的源码,只是明白了其中的大部分以及使用机制,其中还有很多细节的,留待以后整理。
如果错误还请大家指正。
2、环境以及知识点
Qt版本:Qt 5.5.1
系统:windows 10
在阅读本文前,希望你能:
- 熟练使用C++,了解make的编译方法和过程;
- 熟练使用Qt的信号槽功能,对信号槽的写法以及4和5的区别了如指掌;
- QMetaObject元数据系统;
- 懂一些设计模式,能理解观察者模式;
3、信号槽源码分析
以下将按照SIGNAL/SLOT宏定义连接信号槽的方式做讲解
接下来将会从按照以下的步骤来进行分析:
- Qt元数据系统;
- moc预编译;
- Q_OBJECT宏;
- signals和slots关键字以及emit;
- SIGNAL()和SLOT()宏;
- connect 方法;
- 触发信号;
3.1、Qt的元数据系统
没看过Qt源码的同学可能会对QMetaObject有些陌生,我们打开Qt手册,查看此类的说明,介绍如下:
The QMetaObject class contains meta-information about Qt objects.
The Qt Meta-Object System in Qt is responsible for the signals and slots inter-object communication mechanism, runtime type information, and the Qt property system. A single QMetaObject instance is created for each QObject subclass that is used in an application, and this instance stores all the meta-information for the QObject subclass. This object is available as QObject::metaObject().
这里是说,QMetaObject包含了Qt的元对象信息。元对象机制类似Java的反射机制。通过继承QObject,并在定义类是添加一定Qt内置宏,能在运行时动态获取Qt的信号槽、类型信息以及相关属性。
一个简答的例子
void MainWindow::onClickButton() { qDebug()<<"on click button"; const QMetaObject* metaObject = this->metaObject(); qDebug()<<metaObject->className(); qDebug()<<metaObject->superClass()->className(); int methodIndex = metaObject->indexOfMethod("testFunction()"); qDebug()<<methodIndex; qDebug()<<metaObject->method(methodIndex).name(); metaObject->method(methodIndex).invoke(this); QMetaObject::invokeMethod(this, "testFunction"); }
如上,一个简单的例子,通过QMetaObject,我们得到了该对象的类名、父类名、方法并调用了该方法
怎么样,熟悉Java的小伙伴已经发现了,这不就是Java的反射吗,谁说C++没有反射呢
那么,Qt是如何实现”反射“的呢?答案是使用moc预编译
3.2、moc编译
moc全称Meta-Object Compiler,即元对象编译器。我们可以在Qt的安装目录的bin文件下看到moc工具,moc.exe。Qt的构建的时候,会调用该工具生成moc文件,我们在编译目录下看到的moc_xxx.cpp文件就是该工具生成的。
Qt的MinGW版本使用的是qmake进行项目管理,它和cmake功能类似,但没有后者强大。使用qmake生成Makefile后,我们打开Makefile文件,我们可以狠清楚地看到有一个调用moc.exe工具的地方,代码太多,就不列出来了。
此外,我们还发现,并不是所有的代码都会生成moc_xxx.cpp文件的,只有使用了 Q_OBJECT 宏的类文件,才会生成。没有错,moc工具就是根据 Q_OBJECT 宏来生成moc_xxx.cpp文件的,而实现“反射”的元数据系统的也是依靠Q_OBJECT的。
到此,我们其实已经能够大概理清qmake项目的构建步骤了。步骤和常用的cmake项目类似,区别就是,qmake生成的Makefile文件种,会写有调用moc工具的指令,以达到moc_xxx.cpp文件的生成。
我们可以使用moc工具手动生成moc_xxx.cpp,使用指令 moc.exe mainwindow.h,即会在控制台打印moc文件信息,也可以使用 -o 参数来将生成的内容写入文件,其余参数可以使用 moc.exe -h 来查看
3.3、Q_OBJECT
我们可以从源代码中查看 Q_OBJECT 的内容,这里调整一个格式,使用 Q_OBJECT 宏之后,会在类定义的开头多出以下代码:
public: Q_OBJECT_CHECK QT_WARNING_PUSH Q_OBJECT_NO_OVERRIDE_WARNING static const QMetaObject staticMetaObject; virtual const QMetaObject *metaObject() const; virtual void *qt_metacast(const char *); virtual int qt_metacall(QMetaObject::Call, int, void **); QT_WARNING_POP QT_TR_FUNCTIONS private: Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); struct QPrivateSignal {};
可以看到,这里多出几个方法和一些变量
- 属性staticMetaObject,元数据对象,可以从中获取当前类的元数据;
- 方法metaObject(),获取元数据对象指针,大多数情况下,返回staticMetaObject指针;
- 方法qt_metacast(),原数据对象类型转换,转换成指定的类型,使用时一般传入父类的名称字符串;
- 方法qt_metacall(),执行函数的回调,信号触发;
- 方法qt_static_metacall(),回调函数,被qt_metacall()调用,内部执行槽;
这里的几个方法都没有实现体,因为实现部分会有 moc 工具生成,在moc_xxx.cpp 文件中可以查看这些方法的实现体
3.4、signals和slots
signals 用于声明自定义信号,slots 用于声明槽函数,emit 用于发送信号,我们可以从源码中查看这三个宏定义
define slots define signals public define emit
可以看出,这三个宏几乎什么都没有做,signals 就是声明所谓的信号是public方法,而slots和emit更是为空,标准C++在编译的时候,根本不受这三个宏的影响,那么它们的用处在哪里呢?在moc工具调用和connect连接的时候。
打开moc_xxx.cpp文件,对比查看信号
signals: void clickButton(int value); void clickButton2();
// SIGNAL 0 void MainWindow::clickButton(int _t1) { void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 0, _a); } // SIGNAL 1 void MainWindow::clickButton2() { QMetaObject::activate(this, &staticMetaObject, 1, Q_NULLPTR); }
其实,信号就是方法,而 emit clickButton() 发送信号,就是调用 clickButton() 方法,换言之,触发信号,就算不要emit也无妨
3.5、SIGNAL()和SLOT()
查看源码
# define SLOT(a) qFlagLocation("1"#a QLOCATION) # define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
qFlagLocation() 源码如下:
const char *qFlagLocation(const char *method) { QThreadData *currentThreadData = QThreadData::current(false); if (currentThreadData != 0) currentThreadData->flaggedSignatures.store(method); return method; }
store() 方法
void store(const char* method) { locations[idx++ % Count] = method; }
所以 SIGNAL(clickButton()) 宏展开为 qFlagLocation("2"clickButton(int) QLOCATION)
SLOT() 同理,这里的1和2,最后会添加到信号槽的前面,其实是为了区分信号和槽,源码中还有一个0在 METHOD() 宏
qFlagLocation 方法的作用是将信号槽转换成字符串保存起来,store 方法中,locations是个二维数组,而 idx 每次都加一,保证信号和槽的不同的方法存储在不同的数组中。
我们也可以在代码中打印出来看下:
qDebug()<<SIGNAL(clickButton(int)); //2clickButton(int) qDebug()<<SLOT(onClickButton()); //1onClickButton()
3.6、connect方法
最后,就是最关键的connect方法,做了一些简单的注释
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type) { if (sender == 0 || receiver == 0 || signal == 0 || method == 0) { qWarning("QObject::connect: Cannot connect %s::%s to %s::%s", sender ? sender->metaObject()->className() : "(null)", (signal && *signal) ? signal+1 : "(null)", receiver ? receiver->metaObject()->className() : "(null)", (method && *method) ? method+1 : "(null)"); return QMetaObject::Connection(0); } QByteArray tmp_signal_name; if (!check_signal_macro(sender, signal, "connect", "bind")) return QMetaObject::Connection(0); const QMetaObject *smeta = sender->metaObject();//获取发送者的元数据对象 const char *signal_arg = signal;//信号 ++signal; //skip code QArgumentTypeArray signalTypes;//信号参数类型数组 Q_ASSERT(QMetaObjectPrivate::get(smeta)->revision >= 7); //信号转换为签名,并得到信号参数类型数组 QByteArray signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes); //找到信号索引 int signal_index = QMetaObjectPrivate::indexOfSignalRelative( &smeta, signalName, signalTypes.size(), signalTypes.constData()); //小于0表示,表示信号索引有问题 if (signal_index < 0) { // check for normalized signatures //将信号重新规范化,再进行上面的签名转换,并重新得到索引 tmp_signal_name = QMetaObject::normalizedSignature(signal - 1); signal = tmp_signal_name.constData() + 1; //重新进行签名转换,并得到参数类型列表 signalTypes.clear(); signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes); smeta = sender->metaObject(); signal_index = QMetaObjectPrivate::indexOfSignalRelative( &smeta, signalName, signalTypes.size(), signalTypes.constData()); } //重新获取的信号索引还是无效,则是头文件中信号的定义出错,找不到信号,报错信号不存在 if (signal_index < 0) { err_method_notfound(sender, signal_arg, "connect"); err_info_about_objects("connect", sender, receiver); return QMetaObject::Connection(0); } //根据当前信号的索引找到最原始的信号的索引,因为信号是可以被继承,这里找的祖先信号 signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index); signal_index += QMetaObjectPrivate::signalOffset(smeta);//信号的索引再加上信号的偏移量 QByteArray tmp_method_name; //提取槽的编码,应该是QSLOT_CODE或者QSIGNAL_CODE,用于判断槽是信号还是方法 int membcode = extract_code(method); //检查槽编码,槽可以是槽函数或者信号,初次以为,都无效 if (!check_method_code(membcode, receiver, method, "connect")) return QMetaObject::Connection(0); const char *method_arg = method; ++method; // skip code QArgumentTypeArray methodTypes; //转换槽签名,并获取槽的参数类型列表 QByteArray methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes); const QMetaObject *rmeta = receiver->metaObject();//获取接受者的元数据对象 int method_index_relative = -1; Q_ASSERT(QMetaObjectPrivate::get(rmeta)->revision >= 7); switch (membcode) { case QSLOT_CODE://接受者是槽函数 method_index_relative = QMetaObjectPrivate::indexOfSlotRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; case QSIGNAL_CODE://接受者是信号 method_index_relative = QMetaObjectPrivate::indexOfSignalRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; } //槽的索引为-1,表示无效 if (method_index_relative < 0) { // check for normalized methods //将槽进行规范化处理,并重新转换槽签名 tmp_method_name = QMetaObject::normalizedSignature(method); method = tmp_method_name.constData(); methodTypes.clear(); methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes); // rmeta may have been modified above //接受者元数据对象前面可能被修改过,这里重新获取 rmeta = receiver->metaObject(); //重新获取槽的索引 switch (membcode) { case QSLOT_CODE: method_index_relative = QMetaObjectPrivate::indexOfSlotRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; case QSIGNAL_CODE: method_index_relative = QMetaObjectPrivate::indexOfSignalRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; } } //如果还找不到,则说明槽定义有误,报错 if (method_index_relative < 0) { err_method_notfound(receiver, method_arg, "connect"); err_info_about_objects("connect", sender, receiver); return QMetaObject::Connection(0); } //检查信号和槽的参数 if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(), methodTypes.size(), methodTypes.constData())) { qWarning("QObject::connect: Incompatible sender/receiver arguments" "\n %s::%s --> %s::%s", sender->metaObject()->className(), signal, receiver->metaObject()->className(), method); return QMetaObject::Connection(0); } int *types = 0; //队列连接检查,参数要是基本类型,或者使用元数据注册 if ((type == Qt::QueuedConnection) && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) { return QMetaObject::Connection(0); } #ifndef QT_NO_DEBUG //打印调试信息 QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index); QMetaMethod rmethod = rmeta->method(method_index_relative + rmeta->methodOffset()); check_and_warn_compat(smeta, smethod, rmeta, rmethod); #endif QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect( sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types)); return handle; } 方法代码很多很杂,但无非就是检查信号槽的格式,获取参数列表, 最后保存起来 QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection); c->sender = s; //发送者对象 c->signal_index = signal_index; //信号索引 c->receiver = r; //接受者对象 c->method_relative = method_index; //槽索引 c->method_offset = method_offset; //槽偏移 c->connectionType = type; //连接方式 c->isSlotObject = false; c->argumentTypes.store(types); c->nextConnectionList = 0; c->callFunction = callFunction;//静态回调函数 //在发送者元数据内加上连接信息 //信号发送者的对象内存中保存了连接的信息,包括槽的对象,槽地址,连接方式等 QObjectPrivate::get(s)->addConnection(signal_index, c.data());
3.7、触发信号
这时候再回过头来看3.4中的信号触发,我们知道,emit信号就是调用moc文件中的方法,方法的核心就是 QMetaObject::activate()
直接看该方法中调用槽函数的一段
//因为一个信号可能连接多个槽,这里循环遍历链表进行调用 do { QObjectPrivate::Connection *c = list->first; if (!c) continue; // We need to check against last here to ensure that signals added // during the signal emission are not emitted in this emission. QObjectPrivate::Connection *last = list->last; do { if (!c->receiver) continue; QObject * const receiver = c->receiver; const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId; // determine if this connection should be sent immediately or // put into the event queue //直接连接并且发送和接受不再一个线程中,或者队列连接,则放入事件队列中 //可知,直接连接并且发送和接受不在同一个线程,则效果和队列连接相同 if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread) || (c->connectionType == Qt::QueuedConnection)) { queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker); continue; #ifndef QT_NO_THREAD //阻塞式队列连接 } else if (c->connectionType == Qt::BlockingQueuedConnection) { locker.unlock(); //在同一个线程,则报错 if (receiverInSameThread) { qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: " "Sender is %s(%p), receiver is %s(%p)", sender->metaObject()->className(), sender, receiver->metaObject()->className(), receiver); } QSemaphore semaphore;//资源计数器,avail为0 QMetaCallEvent *ev = c->isSlotObject ? new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) : new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore); //根据连接信息构造一个事件,并添加到接受者的 事件队列中 QCoreApplication::postEvent(receiver, ev); //信号发送者的线程阻塞,acquire资源数为1,>avail(0),这里阻塞 //当槽执行玩之后释放,这里的avail才会增加,阻塞结束 semaphore.acquire(); locker.relock(); continue; #endif } 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) { c->slotObj->ref(); QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj); locker.unlock(); obj->call(receiver, argv ? argv : empty_argv); // Make sure the slot object gets destroyed before the mutex is locked again, as the // destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool, // and that would deadlock if the pool happens to return the same mutex. obj.reset(); locker.relock(); } else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) { //we compare the vtable to make sure we are not in the destructor of the object. locker.unlock(); const int methodIndex = c->method(); if (qt_signal_spy_callback_set.slot_begin_callback != 0) qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv); callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv); if (qt_signal_spy_callback_set.slot_end_callback != 0) qt_signal_spy_callback_set.slot_end_callback(receiver, methodIndex); locker.relock(); } else { const int method = method_relative + c->method_offset; locker.unlock(); if (qt_signal_spy_callback_set.slot_begin_callback != 0) { qt_signal_spy_callback_set.slot_begin_callback(receiver, method, argv ? argv : empty_argv); } metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv); if (qt_signal_spy_callback_set.slot_end_callback != 0) qt_signal_spy_callback_set.slot_end_callback(receiver, method); locker.relock(); } if (connectionLists->orphaned) break; } while (c != last && (c = c->nextConnectionList) != 0); if (connectionLists->orphaned) break; } while (list != &connectionLists->allsignals && //start over for all signals; ((list = &connectionLists->allsignals), true));
由上面代码,我们大概可以理解信号槽的几种连接方式:
- 默认连接并且信号槽的对象不在同一个线程中,则效果和队列连接类似;
- 阻塞时队列连接,信号和槽对象不同处于同一个线程中;
- Qt使用QSemaphore来实现阻塞式的槽函数调用;
4、小结
本次的源码因为种种原因,看的不是很详细,但是理解Qt的信号槽机制绰绰有余了
- Qt自带的元数据系统利用C++的宏等特性实现反射机制;
- 利用元数据系统,在连接信号槽是将槽的信息(接收对象、槽方法、参数列表、连接方式等)保存在信号的元数据中;
- 信号也是方法,方法体有moc工具生成,方法内获取该信号连接的所有槽信息,并依序执行;
直到这里,信号槽的逻辑已经显而易见了,它就是一个变种的观察者模式,槽的信息保存在信号对象中也就是设置回调函数,触发信号也就是执行回调函数,只是Qt库将其中的各种操作细节封装起来了,所以,使用起来,不去关注设计模式的细节,也就容易很多了。不得不说,无论是从设计思路,还是开发技巧上看,Qt的开发者真的很牛叉。
5、第三方信号槽库
信号槽机制是Qt首创,但不是其独有,其他各类C++流行框架也都是互相借鉴,C++标准库的预备役的boost中也有信号槽机制的实现。如果平时开发中需要用到信号槽机制,但是又不想引入这些庞大的类库,可以使用轻量级别的信号槽库:http://sigslot.sourceforge.net,该库不详细介绍,有兴趣的小伙伴自己学习把。
以上就是c++ Qt信号槽原理的详细内容,更多关于c++ Qt信号槽的资料请关注我们其它相关文章!