当前位置: 首页 > 工具软件 > MOC > 使用案例 >

QT信号槽原理(二)moc代码中的信号槽部分

南门飞扬
2023-12-01

前言

要弄清楚信号槽原理,必须得了解qt moc机制,本文我们重点解析信号槽部分,qt元数据不深入。
QT自己定义了很多关键字,像signals、emit等,这些关键字是qt层面为一些特性创建的,c++编译器根本不认识,所以就有了moc,全称Meta-Object Compiler,直译是元对象编译器,它负责将qt处理qt自己的关键字,自动生成C++编译器认识的代码。

moc代码详解

下面我们会通过一个例子来分析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宏

实际上emit是个宏,定义如下:

# define emit

这样像上面的test方法的真正定义就变成下面这样:

void MocTest::test()
{
    testSignal("xiao", 11);
}

相当于直接调用了信号函数,但按之前的了解,信号是不需要给出定义的,那qt是怎么处理的呢?
答案就是qt的moc机制为信号生成了函数定义。

moc后代码

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函数。

QMetaObject::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

doactive

这个函数很大,我们只选重要的看,这里分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

第四步:处理queue和blockqueue连接方式

这块代码也比较多,分两块看:

  • 情况一:如果是1)auto方式且发送者接收者不在同一线程,2)queue方式
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_index, c, argv);
                continue;

则调用queued_activate进行信号的处理。
queued_activate的流程主要是3步:

  1. 检查信号类型是否满足queue方式(是否是QMetaType或者用qRegisterMetaType注册过),若不满足则直接结束。
  2. 构造QMetaCallEvent,将信号参数全部克隆(通过QMetaType::create)一份到event中保存。
  3. 将事件post给接收者。

这里post完后queued_activate流程就结束了。

  • 情况二:如果是blockqueue方式
 } 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;
}

这种方式的处理流程如下:

  1. 定义一个信号量用于block
  2. 构造一个QMetaCallEvent,传入信号参数和信号量
  3. 将QMetaCallEvent post给接收者
  4. 通过semaphore.acquire()等待调用完成(在接收者处理完事件后会调用semaphore.release来使这里的acquire返回)。

这里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);
	...
}

(为了精简流程,上面的代码有些许调整,整体流程不变)
可以看到:

  1. 如果接收者是槽函数,则直接通过槽对象的call方法去调用reciver的槽函数。
  2. 如果是invoke函数,则直接调用函数。
  3. 其他情况(比如是个信号)则调用QMetaObject::metacall去处理(对于信号,metacall最终是调用moc出来的信号函数)。

到这里,一个信号的发射过程全部结束了。

补充

QMetaCallEvent的处理

上面提到的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)。

QMetaObject::metacall

这块会在专门讲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函数)。

 类似资料: