Qt 信号槽原理 - 槽函数调用时机

前言

因为之前学习 Qt 多线程时,使用信号槽发现槽函数没有执行,想到我还完全不清楚槽函数的调用时机,于是心血来潮翻了一下这部分的源码,现在记录顺便复习一下。学习一下后发现,当时信号调用是在主线程,也就是 UI 线程,接收对象在另一个新开的线程里,使用 moveToThread 把它移过去了,那个线程有正在执行的函数,我在主线程调用信号,会异步投递一个槽函数相关的事件过去,这个事件需要那个线程回到 eventLoop 中才能处理,而那个线程还在执行一个函数,还没轮到这个事件!

本文主要介绍我所认知的槽函数的调用时机

一 Qt 元对象系统

在学习信号槽之前简单了解一下 Qt 的元对象系统(Meta-Object System)
Qt 的元对象系统提供了对象之间通信的信号槽机制、运行时类型信息和动态属性系统等
元对象系统由以下三个基础组成:

  • QObject 类是所有使用元对象系统的类的基类;
  • 在类中声明 Q_OBJECT 宏,使得类可以使用元对象系统特性,如信号槽;
  • MOC(元对象编译器)为每一个 QObject 的子类提供必要的代码来实现元对象系统特性

所以,自定义的类想要使用信号槽,必须继承自 QObject,同时在类中声明 Q_OBJECT 宏

二 创建测试工程

了解上面的东西后,我们先创建一个工程来体会一下

使用 QtCreator 正常创建一个桌面应用程序,我们添加一个自定义类,
添加自定义类

#ifndef OBJECT_H
#define OBJECT_H

#include <QObject>

class Object : public QObject
{
    Q_OBJECT
public:
    explicit Object(QObject *parent = nullptr);

signals:

};

#endif // OBJECT_H

添加时继承自 QObject,并勾选 Add Q_OBJECT,模板已经帮我们完成了前面两个工作,然后我们添加一些信号函数和槽函数,

#ifndef OBJECT_H
#define OBJECT_H

#include <QObject>

class Object : public QObject
{
    Q_OBJECT
public:
    explicit Object(QObject *parent = nullptr);
    
public slots:
    void click() {}
    void hide() {}
    
signals:
    void clicked(bool checked);
    void pressed();
    void sendMessage(const QString &msg, int id);
};

#endif // OBJECT_H

这里需要注意:

  • 信号函数必须声明在 signals 后面,不需要 public 那些访问限制的修饰,并且只需要声明,不需要实现;
  • 槽函数可以声明在 slots 后面,可以使用 public、private 等修饰,这样声明的槽函数就可以在 QObject::connect 函数中使用 SLOT 宏来指定槽。也可以使用 connect 的其他重载,直接使用成员函数指针或者 lambda 函数

然后我们点击左下角的构建项目,在可执行文件目录下,可以看到这些以 moc_ 开头的 cpp 文件,

这就是第三点,MOC 元对象编译器给每一个 QObject 子类生成的代码,这里再强调:

  • 这个类必须继承自 QObject,直接(我们的 Object 类)或间接(Widget)
  • 必须声明 Q_OBJECT 宏,如果没有这个宏,就不会生成对应的 moc_xxx.cpp

也就是说 Qt 项目的构建过程除了标准 C++ 程序的预处理、编译、链接,还有第 0 步,生成这些 Qt 特性的文件。

这里插一句,如果一开始没有继承 QObject,后面想使用信号槽,将类继承 QObject 和添加 Q_OBJECT 后,但出了下面的问题:

错误

解决方案是右键项目重新执行 qmake,然后构建就能成功了(我也不清楚是啥原因,但是你用 QtCreator 写 Qt 项目时,构建出问题了,实在没啥办法,可以试试删除 build 文件夹,改动一下 .pro 文件,加一个空格也行,重新执行 qmake,清除重新构建,或者重启 QtCreator等等)

三 逐步探索

好嘞,下面我们就从这个 moc_object.cpp 文件开始逐步探索吧!!!

1. qt_static_metacall

当然,里面很多东西我也看不懂,我们先看两处关键的代码,在 qt_static_metacall 函数中,

void Object::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<Object *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->clicked((*reinterpret_cast< bool(*)>(_a[1]))); break;
        case 1: _t->pressed(); break;
        case 2: _t->sendMessage((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2]))); break;
        case 3: _t->click(); break;
        case 4: _t->hide(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
		...
    }
}

可以看到里面的 switch 语句,是不是就是在调用我们的槽函数,同时还有信号函数,因为信号也能被另一个信号触发,即信号可以连接槽函数,也能连接信号函数。这样我们就能猜测到从信号函数被调用,到最终调用槽函数,应该就发生在这里!

2. 信号函数的实现

