概念
在 Qt 编程中,信号与槽机制是实现对象间通信的核心工具。
信号(Signal):对象状态的变化或事件发生时发出的通知,本质上是一种特殊的成员函数声明,它不包含函数体,仅用于通知其他对象某一事件的发生。例如,当用户点击界面上的按钮时,按钮对象就会发出clicked信号,告知系统 “按钮被点击了” 这一事件。
槽(Slot):用于响应信号的普通成员函数。它与普通 C++ 函数类似,可以有参数,也能被重载,并且可以定义在类的public、protected或private部分。不同之处在于,槽函数能够与信号建立连接,一旦与之关联的信号被发射,槽函数便会自动被调用,执行相应的操作。
连接(Connect)::将信号和槽关联起来的关键步骤。通过QObject::connect()函数,我们能够指定信号的发送者、信号本身、接收者以及对应的槽函数,从而构建起信号与槽之间的通信桥梁,使得信号发射时能够准确触发相应的槽函数。
Qt信号槽机制的优缺点
优点:
-
解耦合:信号槽机制允许对象之间进行松耦合的通信。发送信号的对象不需要知道接收槽的对象,反之亦然。这使得代码更加模块化和可维护。
-
灵活性:一个信号可以连接到多个槽,一个槽也可以连接到多个信号。这种多对多的关系使得设计复杂的应用程序变得更加灵活。
-
类型安全:信号和槽在编译时进行类型检查,确保传递的参数类型匹配。这减少了运行时错误的可能性。
-
易于使用:信号槽机制的语法简单直观,易于理解和使用。开发者可以轻松地在对象之间建立复杂的通信关系。
-
支持异步通信:信号槽机制支持异步通信,可以在不同的线程中发送信号和处理槽函数。这有助于构建响应迅速、高效的应用程序。
缺点:
-
性能开销:信号槽机制涉及额外的函数调用和数据传递,可能会带来一定的性能开销。特别是在高频调用的情况下,这种开销可能变得显著。
-
调试难度:由于信号槽机制的松耦合特性,跟踪和调试信号槽之间的连接可能比较困难。尤其是在大型项目中,信号槽的连接关系可能变得复杂。
Qt信号槽机制实现原理
核心原理在于通过元对象系统(Meta-Object System 简称MOS)来实现对象之间的解耦合和类型安全的连接。
元对象系统的概念
元对象系统是Qt中实现对象间通信的核心,提供了运行时类型信息(RTTI)、属性管理、信号和槽的动态连接等功能。它通过Qt的元对象编译器(Meta-Object Compiler, MOC)实现,MOC会在编译时期读取C++代码中的特定宏(如Q_OBJECT),并生成附加的元数据代码。
-
元对象系统是一个基于标准C++的扩展,为Qt提供了信号与槽机制、实时类型信息、动态属性系统。
-
元对象系统的三个基本条件:类必须继承自QObject、类声明Q_OBJECT宏(默认私有有)、元对象编译器moc。
-
元对象编译器Meta-Object Compiler(moc)预处理器,提将qt的程序转换为标准的c++程序,再由标准c++编译器进行编译。它为高层次的事件处理自动生成所需要的必要代码。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件。如果moc发现在一个类头文件中包含了宏 Q_OBJECT,则会生成以moc_className.cpp(自定义类名)的.cpp文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。新的文件同样将进入编译系统,与原文件一起参与编译。
-
QMetaObject类提供了访问元对象信息的接口。通过QMetaObject,可以动态地获取类的元数据,包括类名、属性、方法、信号和槽等
声明与实现
信号和槽的本质都是函数。
我们知道C++中的函数要有声明(declare),也要有实现(implement),
而信号只要声明,不需要写实现。这是因为moc会为我们自动生成。
另外触发信号时,不写emit关键字,直接调用信号函数,也是没有问题的。
这是因为emit是一个空的宏
#define emit
Q_OBJECT宏
public: \
Q_OBJECT_CHECK \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS \
private: \
Q_OBJECT_NO_ATTRIBUTES_WARNING \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
QT_WARNING_POP \
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject, "")
声明了一个只读的静态成员变量staticMetaObject,以及3个public的成员函数
原理:
元对象编译器使用 moc 分析 C++ 源文件。如果moc发现在一个类头文件中包含了宏 Q_OBJECT,则会生成以moc_className.cpp(自定义类名)的.cpp文件,生成的cpp文件中,就是变量staticMetaObject以及 public中的函数实现。
staticMetaObject是一个结构体,用来存储类的信号、槽等元信息,并把qt_static_metacall静态函数作为函数指针存储起来。
因为是静态成员,所以实例化多少个对象,它们的元对象信息都是一样的。
qt_static_metacall函数提供了两种“元调用的实现”:
如果是InvokeMetaMethod类型的调用,则直接 把参数中的QObject对象,转换成目标类然后调用其信号函数
如果是IndexOfMethod类型的调用,即获取元函数的索引号,则计算信号函数的偏移并返回。
信号的触发
信号的实现,直接调用了QMetaObject::activate函数。其中0代表信号这个函数的索引号。QMetaObject::activate函数的实现,在Qt源码的QObject.cpp文件中,略微复杂一些,且不同版本的Qt,实现差异都比较大,大致的实现为先找出与当前信号连接的所有对象-槽函数,再逐个处理。这里处理的方式,分为三种:
if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {
// 队列处理
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
// 阻塞处理
// 如果同线程,打印潜在死锁。
} else {
//直接调用槽函数或回调函数
}
receiverInSameThread表示当前线程id和接收信号的对象的所在线程id是否相等。
如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。
如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。
如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。
(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,本身就是阻塞的,直接调用就好了,
如果走阻塞队列,则多了加锁的过程。如果槽中又发了同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)
队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent放进了事件循环。
等到下一次事件分发,相应的线程才会去调用槽函数。
槽和moc生成
slot函数我们自己实现了,moc不会做额外的处理,所以自动生成的moc_Jerry.cpp文件中,只有Q_OBJECT宏的展开。