要弄清楚信号槽原理,必须得了解qt moc机制,本文我们重点解析信号槽部分,qt元数据不深入。
QT自己定义了很多关键字,像signals、emit等,这些关键字是qt层面为一些特性创建的,c++编译器根本不认识,所以就有了moc,全称Meta-Object Compiler,直译是元对象编译器,它负责将qt处理qt自己的关键字,自动生成C++编译器认识的代码。
下面我们会通过一个例子来分析moc后的代码。
class MocTest : public QObject {
Q_OBJECT
public:
explicit MocTest(QObject* parent = nullptr);
void test();
~MocTest() {}
signals:
void testSignal(const QString& name, int age);
public slots:
void testSlot(const QString& name, int age);
};
void MocTest::test()
{
emit testSignal("xiao", 11);
}
void MocTest::testSlot(const QString& name, int age)
{
qDebug() << "test slot" << name << age;
}
源代码有一个信号testSignal
、一个槽testSlot
和一个普通函数test
。
我们知道,槽函数需要我们给出函数定义,但是信号只需要声明即可,我们还知道要通过emit关键字去发射信号。
实际上emit是个宏,定义如下:
# define emit
这样像上面的test
方法的真正定义就变成下面这样:
void MocTest::test()
{
testSignal("xiao", 11);
}
相当于直接调用了信号函数,但按之前的了解,信号是不需要给出定义的,那qt是怎么处理的呢?
答案就是qt的moc机制为信号生成了函数定义。
moc代码的信息实际是比较多的,本文我们只关注信号槽部分
// SIGNAL 0
void MocTest::testSignal(const QString & _t1, int _t2)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
看到没,实际信号也是一个函数,只是函数定义是qt用moc机制生成的(代码中的staticMetaObject变量是本类的元对象),重点是QMetaObject::activate
和第三个参数0
。
QMetaObject::activate
这个函数是用来激活信号的,下面会详细讲。
在上一篇文章QT信号槽原理(一)connect函数我们说到获取信号和槽的index,这个0
就表示一个index,这里就代表testSignal
这个信号的index,你得告诉activate
函数要激活哪个信号不是。
在这函数中,将信号发送者、信号函数的参数和index都传给了activate函数。
定义如下:
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
void **argv)
{
int signal_index = local_signal_index + QMetaObjectPrivate::signalOffset(m);
if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
doActivate<true>(sender, signal_index, argv);
else
doActivate<false>(sender, signal_index, argv);
}
里面内容很少,计算了index后,就是调用doActivate
。
这个函数很大,我们只选重要的看,这里分5个步骤。
QObjectPrivate::ConnectionDataPointer connections(sp->connections.loadRelaxed());
QObjectPrivate::SignalVector *signalVector = connections->signalVector.loadRelaxed();
const QObjectPrivate::ConnectionList *list;
if (signal_index < signalVector->count())
list = &signalVector->at(signal_index);
else
list = &signalVector->at(-1);
根据传入的信号index获取该index代表的信号关联的所有connection列表。
QObjectPrivate::Connection *c = list->first.loadRelaxed();
if (!c)
continue;
do {
...
} while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);
这里就是一个简单的链表遍历。
其实在前面有一个inSenderThread
局部变量表示当前是否在发送者对象所在的线程:
Qt::HANDLE currentThreadId = QThread::currentThreadId();
bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData->threadId.loadRelaxed();
然后就是判断发送者接收者是否在同线程。
bool receiverInSameThread;
if (inSenderThread) {
receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
} else {
// need to lock before reading the threadId, because moveToThread() could interfere
QMutexLocker lock(signalSlotLock(receiver));
receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
}
主要就是通过发送者和接收者线程id的比较。
我们知道在连接方式为auto时是需要根据发送者和接收者是否在同一线程来决定是使用直接连接还是队列连接,那时就要用到receiverInSameThread
。
这块代码也比较多,分两块看:
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {
queued_activate(sender, signal_index, c, argv);
continue;
则调用queued_activate进行信号的处理。
queued_activate的流程主要是3步:
这里post完后queued_activate流程就结束了。
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
QSemaphore semaphore;
{
QBasicMutexLocker locker(signalSlotLock(sender));
if (!c->receiver.loadAcquire())
continue;
QMetaCallEvent *ev = c->isSlotObject ?
new QMetaCallEvent(c->slotObj, sender, signal_index, argv, &semaphore) :
new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction,
sender, signal_index, argv, &semaphore);
QCoreApplication::postEvent(receiver, ev);
}
semaphore.acquire();
continue;
}
这种方式的处理流程如下:
这里post后会阻塞直到事件处理完成。
需要说明的是,在构造QMetaCallEvent时,对于信号连接到槽或者另一个信号,event构造的参数是不同的:
QMetaCallEvent *ev = c->isSlotObject ?
new QMetaCallEvent(c->slotObj, sender, signal, nargs) :
new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs);
队列连接在第四步处理完,剩下的就是直接连接了,直接连接就是直接调用。
if (c->isSlotObject) {
...
c->slotObj->call(receiver, argv);
} else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
...
Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
...
} else {
...
Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
...
}
(为了精简流程,上面的代码有些许调整,整体流程不变)
可以看到:
到这里,一个信号的发射过程全部结束了。
上面提到的QMetaCallEvent的处理过程这里额外再看下。
QObject处理QMetaCallEvent的源代码如下:
case QEvent::MetaCall:
{
QAbstractMetaCallEvent *mce = static_cast<QAbstractMetaCallEvent*>(e);
if (!d_func()->connections.loadRelaxed()) {
QBasicMutexLocker locker(signalSlotLock(this));
d_func()->ensureConnectionData();
}
QObjectPrivate::Sender sender(this, const_cast<QObject*>(mce->sender()), mce->signalId());
mce->placeMetaCall(this);
break;
}
实际调用的是QMetaCallEvent的placeMetaCall函数,我们看下它的源码:
void QMetaCallEvent::placeMetaCall(QObject *object)
{
if (d.slotObj_) {
d.slotObj_->call(object, d.args_);
} else if (d.callFunction_ && d.method_offset_ <= object->metaObject()->methodOffset()) {
d.callFunction_(object, QMetaObject::InvokeMetaMethod, d.method_relative_, d.args_);
} else {
QMetaObject::metacall(object, QMetaObject::InvokeMetaMethod,
d.method_offset_ + d.method_relative_, d.args_);
}
}
大家有没有发现,这块代码和上面提到的第五步:处理直接连接
基本是一样的。所以这块最终执行的东西是一样的,只是队列方式多了个发送事件的过程而已。
但是,blockqueue方式的用于阻塞的信号量在哪里去release的呢?找啊找,原来在下面这里:
QAbstractMetaCallEvent::~QAbstractMetaCallEvent()
{
#if QT_CONFIG(thread)
if (semaphore_)
semaphore_->release();
#endif
}
QMetaCallEvent构造函数中的semaphore最终是保存到基类QAbstractMetaCallEvent中,待对象析构时自动调用release,这样就可以是信号量的acquire退出阻塞,很巧妙(我还以为是手动去release)。
这块会在专门讲moc的文章中去详细讲,这里简要说下。
QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv)
实际是调用
receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv)
qt_metacall是moc机制生成的函数,其中第一个参数如果是InvokeMetaMethod,则等效于下面:
qt_static_metacall(this, QMetaObject::InvokeMetaMethod, method, argv);
method就是我们经常提到的index,这里就根据method去调用对应的函数(信号函数、槽函数或者invoke函数)。