信号槽的实现原理
Qt的信号/槽机制是一种事件驱动机制,它在某种程度上类似于C++的函数指针,但它更为强大和灵活。信号/槽机制的工作原理如下:
- 信号(Signal):当在程序中发生某个事件时,一个信号被发出。例如,当用户点击一个按钮时,按钮对象可能会发出一个"被点击"的信号。
- 槽(Slot):槽是一个函数,它是在信号被发出时调用的。一个槽可以连接一个或多个信号,当任何一个连接的信号被发出时,槽都会被调用。
- 连接(Connect):信号和槽之间的连接是通过
QObject::connect()
函数建立的。当一个信号被发出时,所有连接到该信号的槽都会被调用。
Qt的信号/槽机制是如何实现的呢?其背后的机制包括元对象系统(Meta-Object System,MOS)和事件队列。
- 元对象系统:Qt通过MOC(Meta-Object Compiler,元对象编译器)扩展了C++的语言特性。MOC会读取包含
Q_OBJECT
宏的头文件,并生成一个相应的C++源文件,这个源文件包含了类的元信息,如类名、父类名、信号/槽的名字等。这就是Qt能够在运行时进行类型检查和信号与槽的匹配的原因。
- 事件队列:当一个信号被发出时,如果连接到该信号的槽函数没有立即执行,那么这个调用就会被放入到事件队列中,等待事件循环下一次循环时执行。这意味着,即使在多线程环境中,槽函数也总是在它所属的线程中被执行,这大大简化了线程同步的问题。
下面进一步深入。
moc系统
首先,给定一个示例:
Q_OBJECT
这个宏是Qt元对象的核心,查看它的展开如下:相当于对于需要Qt信号槽机制的类,比如注册Qt的元对象(即Q_OBJECT),然后Q_OBJECT为这个类添加了一些列元对象结构和相应的函数接口,比如上面的核心
staticMetaObject
、qt_static_metacall
.查看该头文件编译后的moc_xx.cpp:
moc_xx.cpp源码
结合其中Qt生成的注释来理解:Moc编译器创建了一个包含信号和槽信息的元对象。这个元对象是
MyWidget
类的一部分,并包含了类名、信号和槽的名称、信号和槽的参数类型等信息。元对象的结构
元对象的定义部分看起来是这样的:
可以看到,元对象包含了一些元数据,如类名、信号和槽的名称等。这些信息是在编译时由Moc编译器生成的,并存储在
qt_meta_stringdata_MyWidget
和qt_meta_data_MyWidget
这两个静态变量中。信号和槽的调用
元对象还包含一个
qt_static_metacall
函数,这个函数是用来调用信号和槽的。这个函数会接收一个指向QObject的指针、一个表示调用类型的枚举值(如InvokeMetaMethod)、一个表示被调用方法的索引以及一个参数列表。在这个例子中,
qt_static_metacall
函数的实现是这样的:信号槽调用
当Qt需要发射一个信号或调用一个槽时,它会调用
qt_static_metacall
函数,并传递相应的参数。例如,如果代码中有这样一行:查看emit,可以看到:
emit
关键字在C++中本身并不具有任何意义,它只是一个标记,用于在语义上表示发出一个信号。实际上,emit
在预处理后会被完全忽略。当你写下 emit someSignal();
时,实际上等同于直接调用 someSignal();
。emit
关键字在C++中本身并不具有任何意义,它只是一个标记,用于在语义上表示发出一个信号。实际上,emit
在预处理后会被完全忽略。当你写下 emit someSignal();
时,实际上等同于直接调用 someSignal();
。那么,信号函数是如何实现的呢?在Qt中,信号是通过
QMetaObject::activate
函数进行发射的。下面是你提供的mySignal
信号函数的实现:QMetaObject::activate
函数接受四个参数:发射信号的对象、对象的元对象、信号在元对象中的索引以及信号的参数列表。在这个例子中,发射信号的对象是this
,元对象是staticMetaObject
,信号在元对象中的索引是0,信号没有参数,所以参数列表是nullptr
。QMetaObject::activate
函数的工作原理是这样的:- 首先,它会获取信号的接收者列表。这个列表是在信号和槽连接时创建的,包含了所有连接到这个信号的槽。
- 然后,它会遍历这个列表,对每一个接收者,都会调用其对应的槽。这是通过调用接收者的
qt_metacall
函数实现的。
qt_metacall
函数会根据传入的索引调用相应的槽。这个过程和我之前描述的qt_static_metacall
函数的工作方式类似。唯一的区别是qt_metacall
函数是虚函数,可以在子类中被重写,而qt_static_metacall
函数是静态函数。
connect
以下部分没认真查看源码,主要来源参考GPT和上面参考链接。
在进一步之前,我们来看
connect
的实现:QObject::connect
函数是 Qt 信号/槽机制的核心,它用于将信号和槽连接起来。在 Qt 中,你可以这样使用 connect
函数:当
sender
发射 someSignal
信号时,receiver
的 someSlot
槽就会被调用。相应的源码:
connect干的事情并不多,好像就是构造了一个Connection对象,然后存储在了发送者的内存中。在Qt中,
QObject::connect
函数创建的槽连接会保存在发射信号的QObject
实例中。具体来说,每个QObject
实例都有一个QObjectPrivate
的私有数据成员。这个QObjectPrivate
中有一个名为connectionLists
的成员,它就是用来保存槽连接的。每一个槽连接包含了接收者、槽的索引以及其他的一些信息。信号槽连接后在内存中已QObjectConnectionListVector对象存储,这是一个数组,Qt巧妙的借用了数组快速访问指定元素的方式,把信号所在的索引作为下标来索引他连接的Connection对象,众所周知一个信号可以被多个槽连接,那么我们的的数组自然而然也就存储了一个链表,用于方便的插入和移除,也就是CommectionList对象。
当
QObject::connect
函数被调用时,它会创建一个新的槽连接,并将其添加到connectionLists
中。而当QMetaObject::activate
函数被调用时,它会遍历connectionLists
,并对每一个槽连接调用相应的槽。信号触发
Qt为我们提供了5种类型的连接方式,如下
- Qt::AutoConnection 自动连接,根据sender和receiver是否在一个线程里来决定使用哪种连接方式,同一个线程使用直连,否则使用队列连接
- Qt::DirectConnection 直连
- Qt::QueuedConnection 队列连接
- Qt::BlockingQueuedConnection 阻塞队列连接,顾名思义,虽然是跨线程的,但是还是希望槽执行完之后,才能执行信号的下一步代码
- Qt::UniqueConnection 唯一连接
一般情况下,我们都使用默认的连接方式,除非一些特殊的需求,我们才会主动指定连接方式。当我们执行信号时,函数的调用关系可能会像下面这样
- 直连
对于大多数的开发工作来说,我们可能都是在同一个线程里进行的,因此直连也是我们使用连接方式最多的一种,直连说白了就是函数回调。还记得我们第三小节讲的connect吗,他构造了一个Connection对象,存储在了发送者的内存中,直连其实就是调用了我们之前存储在Connection中的函数地址。以上代码调用的堆栈(没有加上Qt源码部分的堆栈信息):
- 队列连接
connect连接信号槽时,我们使用Qt::QueuedConnection作为连接类型时,槽函数的执行是通过抛出QMetaCallEvent事件,经过Qt的事件循环达到异步的效果。
下面代码摘自Qt源码,queued_activate函数即是处理队列请求的函数,当我们使用自动连接并且接受者和发送者不在一个线程时使用队列连接;或者当我们指定连接方式为队列时使用队列连接。
测试代码
查看调用栈:
总结
使用
QObject::connect
绑定信号槽的执行工作原理:- 首先,
connect
函数会通过信号和槽的名字查找它们在元对象中的索引。这是通过调用QMetaObject::indexOfSignal
和QMetaObject::indexOfMethod
函数实现的。
- 然后,
connect
函数会创建一个槽连接,保存了信号发射者、信号的索引、接收者以及槽的索引等信息。这个槽连接会被添加到发射者的一个列表中,这个列表保存了该发射者的所有槽连接。
- 当发射者发射信号时,会调用
QMetaObject::activate
函数。activate
函数会遍历发射者的槽连接列表,找到所有与该信号相关的槽连接。
- 对于每一个槽连接,
activate
函数会检查接收者是否还存在(因为接收者可能已经被删除)。如果接收者存在,就会调用接收者的qt_metacall
函数,传入槽的索引和信号的参数。
- 最后,
qt_metacall
函数会根据传入的索引调用相应的槽
- 作者:Olimi
- 链接:https://olimi.icu/article/b654d138-fc92-4ec4-8774-f6e3c93ff980
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。