然后文件最下面就是我们在 signals 后面声明的三个信号函数,之前说了信号函数我们只需声明,不需要实现,自动生成的代码给我们实现了,下面我们来看看,一点不复杂,

// SIGNAL 0
void Object::clicked(bool _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void Object::pressed()
{
    QMetaObject::activate(this, &staticMetaObject, 1, nullptr);
}

// SIGNAL 2
void Object::sendMessage(const QString & _t1, int _t2)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))), const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t2))) };
    QMetaObject::activate(this, &staticMetaObject, 2, _a);
}

我们声明的三个信号函数,参数的个数不一样,声明的位置(顺序)不一样,这里就可以看出有什么区别了

  1. 注意到 // SIGNAL 0 这些带有编号的,这个很重要,可以看到函数里面的 QMetaObject::activate 的参数里面就带着这个编号,其实就是索引(从 0 开始的嘛),也就是信号函数它们被组织起来了,每个信号函数有对应的索引

  2. 它们的参数不同,函数中的 void *_a[ ] 也不同,第二个函数没有声明,其实就是 QMetaObject::activate 参数里面的 nullptr,这个_a[ ] 特点就是第一个元素是 nullptr,后面就是每一个参数的地址,这就是一个参数列表,是不是和 main 函数的参数 char *argv[ ] 比较像

3. QMetaObject::activate

最后就是这个 QMetaObject::activate,这里就要翻源码了,这里可以在线阅读很多 C/C++ 项目的源码 Code browser

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

再看一下参数:

  • sender,这个信号所属类的对象,发送者
  • m,在之前的 moc 文件中有这样一个定义,activate 函数里就是用这个对象
QT_INIT_METAOBJECT const QMetaObject Object::staticMetaObject = { {
    QMetaObject::SuperData::link<QObject::staticMetaObject>(),
    qt_meta_stringdata_Object.data,
    qt_meta_data_Object,
    qt_static_metacall,
    nullptr,
    nullptr
} };
  • local_signal_index,与信号函数相关的索引
  • argv,信号函数的参数列表

经过处理后,然后就是调用了一个模板函数 doActivate,这个函数有 170 来行,关键的内容就在这里面了

template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv)

4. doActivate

这个函数内容很多,牵扯的东西比较多,所以很多都看不懂,下面我就挑一些看得懂、关键的来介绍

template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv) {
//...
	void *empty_argv[] = { nullptr };
	if (!argv)
	    argv = empty_argv;
//...
}

之前我们有一个信号函数的参数为空,这里面就把那个参数列表给补上了,仍然是第一个元素是 nullptr

template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv) {
	// 获得发送对象的 d 指针
	QObjectPrivate *sp = QObjectPrivate::get(sender);
//...
	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);
//...
}

这里就稍微复杂了,简单总结就是,函数第一行获取发送对象 sender 的 d 指针,最后得到了一个 ConnectionList *list,一个单链表,每一个元素都是一个 Connection 结构体,就是通过 QObject::connect 函数连接信号和槽时得到的一个结构体

// ConnectionList is a singly-linked list
struct ConnectionList {
     QAtomicPointer<Connection> first;
     QAtomicPointer<Connection> last;
 };

struct Connection : public ConnectionOrSignalVector
{
	...
    // linked list of connections connected to signals in this object
    QAtomicPointer<Connection> nextConnectionList;
    Connection *prevConnectionList;
    QObject *sender;
    QAtomicPointer<QObject> receiver;
    QAtomicPointer<QThreadData> receiverThreadData;
    union {
        StaticMetaCallFunction callFunction;
        QtPrivate::QSlotObjectBase *slotObj;
    };
    ushort method_offset;
    ushort method_relative;
    signed int signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
    ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
    ...
};

一个 Connection 对象里面存储了很多重要信息

  • QObject *sender:发送对象 sender
  • signed int signal_index:与信号函数有关的 index
  • QAtomicPointer<QObject> receiver:接收对象 receiver
  • QAtomicPointer<QThreadData> receiverThreadData:接收对象所在线程的线程数据
  • StaticMetaCallFunction callFunction:和之前 moc 文件中的 qt_static_metacall 函数有关
  • ushort connectionType:QObject::connect 的第五个参数

既然得到了一个 Connection 链表,很容易想到下面就是对这个链表进行遍历然后调用与槽函数有关的函数了

先不急着遍历,有一个很重要的信息就是 connect 的第五个参数,连接类型,连接类型主要是考虑到信号函数调用时所在的线程,和接收对象所在线程的关系,下面一开始得到的 inSenderThread 表示当前调用这个信号函数的线程是不是在 sender 对象所在的线程,后面马上要用


Qt::HANDLE currentThreadId = QThread::currentThreadId();
bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData.loadRelaxed()->threadId.loadRelaxed();

