type
status
date
slug
summary
tags
category
icon
password
本文基于 Qt 6.8,从源码上分析 Qt 信号槽机制的实现原理。
信号槽机制的使用方法
这里不讨论 qml 中的信号槽机制。
一般使用信号槽的方法分为以下几步:
- 在字段
signals
下声明信号,无需定义。
- 在字段
public slots
或private slots
下声明槽,并向普通的成员函数一样添加定义。
- 使用
QObject::connect()
函数连接信号和槽。
- 发射信号→槽响应
其中信号也可以连接信号,以及普通的成员方法(Qt5开始,
QObject::connect()
可传入函数指针)。后面只分析信号和槽,像信号和信号、信号和普通函数的处理基本上是类似的。signal的代码生成
在使用信号槽的时候可以发现,发射信号的时候可以使用
emit
,也可以不使用,实际上 emit
是一个定义为空的宏定义,用不用都无所谓。而使用的时候,信号仅仅需要声明,而不需要定义。而从其声明中就可以看到,信号就是普通的函数,在C++中,直接调用一个没有定义的函数是一种语法错误。因此可以猜测,信号一定在编译的某个时候自动生成了定义。
在《Qt 核心机制源码分析之元对象系统》的 ”6、信号发射的实现“中提到过:在 moc 阶段,信号会被扩展,其核心逻辑为:
QMetaObject::activate
的调用。信号和槽的连接—connect
connect 的用法一般有两种,一种使用
SIGNAL()
、SLOT()
宏,另一种使用函数指针/functor。使用函数指针/functor的 connect
在 Qt 6.8 中,有多个 connect 的重载,都为模板方法,大致分别为
- Connect a signal to a pointer to qobject member function。
连接信号到一个 QObject 的成员函数指针。
- connect to a function pointer (not a member)
连接信号到一个非成员函数指针
- connect to a functor
连接信号到一个 functor (std::function 或 lambda)
- connect to a functor, with a "context" object defining in which event loop is going to be executed
连接到一个 functor,同时传入一个“上下文”对象
这些
connect
函数最后都由 QObject::connectImpl
实现。这里主要分析
QObject::connectImpl
的实现。connect
模板函数涉及大量 type_trait,使得该模板函数非常强大,不属于这里讨论的范畴。QObject::connectImpl
参数:
- sender: 指向信号所在的 QObject 对象
- signal: 指向信号函数指针的指针
- receiver: 指向槽所在的 QObject 对象
- slot: 指向槽函数指针的指针
- slotObj: 不使用虚函数实现多态的槽函数封装(减少虚表和RTTI的生成,优化性能)
- types: 信号/槽参数类型数组
- senderMetaObject: 信号所在的 QObject 类的元信息对象
这里的实现逻辑非常简单:
从
sender
所属的类及其父类的元对象类中获取当前信号 signal
的索引 signal_index
,最后通过 QObjectPrivate::connectImpl
实现。QObjectPrivate::connectImpl
这里主要的实现分为“创建连接”和“添加连接”。
添加连接的方法为
inline void QObjectPrivate::addConnection(int signal, Connection *c)
QObjectPrivate::addConnection
这段代码本质上就是一个维护“连接”双向链表的头插法或尾插法插入。
有关“连接”的数据结构和底层优化,详见《Qt源码剖析之信号槽机制(二)—连接的性能优化》
使用 SIGNAL() 和 SLOT() 的 connect
接口如下:
可以看到,除了 signal 和 receiver 参数类型都是 C 风格字符串,这其实也说明了 SIGNAL() 和 SLOT() 宏其实就是将函数编码为字符串。
其实无论是使用函数指针还是字符串,归根揭底都是需要新建连接和添加连接,因此需要获取信号所在元信息的索引 signal_index,以及槽函数的封装。在 《Qt 核心机制源码分析之元对象系统》中提到过,元信息中存储了一个元字符串数据。对应 QMetaObject::Data::stringdata。
SIGNAL() 和 SLOT()
简单来说,这两个宏的功能就是在信号和槽前面加入一个标识符,然后返回字符串类型。
例如:
connect
这个 connect 的实现逻辑大致分为以下几点
- 根据传入的 signal slot 字符串信息,获取其在元信息类中的索引
signal_index
和method_index_relative
。
- 调用
QMetaObjectPrivate::connect
创建连接,并将其插入连接链表中。具体的实现和QObjectPrivate::connectImpl
基本一直,不再赘述。
总结
无论是哪种 connect,其大致步骤都是:
- 获取信号索引 signal_index
- 获取槽信息(封装成可调用类或者直接使用槽索引)。
- 创建连接 Connection
- 添加连接到连接链表中
我们注意到,如果使用 SIGNAL() 和 SLOT() 宏来进行连接,其实现中使用的槽索引,也就是说槽函数必须为 QObject 类的成员函数,需要使用元信息,因此其不能连接普通非成员函数或functor。
信号的发射与槽的响应
在 《Qt 核心机制源码分析之元对象系统》提到过,signal的核心逻辑为:
QMetaObject::activate
的调用。QMetaObject::activate
可以看到
QMetaObject::activate
最终调用的是 doActivate<false>(sender, signal_index, argv);
(模板参数为true的情况一般是调试时设定了回调函数)doActivate
这个方法是信号槽机制中较为重要的方法,内容较多,可先简单浏览代码,后面逐个分析关键要点。
连接优先级 currentConnectionId
每一个 connection 都维护一个 currentConnectionId,插入到连接链表时递增,也就是说越晚 connect ,其 currentConnectionId 越大。
维护这个优先级 id 的目的是,响应当前信号槽连接的时候,不响应新建 signal 相同的连接,这个新建的连接的响应需要等到下一次 signal 发射。
因此在遍历连接链表时,其循环条件为:
队列连接 Qt::QueuedConnection
如果 connect 的第五个参数为 Qt::AutoConnection(且 sender 和 receiver 不在同一线程) 或者 connect 的第五个参数为 Qt::QueuedConnection。则该连接为队列连接。
所谓的队列连接,就是信号发射后,将连接封装成事件(
QMetaCallEvent
),并投递到 receiver 所在线程的事件循环的事件队列中。其源码实现如下:
阻塞队列连接 Qt::BlockingQueuedConnection
阻塞队列连接的实现和队列连接基本上是一致的,当阻塞队列必须阻塞等待当前投递的
QMetaCallEvent
事件响应完成。这是通过信号量来实现阻塞的:
可以看到,在投递事件前,定义了一个信号量
QSemaphore semaphore
该信号量的的值为0,因此事件投递完成后会在 semaphore.acquire();
阻塞。根据文档,自然是槽函数响应完成后继续执行,因此肯定有某个地方调用了
semaphore.acquire();
在《Qt源码剖析之事件循环系统(一)—事件系统》中介绍过,使用
postEvent()
投递的事件需要在堆上分配内存,在上述代码上看确实如此。同时如果我们打开 Qt 的帮助文档,可以看到对
postEvent()
的描述中提到: The event must be allocated on the heap since the post event queue will take ownership of the event and delete it once it has been posted. 同时在《Qt源码剖析之事件循环系统(二)—事件循环系统的实现》的 “3.1 处理 Posted Event 队列 (QCoreApplicationPrivate::sendPostedEvents())” 的源码中可以看到当事件处理完成后,会自动释放事件,此时事件及其父类的析构函数被调用:
在创建
QMetaCallEvent
时,传入了一个信号量,其父类 QAbstractMetaCallEvent
的析构函数中:因此,信号量 acquire 阻塞信号发射处的线程→槽函数执行完成→槽函数调用事件处理完成→事件析构→信号量 release → 信号发射处的线程释放
QMetaEvent 事件的响应
上面提到的两种队列连接都是通过投递 QMetaEvent 事件到 receiver 所在的事件循环队列来实现的,也就是其依赖于事件循环系统。这里简单说一下这个事件的响应
QObject::event
的调用详见 《Qt源码剖析之事件循环系统(二)—事件循环系统的实现》。这个方法是虚函数,可以被用户重写,因此在重写该方法的时候请记住,最后一定要调用一下父类 QObject::event
!!!QMetaCallEvent::placeMetaCall
中有三种槽函数的调用方法,下面马上介绍。直接连接 Qt::DirectConnection (三种槽函数的调用方法)
当显式声明 Qt::DirectConnection 或 使用 Qt::AutoConnection 且 sender 和 receiver 属于同一线程时,就和变成直接连接。
直接连接是指发射信号后,在同一线程同步执行槽函数,和直接调用槽函数的一样。
可以看到调用槽函数有三种方式:
- 调用
QtPrivate::QSlotObjectBase::call
(专门用于处理现代 C++ 中更灵活的连接方式)
这是一个对槽函数的封装类,持有槽函数的函数指针。不使用虚函数实现多态的槽函数封装(减少虚表和RTTI的生成,优化性能)。
- 调用
Connection::callFunction
(处理优化后的 QObject 成员函数调用)
Connection::callFunction
在创建 Connection 的时候由 QMetaObjectPrivate::connect
进行初始化。Connection::callFunction
直接指向 moc 生成的 QMetaObject::Data::static_metacal
,避免免了通过 QMetaObject::metacall
进行的基于名称或索引的查找,从而 提高性能。 “使用 SIGNAL() 和 SLOT() 的 connect” 就是基于
QMetaObjectPrivate::connect
实现的。 可以看到,当
reveicer
为 QObjct
类型,也就是有元信息 (QMetaObject
) 时,会调用 moc 生成的 qt_static_metacall
。rmeta->d.static_metacall
即 QMetaObject::Data::static_metacall
是由 moc 生成代码时初始化为 moc 生成的 qt_static_metacall
。详见 《Qt 核心机制源码分析之元对象系统》的 “3、静态元对象 staticMetaObject”。
- 调用
QMetaObject::metacall
(性能最差)
实际上调用的是 moc 生成的
QObject::qt_metacall
,该方法通过计算出槽函数在元对象系统中的绝对索引 method = c->method_relative + c->method_offset
。然后通过
moc 生成的 QObject::qt_metacall
间接调用 moc 生成的 qt_static_metacall
。性能稍微不如上面两种方法。单次调用 Qt::SingleShotConnection
如其名字所示,连接只触发依次,也就是触发该 Connection 的时候从连接链表中移除,不再赘述
自动连接 Qt::AutoConnection
上面已经分析过,自动连接根据 sender 和 receiver 是否属于同一线程决定使用直接连接还是队列连接。
总结
本文深入分析了 Qt 6.8 中信号槽机制的底层实现原理,涵盖了从连接建立到信号发射、槽函数响应的整个流程。
Qt 的信号槽机制是一个高度优化、灵活且线程安全的系统。
所谓线程安全,是基于锁实现的,因此其性能会受到一定的影响。
但信号槽机制依赖 “元对象系统”、“事件循环系统”、“多线程机制”,能做到线程安全已经非常了不起了。
信号槽机制作为底层机制,其中的性能优化请见下一篇~