Qt杂谈6.浅谈信号槽那些事

发布时间 2023-12-19 17:25:05作者: Qt小罗

1 引言

Qt信号槽是一大特色,介绍它的文章也数不胜数,为啥还要说呢,主要还是想从实现原理作为切入点,谈谈一个信号发射到槽函数执行所经历的大致流程,从宏观角度进行一个简单梳理,相比于一般的文章稍微深入一点点吧,毕竟水平有限,希望能帮到一些有一定Qt基础的人。

2 信号槽执行流程

这里主要分析信号槽队列连接方式,比较有代表性一点。

2.1 建立连接

首先,使用QObject::connect()函数建立信号与槽的连接,这些信号槽的连接信息将以 QObjectPrivate::Connection 对象的形式存储在 ConnectionList(QList<QObjectPrivate::Connection>) 列表中。每一个 Connection 对象包含了信号发送者对象的指针、信号的索引(对应于发送者信号列表中的位置)、槽的接收者对象的指针、槽的方法信息、连接的类型、额外的连接标志或者参数。

连接的类型一般有三种:
直接连接(DirectConnection):槽函数会直接在信号发射的那个线程上立即执行。
队列连接(QueuedConnection):如果信号和槽属于不同的线程,槽函数的执行将会在目标槽所属对象的事件循环中异步执行。为了达到这个目的,Qt 封装一个 QMetaCallEvent 事件。
阻塞队列连接(BlockingQueuedConnection):与队列连接类似,但是发射信号的线程会阻塞等待槽函数执行完毕。

其中,ConnectionList 是一个链表数据结构,它保存了与一个特定信号相关联的所有连接,在内存中动态分配,通常在对象被创建时初始化。如何关联上呢?QObject 类有一个指向 QObjectPrivate 类型的私有数据指针(d_ptr),QObjectPrivate 包含一个或多个 ConnectionList 实例,这里需要注意的是每一个信号对应一个 ConnectionList。

2.2 发射信号

当信号被发射时,moc生成的QMetaObject::activate函数会被调用,它会负责查找所有与信号链接的槽,并依次调用每个槽。

  1. 首先通过 QObjectPrivate 获取到所有的信号槽连接信息;
  2. 通过发射信号的索引,activate() 函数可以确定哪些 Connection 对象是与当前发射的信号相关的;
  3. activate() 函数遍历与信号相关的所有 Connection 对象。对于每个 Connection,它会根据连接时的类型来决定是直接调用槽函数,还是将调用事件(QMetaCallEvent)推送到指定接收者的事件队列中;
  4. 如果槽是在相同线程被直接连接的,那么 QObject::activate() 会直接调用该槽函数。如果是跨线程连接的方式,事件将被排队,在接收者对象所处线程的事件循环中被处理,随后调用对应的槽函数;
  5. 对于每个槽函数的调用,activate() 会传递正确的参数,这是通过从信号发射时传递的参数列表中复制参数实现的。

2.3 执行槽函数

前面提到,在信号发射时,Qt会检查这些连接并确定如何调用关联的槽函数。如果连接是 Qt::DirectConnection 并且信号与槽位于同一线程,那么槽会直接被调用,如果连接是 Qt::QueuedConnection 或者信号与槽位于不同的线程,Qt 则会创建一个 QMetaCallEvent。

  1. 封装事件(Event Wrapping):
    如果连接类型是 Qt::QueuedConnection 或 Qt::BlockingQueuedConnection,并且发射信号的线程与接收槽的线程不同,Qt会创建一个特殊的事件对象 QMetaCallEvent,它包含了调用槽函数所需的所有信息,包括接收者的槽函数指针、参数值的拷贝等。
  2. 事件投递(Event Posting):
    生成的事件被投递到接收对象所属线程的事件队列中。Qt使用QCoreApplication::postEvent()函数在应用程序的主事件循环中投递事件,在这个过程中,发射信号的线程不会阻塞等待(除非是 Qt::BlockingQueuedConnection 类型,这时会等待槽方法执行完毕)。
  3. 事件处理(Event Processing):
    在接收者所属线程,线程的事件循环将持续运行等待事件,接收者线程的事件循环收到 QMetaCallEvent 后,会根据事件中封装的信息,调用目标对象的 qt_metacall 函数执行相应的槽方法。
    qt_metacall 是 Qt 框架中的一个特殊成员函数,它是 QObject 类的一部分,它允许动态调用槽和操作属性,由 Q_OBJECT 宏定义,是元对象系统的核心,它允许实现反射和对象间的通信,而 QMetaCallEvent 是一种用于线程间槽调用的事件。这两者协同工作,使得 Qt 的信号和槽机制可以跨线程安全地操作。
  4. 槽函数执行(Slot Invocation):
    在 qt_metacall 函数中,根据事件的信息,使用反射机制调用具体的槽函数,并将参数传递给它。这样,即便是跨线程,槽函数也可以在接收者所在线程的上下文中被正确执行。

3 总结

很多东西被隐藏在Qt的元对象系统内部,通常开发者不必直接访问或操作这些内部数据结构,但是如果想理解得更加深入,还是需要对实现原理做一定的了解。好了,有不对的地方,还请大家不吝赐教。