// We need to check against the highest connection id to ensure that signals added
// during the signal emission are not emitted in this emission.
uint highestConnectionId = connections->currentConnectionId.loadRelaxed();
do {
    QObjectPrivate::Connection *c = list->first.loadRelaxed();
    if (!c)
        continue;

    do {
        QObject * const receiver = c->receiver.loadRelaxed();
        if (!receiver)
            continue;

        QThreadData *td = c->receiverThreadData.loadRelaxed();
        if (!td)
            continue;

        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();
        }

		...// 下面就是关键

    } while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);

} while (list != &signalVector->at(-1) &&
    //start over for all signals;
    ((list = &signalVector->at(-1)), true));

然后开始遍历,两个 do while 循环,主要看内层的循环(这个外层循环的 while 条件有点看不懂),得到一个 Connection 对象然后进行操作

然后得到接收对象相关的线程信息,还是先判断线程的关系,这里通过 inSenderThread 条件,考虑是否先上锁来获取 receiverInSameThread,表示接收对象 receiver 是否在这个信号函数调用所在的线程

  • 接收对象就在这个线程里,立即准备执行槽函数的相关函数
  • 接收对象不在这个线程里,则通过事件机制来做异步处理

下面就来看 Qt 是怎么处理的吧

...// 下面就是关键

// 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);
    continue;
#if QT_CONFIG(thread)
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
    ... // postEvent,向接收对象投递一个 QMetaCallEvent
    continue;
#endif
}
// ... 后面是同步调用槽函数

异步处理

连接类型默认是 Qt::AutoConnection,这里代码自己判断是不是在同一个线程,如果不在同一个线程,则需要通过事件机制来处理:

  • Qt::QueuedConnection:调用了 queued_activate,里面使用了 QCoreApplication::postEvent 投递一个 QMetaCallEvent 事件
  • Qt::BlockingQueuedConnection:直接在这里投递事件,并且使用了一个信号量来阻塞,直到槽函数执行完

先判断是否需要异步处理,处理完后 continue,继续下一个 Connection。如果不是异步处理,则在这里直接调用槽函数,接着往下看

同步处理

这里有一个 if else 分支,这里就简化看一下(具体的自己看源码吧),针对不同条件来处理,关键就这三个函数

// ... 后面是同步调用槽函数

if (c->isSlotObject) {
	...
	obj->call(receiver, argv);
	...
} else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
	...
	callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
	...
} else {
	...
	QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
	...
}

其中的 callFunctionQMetaObject::metacall 的参数不就和 moc 文件里的 qt_static_metacall 函数对应上了?!

int QMetaObject::metacall(QObject *object, Call cl, int idx, void **argv)
{
    if (object->d_ptr->metaObject)
        return object->d_ptr->metaObject->metaCall(object, cl, idx, argv);
    else
        return object->qt_metacall(cl, idx, argv);
}

可是这个函数名也没对上啊,原来,moc 文件中还有一个函数 qt_metacall,里面就调用了最后的 qt_static_metacall

int Object::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 < 5)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 5;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
		...
    }
    return _id;
}

最后,终于回到了这个函数,调用了具体的槽函数。 doActivate 函数稍微有点复杂,建议看源码

void Object::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<Object *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->clicked((*reinterpret_cast< bool(*)>(_a[1]))); break;
        case 1: _t->pressed(); break;
        case 2: _t->sendMessage((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2]))); break;
        case 3: _t->click(); break;
        case 4: _t->hide(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        ...
    }
}

总结

从信号函数到这里一路流水账,中间有很多细节没有处理,但是不需要在意,另外使用信号槽还有 lambda 等方式,这种连接的话,流程我不是很清楚

总之只要记住槽函数的同步和异步调用就行了,最后再总结一下:

  • 继承 QObject,添加 Q_OBJECT 宏
  • 信号函数必须声明在 signals 后面
  • 槽函数声明在 slots 后面,可以理解槽函数就是正常的成员函数,可以用在 SLOT 宏里面
  • moc 为我们实现了信号函数、生成了 qt_static_metacall 函数
  • 是信号函数被调用时所在的线程和接收对象所在的线程比较,不是发送对象和接收对象
  • 异步处理时使用了事件机制,通过 postEvent 投递一个事件给接收对象(先投递给它所在的线程,线程对应的事件循环把事件发送给对象)
  • 同步处理也就是直接在信号函数里面调用了槽函数
  • 每个信号函数都关联了一个 ConnectionList,进行遍历处理,可以理解为观察者模式

希望能帮你理解到槽函数的调用时机,关于异步处理,可以先简单理解为生产者消费者模式,具体请先了解事件机制~
想了解异步处理可以看我下一篇文章 Qt 信号槽原理 - 多线程时槽函数调用时机

本人实力有限,只看得懂部分源码,可能会断章取义,如果有什么错误欢迎友好交流讨论
本文为个人学习记录,转载引用请注明出处

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值