Qt信号槽机制
P.S.(该系列文章是个人学习总结,拿出来和大家讨论,水平有限,如有错误,特别、非常、极其欢迎批评和指正!)
开始之前,先放一个链接,这个网站可以查看不同版本 Qt 相关的源码,不调试的话用这个就很方便。Qt源码浏览
在分析 Qt 信号槽的机制之前,先来看一下实际工程代码中存在的典型场景,一个典型的对象间通信的场景如下: 当一个类的数据或者是行为发生变化时,对这个变化感兴趣的对象收到响应的通知,决定进行如何响应。在GUI程序中,界面元素的组成非常复杂,界面上一个对象的行为可能也有很多种,一个控件想要准确高效的响应另外一个控件的变化尤其重要。在 Qt 中信号槽机制挑起了这一大梁,在继承QObject并且声明了Q_OBJECT宏之后,我们就可以简单的通过如下的方式建立连接,在信号函数发送后,槽函数就会被调用,清晰明了的解决了对象间通信的问题。
cpp
connnect(objectA, SIGNALS(valueChanged()), objectB, SLOTS(onValueChanged()));
但是我们在上一篇文章中已经知道,这里的宏只是把函数转换为了字符串,并没有做额外工作。 那么 Qt 是如何做到这一点的呢? 现在 Qt 发展出了更清晰的取地址,匿名函数等形式的信号槽,但是宏定义的形式最能体现元对象系统在对象间通信的作用。
1.从观察者模式说起
在设计模式中,关于对象间通讯的问题,有个经典的设计模式为观察者模式,或者其他的变体,如订阅-发布模式等。为什么 Qt 没有直接使用观察者模式来解决通讯问题?信号槽机制有什么优点和限制? 我们还是先从观察者模式说起,一个简单的 C++ 观察者示意。
c++
class Observer
{
public:
virtual void onValueChanged() = 0;
};
class Subject
{
public:
void add(Observer*);
void remove(Observer*);
void setValue(int v) {
m_vA = v;
notify();
}
void notify()
{
for (auto ob : m_obs) {
ob->onValueChanged();
}
}
private:
int m_vA;
std::list<Observer*> m_obs;
};
这里只抽象了观察者,我们继承 Observer
并且重写其中的虚函数,通过 add(...)
方法添加观察者,当我们内部有值变化时, Subject
就可以调用观察者的借口来完成通知,这样就完成了一对多的对象通信,稍加改造我们也可以完成多对多的通信。 这里会有两个问题:
- 写了一个类,这个类里面有个函数叫做onValueChanged特别合适,但是这个签名已经被override了怎么办
- 如果一个对象有多个行为变化,观察者需要响应不同的逻辑,怎么办?
对于情景1,我们暂时没有别的方法,毕竟你继承了这样的体系,就要遵守这样的"协议",只能修改自己的函数名(起名字真的太难了)。 对于情景2,我们可以尝试多重方法,例如
Subject
中参数,我们把更新的类型写到notify(...)
的参数中,Observer
根据参数解析后决定自己的行为Subject
中增加枚举值,提供不同的列表,Observer
中加接口,给Subject
不同方法来调用,相当于定制了不同的频道
本质上是一样的,以第二种为例,类似下面这样
c++
class Observer
{
public:
virtual void onValueAChanged() = 0;
virtual void onValueBChanged() = 0;
virtual void onValueCChanged() = 0;
};
class Subject
{
public:
enum class EType
{
eChannelA,
eChannelB,
eChannelC
};
void add(Observer*, EType);
void remove(Observer*, EType);
void notify(EType tpye)
{
//...
// 通知对应类型的观察者
auto& obs = m_mapObs[(int)type];
if (...) {
// ChannelA
}
}
private:
//...
std::unordered_map<int, std::list<Observer*>> m_mapObs;
};
这样就可以分别响应三个变化,多对多的通知也是可以的,这种方法的变化也有很多,例如可以使用std::function包装起来,直接调用等等。但是不管怎么说,都需要一组标志位和逻辑判断。 如果有一种方式,让 Observer
想观察哪个值就观察哪个值,想用什么签名就用什么签名就好了。 对比传统观察者模式和 Qt 的信号槽,我们会发现一些不同:
- 一般的观察者模式会受到接口数量,通常只有几个固定的接口,很难自由的变化和动态的增加。
- 受到函数签名的限制,不能随便改变函数的命名。
如果考虑到重复检验,线程安全可能更复杂。
2.简易的信号槽实现
针对以上问题,如何做优化呢?我们层层递进。 思考1: 在这个场景中,观察者模式的最大问题就是对函数签名的限制,导致我们只能在默认的强耦合规则下重写实现自己的方法,所以我们的目标就是强化这个观察者模式,解决对函数签名的限制。 思考2: 问题变成了如何让函数签名之间的关系转换为签名无关的映射。合理的思考,如果我们能标记所有的成员函数,通过一定的方法,把函数签名之间的联系转换为对象对应的类之间某个标记的联系。比如我们把每个类的方法编个号,把 "一旦对象A的 nofity
被调用,对象B的 onvaluechaned
就会被调用"转换为 "对象 A 的函数 1 被调用,对象 B 的函数 2 就被调用"。 思考3: 问题转换为我们如何在代码运行时知道函数签名和编号索引的关系。这要求我们在运行时也知道 C++ 类的相关信息,知道这个类有几个方法,某个函数签名是第几个,但 C++ 是静态语言,运行时没有这些信息...... 来了来了!上一篇文章,我们有了一个叫做元对象的东西,可以帮我们实现这个需求。回忆一下我们之前提到的元数据和元对象,元数据和元对象记录了一个类相关信息,包括类名,方法,枚举等等。我们可以在可以在编译前扫描源文件是生成元数据,把类的成员函数编号,在运行时就可以通过函数名映射为 index 值,具体的过程可以参考上一篇文章。
这里我们差不多就解决了这个函数签名问题。然后就是实现原来观察者模式的一些过程。主要分为如下几个步骤。
2.1函数名转换为索引
怎么建立连接(观察者模式中的观察和被观察的关系)? 我们的目的是借助元对象和元数据来建立关系,并且希望不受函数名的影响,我们可以采取的方法是:通过元数据将函数名转换为方法的索引值,将方法名的连接转换为索引值的关系,不考虑参数校验,示意代码如下,假设我们的基类为 Object
。
cpp
viod connect(Object* send, const char* signame, Object* recv, const char* slotname)
{
int sigindex = send->metaData()->methodIndex(signame);
int soltindex = recv->metaData()->methodIndex(slotname);
doConnect(send, sigindex, recv, recvindex); // 转换为与签名无关的索引值连接
}
2.2保存连接
连接关系存在哪里,存哪些信息? 连接关系我们使用Connect对象来表示,按照观察者模式的惯例,存在变化的那端。为了不影响我们类本身的结构,我们把这部分信息也存在它的元对象数据中。和观察者模式类似,我们需要存recv对象的指针,还有转换之后的recv对象槽函数的index值,参数信息可以考虑调用的时候传递过去。
c++
//-----------------connect结构---------
struct Connect
{
// ...
Object* recv;
int index;
};
//------------------------------------------------
//存储在元对象中的数据,信号中连接的槽函数的信息。
std::unorder_map<int, std::list<Connect>> m_connects;
//------------------------------------------------
// doConnect的实现
void doConnect(Object* send, const int sendIndex, Object* recv, int recvIndex)
{
auto& connections = send->metaData()->connections(); // 返回了元数据中的m_connects
auto& sigConnectList = connections[sendIndex]; // 索引值为sendIndex的connect链表
sigConnectList->push_back(Connect(recv, recvIndex));
}
2.3通知
如何触发(通知) 因为我们建立了签名和方法之间的映射,元数据生成的时候可以同时生成这一部分代码,注意,notify
函数现在我们通过程序生成,因为程序在生成元数据的时候很方便的知道这个方法对应哪个方法。 实际的调用流程很通用,我们抽出来放在元对象中统一实现。示意过程如下:
cpp
//---------------moc_Subject.cpp---------------------
//moc_Subject.cpp中的一部分实现,由程序生成的代码(被观察者)
void Subject::notify()
{
// 每个函数的映射都知道,假设notify的编号是0
MetaObject::fireSiganl(this, &metaObject, 0, nullptr);// 最后一个位置用来传递参数,没有参数传nullptr
}
//--------------------------------------------------
//---------------MetaObject.cpp--------------------
// 这部分是通用的逻辑
MetaObject::fireSignal(QObject* sender, MetaObject* obj, int index_signal, void** argv)
{
auto* connects = obj->connections();
auto& sig_connects = connects[index_signal]; // 该信号上所有的connect
for ( auto& conn : sig_connects) {//逐一调用,相当于观察者的接口
Object* recv = conn->recv;
recv->meta_call(conn->index, argv);
}
}
//----------------moc_Observer.cpp-----------------
// 另一个类中(观察者)程序生成的代码
void Observer::meta_call(int index, void** argv)
{
switch index:
case 0:
onValueChanged();// 在这里进行实际的通知调用
break;
case 1:
//...
//...
}
这样我们就实现了任意两个对象的任意名称的信号槽函数的调用,尽管这个流程不严谨,功能比较单一,也不稳定,但是总体思路可以实现,梳理一下:
- 继承自Object基类,声明D_OBJECT宏,这个宏能够被识别生成元数据和元对象,这个是前提
- 改进我们生成元数据的脚本,在生成元对象的时候帮我们生成信号的实现函数和meta_call的代码内容
- 在MetaObject中实现相关的静态方法的调用
- 调用connect建立连接,调用emit发送信号,完成对象之间的通信
对于用户来讲,只有1和4是有感知的。这样,整个流程都走通了,当然这只能算得上是示意流程,这里面还有其他很多细节要考虑,例如如果两个对象不在一个线程里面怎么办,槽函数的调用可以不可以是异步的,信号可以和信号连在一起,怎么处理等等。
但是从基本的思想来讲,这些简化的流程可以帮助我们更好的理解,下面我们可以看一Qt的相关实现,把颗粒度稍微细化一下,更好的理解 Qt 元对象和其应用。
3.Qt 信号槽
Qt 信号槽机制基于Qt的元对象系统,提供了更丰富的对象间通讯的能力,而且是线程安全的,使用上也很方便,以上一章的代码来看,我们在自己的类里面声明了信号和一个槽函数,并使用了 Q_OBJECT
宏,这个宏的作用可以看上一章内容。
cpp
// Myclass.h
#pragma once
#include "QObject"
class Myclass : public QObject
{
Q_OBJECT
//...
public slots:
void onValueChanged() {}
signals:
void vualeChanged();
};
接下来我们会分析源码,看一下源码是如何处理这个信号槽的调用过程,元对象和元数据在里面有扮演了什么角色。
3.1信号槽的使用
cpp
// 连接绑定信号槽
connnect(objectA, SIGNALS(valueChanged()), objectB, SLOTS(onValueChanged()));
// 在需要的地方发送信号,之后槽函数会被调用
{
//'''
emit valueChanged(); // onValueChanged()会被调用
}
这里在文件 qobjectdefs.h
可以看到 emit
只是一个宏定义,没有实际的作用,其实就是一个函数调用,不同的是这个函数我们不实现,而是由 Qt 替我们实现。
3.2信号槽的实现原理
在我们写代码的时候你会发现,我们只会写信号的声明,不写实现,在用的地方直接 emit xxx()
。在这背后,在编译器编译之前,Qt 的元编译器会扫描文件,不但会帮我们生成元数据、构造元对象,还会帮我们实现其中的信号函数。信号的实现很简单,简单的把当前对象的指针,元对象指针,当前信号的编号和参数包装之后调用 QMetaObject
的方法即可,需要注意的有亮点:
- 如果信号有参数,这里会把参数列表转换为void**的数组
- 这里信号的编号是生成代码时硬编码写死的(本来就是不会变的),如果有其他的信号,这里会有1,2,3等
c++
// SIGNAL 0
void Myclass::vualeChanged()
{
QMetaObject::activate(this, &staticMetaObject, 0, Q_NULLPTR);
}
除此之外,还生成了以下代码,注意第一个函数,这个静态函数是槽函数的落地点,可以看到,这里将外部传递进来的对象转换为具体的MyClass类,然后根据对应的方法的id编号,调用相关的函数名。这里的代码都是moc生成的,根据元数据直接编码,把信号函数编码为0,槽函数编码为1。 注意,有的信号的另一端连接的也是一个信号,但归根结底都是函数调用
cpp
void Myclass::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
Myclass *_t = static_cast<Myclass *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->vualeChanged(); break;
case 1: _t->onValueChanged(); break;
default: ;
}
} else if (_c == QMetaObject::IndexOfMethod) {
int *result = reinterpret_cast<int *>(_a[0]);
void **func = reinterpret_cast<void **>(_a[1]);
{
typedef void (Myclass::*_t)();
if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Myclass::vualeChanged)) {
*result = 0;
return;
}
}
}
Q_UNUSED(_a);
}
// 元对象
const QMetaObject Myclass::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Myclass.data,
qt_meta_data_Myclass, qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};// 注意这里的qt_static_metacall,静态函数的地址
//.. cast and get metaobject
int Myclass::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 < 2)
qt_static_metacall(this, _c, _id, _a);
_id -= 2;
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 2)
*reinterpret_cast<int*>(_a[0]) = -1;
_id -= 2;
}
return _id;
}
3.3信号的连接
根据我们之前的表述,信号连接需要处理一下两部分内容
- 将函数签名映射转换为字符串映射
- 构造每个函数的connect列表,记录每个信号的观察者
代码位于qtbase/src/corelib/kernel/qobject.cpp
的2781行。这之间肯定少不了各种检查,检查函数是都存在且正确,参数是否匹配等等,为了逻辑更清晰,这里注释和省略了一些次要的细节,但是代码还是很长,如果不太关注源码逻辑可以看下面的极简注释版的,相对详细的放在后面
cpp
// 极简注释
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type)
{
//-------------------------1.检查函数参数-------------------------
// 做一系列检查,检查参数是否有效,检查求解出来的签名对应的索引是否有效
//-------------------------2.求signal_index-----------------------
// 求信号函数名对应的索引值并检查是否有效
int signal_index = QMetaObjectPrivate::indexOfSignalRelative(
&smeta, signalName, signalTypes.size(), signalTypes.constData()); // 信号编号值
// 这里第一次求索引如果没有会求标准化的签名,注意前面的++signal其实对传入的参数做了修饰,这里不再展示了
if (signal_index < 0) {
//...
return QMetaObject::Connection(nullptr);
}
signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
signal_index += QMetaObjectPrivate::signalOffset(smeta); // 第一个信号的偏移值
//-------------------------3.求method_index_relative--------------
// 接受对象的方法索引(可能是槽或者另一个信号),和信号类似
int method_index_relative = -1;
switch (membcode) {
case QSLOT_CODE: //是一个槽函数
method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
&rmeta, methodName, methodTypes.size(), methodTypes.constData());
break;
case QSIGNAL_CODE: // 被连接的也是一个信号
method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
&rmeta, methodName, methodTypes.size(), methodTypes.constData());
break;
}
//-------------------------4.检查信号槽参数匹配-------------------
// 检查参数连接的两个函数的参数是否匹配
if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(),
methodTypes.size(), methodTypes.constData())) {
//...
return QMetaObject::Connection(nullptr);
}
//-------------------------5.连接-------------------------
QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));
return handle;
}
// Private做具体的connect构造
QObjectPrivate::Connection *QMetaObjectPrivate::connect(const QObject *sender,
int signal_index, const QMetaObject *smeta,
const QObject *receiver, int method_index,
const QMetaObject *rmeta, int type, int *types)
{
QObject *s = const_cast<QObject *>(sender);
QObject *r = const_cast<QObject *>(receiver);
// ...
QOrderedMutexLocker locker(signalSlotLock(sender),
signalSlotLock(receiver));
QObjectPrivate::ConnectionData *scd = QObjectPrivate::get(s)->connections.loadRelaxed();
if (type & Qt::UniqueConnection && scd) {
// 检查连接类型
// ...
}
// 构造connect对象
std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
// 设置connect信息
QObjectPrivate::get(s)->addConnection(signal_index, c.get()); // 添加connect
locker.unlock();
//...
return c.release();
}
// 数据写入connect列表
void QObjectPrivate::addConnection(int signal, Connection *c)
{
//...
ConnectionData *cd = connections.loadRelaxed();
cd->resizeSignalVector(signal + 1);
// 取出当前信号的connect链表,将connect加入其中
ConnectionList &connectionList = cd->connectionsForSignal(signal);
if (connectionList.last.loadRelaxed()) {
Q_ASSERT(connectionList.last.loadRelaxed()->receiver.loadRelaxed());
connectionList.last.loadRelaxed()->nextConnectionList.storeRelaxed(c);
} else {
connectionList.first.storeRelaxed(c);
}
//...
}
下面相对详细一些的源码,同样删除了部分次要逻辑,比如检查,回调等
c++
// Qt 源码的connect实现,相对细节多一点
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type)
{
// 做一系列检查,检查参数是否有效,检查求解出来的签名对应的索引是否有效
// 检查参数是否为空,检查宏使用是否正确
if (...) {
//...
return QMetaObject::Connection(nullptr);
}
//-----------------------------------------------------------
// 求信号函数名对应的索引值并检查是否有效
const QMetaObject *smeta = sender->metaObject();
const char *signal_arg = signal;
++signal; //skip code
QArgumentTypeArray signalTypes;
Q_ASSERT(QMetaObjectPrivate::get(smeta)->revision >= 7);
QByteArray signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes);
int signal_index = QMetaObjectPrivate::indexOfSignalRelative(
&smeta, signalName, signalTypes.size(), signalTypes.constData()); // 信号编号值
// 这里第一次求索引如果没有会求标准化的签名,注意前面的++signal其实对传入的参数做了修饰,这里不再展示了
if (signal_index < 0) {
//...
return QMetaObject::Connection(nullptr);
}
signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
signal_index += QMetaObjectPrivate::signalOffset(smeta); // 第一个信号的偏移值
//------------------------------------------------------------------
// 接受对象的方法索引(可能是槽或者另一个信号),和信号类似,这里简化了细节
//...
int method_index_relative = -1;
switch (membcode) {
case QSLOT_CODE: //是一个槽函数
method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
&rmeta, methodName, methodTypes.size(), methodTypes.constData());
break;
case QSIGNAL_CODE: // 被连接的也是一个信号
method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
&rmeta, methodName, methodTypes.size(), methodTypes.constData());
break;
}
if (method_index_relative < 0) {
//...
return QMetaObject::Connection(nullptr);
}
//-------------------------------------------------------------------
// 检查参数连接的两个函数的参数是否匹配
if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(),
methodTypes.size(), methodTypes.constData())) {
//...
return QMetaObject::Connection(nullptr);
}
// 如果是QueuedConnection,检查参数类型是否注册过了
int *types = nullptr;
if ((type == Qt::QueuedConnection)
&& !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) {
return QMetaObject::Connection(nullptr);
}
//...
//--------------------------------------------------------------
// 检查完毕,连接
QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));
return handle;
}
// qtbase/src/corelib/kernel/qobject.cpp的3391行
// Private做具体的connect构造
QObjectPrivate::Connection *QMetaObjectPrivate::connect(const QObject *sender,
int signal_index, const QMetaObject *smeta,
const QObject *receiver, int method_index,
const QMetaObject *rmeta, int type, int *types)
{
QObject *s = const_cast<QObject *>(sender);
QObject *r = const_cast<QObject *>(receiver);
// ...
QOrderedMutexLocker locker(signalSlotLock(sender),
signalSlotLock(receiver));
QObjectPrivate::ConnectionData *scd = QObjectPrivate::get(s)->connections.loadRelaxed();
if (type & Qt::UniqueConnection && scd) {
// 检查连接类型
// ...
}
// 构造connect对象
std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
c->sender = s;
c->signal_index = signal_index;
c->receiver.storeRelaxed(r);
QThreadData *td = r->d_func()->threadData;
td->ref();
c->receiverThreadData.storeRelaxed(td);
c->method_relative = method_index;
c->method_offset = method_offset;
c->connectionType = type;
c->isSlotObject = false;
c->argumentTypes.storeRelaxed(types);
c->callFunction = callFunction;
QObjectPrivate::get(s)->addConnection(signal_index, c.get()); // 添加connect
locker.unlock();
QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index);
if (smethod.isValid())
s->connectNotify(smethod);
return c.release();
}
// 数据写入connect列表
void QObjectPrivate::addConnection(int signal, Connection *c)
{
Q_ASSERT(c->sender == q_ptr);
ensureConnectionData();
ConnectionData *cd = connections.loadRelaxed();
cd->resizeSignalVector(signal + 1);
// 取出当前信号的connect链表,将connect加入其中
ConnectionList &connectionList = cd->connectionsForSignal(signal);
if (connectionList.last.loadRelaxed()) {
Q_ASSERT(connectionList.last.loadRelaxed()->receiver.loadRelaxed());
connectionList.last.loadRelaxed()->nextConnectionList.storeRelaxed(c);
} else {
connectionList.first.storeRelaxed(c);
}
c->id = ++cd->currentConnectionId;
c->prevConnectionList = connectionList.last.loadRelaxed();
connectionList.last.storeRelaxed(c);
QObjectPrivate *rd = QObjectPrivate::get(c->receiver.loadRelaxed());
rd->ensureConnectionData();
c->prev = &(rd->connections.loadRelaxed()->senders);
c->next = *c->prev;
*c->prev = c;
if (c->next)
c->next->prev = &c->next;
}
3.4信号的发送(调用)
c++
// SIGNAL 0
void Myclass::vualeChanged()
{
// 这里0表示当前信号的索引值,元数据解析的时候就已经确定的,后面是参数列表,应该没有参数,这里是nullptr
QMetaObject::activate(this, &staticMetaObject, 0, Q_NULLPTR);
}
// active的实现
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);
}
接下来是主要的处理信号到槽函数调用的逻辑,同样的,对细节不感兴趣的看一下简洁版的逻辑。 代码位于qtbase/src/corelib/kernel/qobject.cpp
的3766行
cpp
template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv)
{
//-----------------1.检查相关数据,参数判空-----------------
QObjectPrivate *sp = QObjectPrivate::get(sender);
// 如果信号被阻塞了,return
if (sp->blockSig)
return;
//-----------------2.取出connect队列------------------------
bool senderDeleted = false;
{
//...
const QObjectPrivate::ConnectionList *list; // connectList,这个信号上的连接
if (signal_index < signalVector->count())
list = &signalVector->at(signal_index);
else
list = &signalVector->at(-1);
//-------------------2.处理信号发送---------------------------------
// 线程数据,某些连接方式会根据信号发送和接受者是否在同一个线程做特殊处理,保证了信号槽的线程安全性
Qt::HANDLE currentThreadId = QThread::currentThreadId();
bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData.loadRelaxed()->threadId.loadRelaxed();
do {//connect的recv对象的方法
//...
do {//从链表中第一个接受的对象开始调用
//-------------------处理跨线程和特殊连接方式--------------
// 根据是否在一个线程和连接方式决定同步调用,还是异步调用
bool receiverInSameThread; // 判断发送和接收是否在一个线程内
//...
// 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) {
QSemaphore semaphore;
{
QBasicMutexLocker locker(signalSlotLock(sender));
//... 加锁,postEvent
QCoreApplication::postEvent(receiver, ev);
}
semaphore.acquire();//等待结果,阻塞式的异步调用
continue;
#endif
}
//------------------处理一般的信号调用-------------------------------
// 以上为处理特殊连接方式的异步调用,下面为主要流程,对应了几种不同连接方式的调用
QObjectPrivate::Sender senderData(receiverInSameThread ? receiver : nullptr, sender, signal_index);
if (c->isSlotObject) {
c->slotObj->ref();
struct Deleter {
void operator()(QtPrivate::QSlotObjectBase *slot) const {
if (slot) slot->destroyIfLastRef();
}
};
const std::unique_ptr<QtPrivate::QSlotObjectBase, Deleter> obj{c->slotObj};
{
Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.get());
obj->call(receiver, argv); // 调用
}
} else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
//we compare the vtable to make sure we are not in the destructor of the object.
//...
// begin callback...
{
// 在这里进行调用
Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
}
// end callback...
} else {
const int method = c->method_relative + c->method_offset;
//begin callback...
{
// 这里进行调用
Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
}
// end callback...
}
} while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);
} while (list != &signalVector->at(-1) &&
//start over for all signals;
((list = &signalVector->at(-1)), true));
//...
}
//...
}
下面带有更多细节的源码实现
cpp
// doActive的实现,这里的模板参数用来表示是否有记录信号发送的回调,这里先不关注,当是false就好
// 这个实现函数较长,除去次要的逻辑,这里用注释分为几部分方便阅读
template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv)
{
//-----------------检查相关数据,参数判空-----------------
QObjectPrivate *sp = QObjectPrivate::get(sender);
// 如果信号被阻塞了,return
if (sp->blockSig)
return;
// ... spycallback
void *empty_argv[] = { nullptr };
if (!argv)
argv = empty_argv;
//...
//------------------取出connect队列------------------------
bool senderDeleted = false;
{
Q_ASSERT(sp->connections.loadAcquire());
QObjectPrivate::ConnectionDataPointer connections(sp->connections.loadRelaxed());
QObjectPrivate::SignalVector *signalVector = connections->signalVector.loadRelaxed();
const QObjectPrivate::ConnectionList *list; // connectList,这个信号上的连接
if (signal_index < signalVector->count())
list = &signalVector->at(signal_index);
else
list = &signalVector->at(-1);
//-------------------处理信号发送---------------------------------
// 线程数据,某些连接方式会根据信号发送和接受者是否在同一个线程做特殊处理,保证了信号槽的线程安全性
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 {//connect的recv对象的方法
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();
}
// 根据是否在一个线程和连接方式决定同步调用,还是异步调用
// 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) {
if (receiverInSameThread) {
qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
"Sender is %s(%p), receiver is %s(%p)",
sender->metaObject()->className(), sender,
receiver->metaObject()->className(), receiver);
}
QSemaphore semaphore;
{
QBasicMutexLocker locker(signalSlotLock(sender));
if (!c->receiver.loadAcquire())
continue;
QMetaCallEvent *ev = c->isSlotObject ?
new QMetaCallEvent(c->slotObj, sender, signal_index, argv, &semaphore) :
new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction,
sender, signal_index, argv, &semaphore);
QCoreApplication::postEvent(receiver, ev);
}
semaphore.acquire();//阻塞式的异步调用
continue;
#endif
}
//------------------处理一般的信号调用-------------------------------
// 以上为处理特殊连接方式的异步调用,下面为主要流程
QObjectPrivate::Sender senderData(receiverInSameThread ? receiver : nullptr, sender, signal_index);
if (c->isSlotObject) {
c->slotObj->ref();
struct Deleter {
void operator()(QtPrivate::QSlotObjectBase *slot) const {
if (slot) slot->destroyIfLastRef();
}
};
const std::unique_ptr<QtPrivate::QSlotObjectBase, Deleter> obj{c->slotObj};
{
Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.get());
obj->call(receiver, argv); // 调用
}
} else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
//we compare the vtable to make sure we are not in the destructor of the object.
const int method_relative = c->method_relative;
const auto callFunction = c->callFunction;
const int methodIndex = (Q_HAS_TRACEPOINTS || callbacks_enabled) ? c->method() : 0;
// begin callback...
{
// 在这里进行调用
Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
}
// end callback...
} else {
const int method = c->method_relative + c->method_offset;
//begin callback...
{
// 这里进行调用
Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
}
// end callback...
}
} while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);
} while (list != &signalVector->at(-1) &&
//start over for all signals;
((list = &signalVector->at(-1)), true));
//...
}
//...
}
经过上述调用过程,最后落在callFunction(...)
和QMetaObject::metacall(...)
中,结合connect构造时的上下文,前者是静态函数qt_static_metacall,后者经过调用之后也会走到这里,最后的函数调用终究回到最开始提到的moc生成的代码中,这样就完成了一次信号槽函数的通讯。
4.连接方式,多线程,其他
信号槽的另一个优点就是,信号槽的调用是线程安全的。仔细观察connect的声明,我们会发现在最后面有一个参数表示连接类型,在doActivate中我们也发现,对于特殊连接类型,Qt 做了逻辑上的特殊处理 当前版本(5.x)的连接类型可以参考官网介绍,在之后的版本里面引入了额外的类型,有兴趣的可以自己查看。 根据官网的描述和代码中的逻辑(前文doActivate中跨线程处理部分),默认连接类型为Qt::AutoConnection
,该类型在信号处理时,会检查发送方和接收方是否在一个线程内,如果在一个线程内,使用直连的方式Qt::DirectConnection
,相当于一次同步的函数调用,如果不在一个线程中,你们会通过post的方式向队列里添加一个事件,进行一次异步的函数调用Qt::QueuedConnection
。 另外一种不常用的是Qt::BlockingQueuedConnection
,如果发送和接受方不在一个线程内,会进行一次异步回调,但是此时会阻塞当前信号进程,直到调用结束。 Qt::UniqueConnection
很好理解,表明一个信号上最多只能有一个连接,可以和其他几种连接方式配合使用。
在实际使用过程中,Qt 还提供了阻塞信号,断开信号连接等功能,属于使用问题,这里就不再讨论了。
5.总结
根据上面分析的内容,从观察者模式出发,在宏观上对基于元对象系统的信号槽连接方式有了一个较为完整的认识,它解决了一部分观察者模式的弊端,使用起来更加方便。其实现原理总结下来有以下几步:
- 为了使用 Qt 元对象系统,声明必要的宏定义,例如 Q_OBJECT,slots, signals 等,元对象编译器(moc)会解析文件,生成元数据并根据数据自动生成信号的代码
- connect 连接时,根据元对象数据将函数名连接转换为方法索引值连接存储在这个信号对应的连接关系容器中
- 发送信号时,遍历这个信号对应容器的所有连接对象,逐个执行方法调用