前言
Qt中的信号和槽机制想必是Qt开发者再熟悉不过的机制了,但是在某个上班摸鱼的日子我做了一个示例,才发现其中还是有很多的坑,接下来主要通过源码的简单解析记录一下参数传递相关特性供参考。示例代码如下:
class HttpResultPackgeTest : public QObject
{
Q_OBJECT
public:
HttpResultPackgeTest(const HttpResultPackgeTest &){qDebug() << "HttpResultPackgeTest(const HttpResultPackgeTest &)"}
HttpResultPackgeTest(){qDebug() << "HttpResultPackgeTest()"}
public:
int httpCode = 0;
QString httpErrorString;
QByteArray httpData;
};
Q_DECLARE_METATYPE(HttpResultPackgeTest)
class Sender : public QObject
{
Q_OBJECT
signals:
void sigSendMess(const HttpResultPackgeTest& packge);
}
class Reciever: public QObject
{
Q_OBJECT
public slots:
void slotRecieveMess(const HttpResultPackgeTest& packge);
}
void MainWindow::test()
{
connect(sender, &Sender::sigSendMess, reciever, &Reciever::slotRecieveMess, Qt::QueuedConnection);
}
上述代码示例是一个很简单的信号绑定,但是在实际运行时,sender发送信号后会打印一次HttpResultPackgeTest(const HttpResultPackgeTest &),即又构造了一次,这让我很困惑,经过几天的研究,我终于了解到它们的内部机制。
一、信号和槽连接
connect函数为QObject的静态函数,通常拥有5个参数,第5个参数主要是设置连接类型,主要有以下几种类型:
enum ConnectionType {
AutoConnection,
DirectConnection,
QueuedConnection,
BlockingQueuedConnection,
UniqueConnection = 0x80
};
具体作用就不放在这里说了,后续有空了会再发一篇针对源码分析连接类型的文章。在此我们只分析DirectConnection(直接连接)和QueuedConnection(队列连接)状态下参数的传递机制。
二、元对象
连接信号和槽需要一个前提:发送者和接收者都需要继承QObject和添加Q_OBJECT宏,使编译器能够生成元对象文件(_moc文件),且自定义参数类也要如此,以下内容为简单定义的参数类和发送类
//参数类
class HttpResultPackgeTest : public QObject
{
Q_OBJECT
public:
int httpCode = 0;
QString httpErrorString;
QByteArray httpData;
};
Q_DECLARE_METATYPE(HttpResultPackge)
//发送者
class HttpRequest : public QObject
{
Q_OBJECT
//......
signals:
void signalTest(const HttpResultPackgeTest & packge);
};
编译后生成_moc文件,打开后可以在文件中找到这几句话:
void HttpRequest::signalTest(const HttpResultPackgeTest & _t1)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 1, _a);
}
信号发送时,实际就是执行的这两句话,第一句话为构造void*指针数组,且将参数放入第二个元素当中;第二句话是才是关键,再往里面跟进:
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
//......
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.load();
// 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;
#if QT_CONFIG(thread)
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
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;
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);
locker.unlock();
semaphore.acquire();
locker.relock();
continue;
}
//......
} while (c != last && (c = c->nextConnectionList) != 0);
if (connectionLists->orphaned)
break;
} while (list != &connectionLists->allsignals &&
//start over for all signals;
((list = &connectionLists->allsignals), true));
}
}
以上为qobject.cpp中的源码,QMetaObject中的activate还是比较复杂的,所以我对代码进行了精简,保留了重要部分,感兴趣的可以看一看源码。对代码简单分析就能看到队列连接的条件上还标注有注释“// put into the event queue”,这也是队列连接参数传递的方法。
三、队列连接
当条件判断为队列连接后将进入queued_activate函数,同时顺带也发现阻塞连接是通过信号量来实现的。继续跟进queued_activate函数,这里也对queued_activate函数的代码做了部分精简:
static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv,
QMutexLocker &locker)
{
//......
if (nargs > 1) {
for (int n = 1; n < nargs; ++n)
types[n] = argumentTypes[n-1];
locker.unlock();
for (int n = 1; n < nargs; ++n)
args[n] = QMetaType::create(types[n], argv[n]);
locker.relock();
//......
}
QMetaCallEvent *ev = c->isSlotObject ?
new QMetaCallEvent(c->slotObj, sender, signal, nargs, types, args) :
new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs, types, args);
QCoreApplication::postEvent(c->receiver, ev);
}
由上述代码分析可知,队列连接的所有参数都是通过create函数创建的,且信号的发送是通过事件循环传递,下面再来简单更近一下create函数
void *QMetaType::create(int type, const void *copy)
{
QMetaType info(type);
if (int size = info.sizeOf())
return info.construct(operator new(size), copy);
return 0;
}
该函数主要通过元对象中包含的信息来构造一个新的对象出来,因此这也是为什么需要使用Q_DECLARE_METATYPE注册MetaType的原因,并且需要保留被拷贝参数的内容,所以它会调用拷贝构造来构造出一个新的对象传递给槽函数。
了解到原理后得出总结:在实际开发当中,这个机制对性能有很大的影响,假如有一个占用内存很大的类做为信号和槽的参数并且使用了队列连接,那么对程序的整体性能和内存占用是有一定影响的。如果想要避免,在这里也有我通常使用的两种解决方法:
1.尽量使用QT原生类,如QString、QByteArray等,因为他们都有隐式共享机制,不存在深拷贝的情况,但也要结合实际使用,不能被套在里面了。
2.学习刚刚_moc文件代码中参数传递的方法,将参数转换为void*,然后再在槽函数中强转回去。但需要考虑对象的存储地址,比如在栈内存中的对象就不适用于此方法传递。
二、直接连接
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
//......
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 {
//......
QConnectionSenderSwitcher sw;
if (receiverInSameThread) {
sw.switchSender(receiver, sender, signal_index);
}
if (c->isSlotObject) {
//connect中SLOT宏写法
//......
} else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
//connect中&Class::slotMethod写法
const int methodIndex = c->method();
const int method_relative = c->method_relative;
const auto callFunction = c->callFunction;
locker.unlock();
if (qt_signal_spy_callback_set.slot_begin_callback != 0)
qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv);
{
Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
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();
}
//......
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));
}
}
上述代码为queued_activate的后续代码,也能非常简单的发现:
- 直接调用源代码中也区分了几种情况,主要用于区别connect上的写法区别(SLOT宏、&Sender::sigSendMess、lambda)
- 调用原理就是通过回调的方法实现,因此参数并没有进行拷贝,但是具体如何实现的需要后续再更深入的分析
总结
以上就是今天要讲的内容,本文仅仅简单通过Qt源码对信号和槽的参数传递进行理解。