- 作者:老汪软件技巧
- 发表时间:2024-09-21 11:01
- 浏览量:63
这篇文章如下看懂了,面试的时候如果面试官问道关于信号和槽的原理,那就把细节告诉他。让他知道是有能力看源码的,不是简单的背背八股文就完事了的.
1. 测试代码
给出一段测试代码,用于后面的信号、槽源码分析
class A
class A : public QObject
{
Q_OBJECT
public:
A() = default;
~A() = default;
signals:
void Signal_A_one(); // 信号1
void Signal_A_two(); // 信号2
void Signal_A_three(); // 信号3
public slots:
void Slot_A_one() // 槽函数1
{
qDebug() << "slot a one";
}
void Slot_A_two() // 槽函数2
{
qDebug() << "slot a two";
}
};
class B
class B : public QObject
{
Q_OBJECT
public:
B() = default;
~B() = default;
signals:
void Signal_B_one(); // 信号1
void Signal_B_two(); // 信号2
void Signal_B_three(); // 信号3
public slots:
void Slot_B_one() // 槽函数1
{
qDebug() << "slot a one";
}
void Slot_B_two() // 槽函数2
{
qDebug() << "slot a two";
}
};
连接代码,这里将类A的信号绑定三个槽函数,使用了三种连接方式
A aa;
B bb;
// 三种connect连接方式
QObject::connect(&aa, &A::Signal_A_one, &bb, &B::Slot_B_one);
QObject::connect(&aa, SIGNAL(Signal_A_one), &bb, SLOT(Slot_B_two));
QObject::connect(&aa, &A::Signal_A_one, []{ qDebug() << "lambda slot"; });
2. connect函数源码分析2.1 类分析
connection函数底层会使用到几个类,有必要先了解一下,防止后面看源码不懂
QObject
class QObject
{
protected:
QScopedPointer d_ptr;
}
QObject只有这一个成员,成员类型是QObjectData类型
还有一个类QObjectPrivate继承自这个对象,在Qt源码中经常会用到这个类
class QObjectPrivate : public QObjectData
{
public:
struct ConnectionOrSignalVector;
struct Connection : public ConnectionorOrSignalVector;
struct ConnectionList;
struct Sender;
struct SignalVector : public ConnectionOrSignalVector;
struct ConnectionData;
public:
// 成员
QAtomicPointer threadData;
QAtomicPointer connections;
union {
QObject* currentChildBeingDeleted;
QAbstractDeclartiveData* declarativeData;
};
};
这个类里面定义了很多类,下面逐一介绍,以及每个成员变量的作用是什么:
ConnectionOrSignalVector
struct ConnectionOrSignalVector
{
union {
ConnectionOrSignalVector* nextInOrphanList;
Connection* next; // 指向下一个连接对象
};
};
ConnectionOrSignalVector是很多类的基类,这里面定义了一个联合体,其中next是用来指向下一个Connection连接对象的
Connection连接对象,这个类里面存储着关于发送者和接收者双方连接的一些信息,有当前线程数据、元调用函数、信号偏移值等.
struct Connection : public ConnectionOrSignalVector
{
// 二级指针,如果是最新绑定的,那么指向receiver其中的sender
// 否则就是指向上一个绑定的Connection对象
Connection** prev;
QAtomicPointer nextConnectionList; // 指向下一个Connection
Connectin* prevConnectionList; // 指向上一个Connection
QObject* sender; // 发送者对象
QAtomicPointer receiver; // 接收者对象
QAtomicPointer receiverThreadData; // 当前线程的数据
// 指向元调用函数,使用staticMetaObject对象还是qt_static_metacall函数
union {
StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase* slotObj;
};
QAtomicPointer<const int> argumentTypes;
QAtomicInt ref_; // 引用计数,默认为2(发送者和接收者)
uint id = 0;
ushort method_offset;
ushort method_relative;
signed int signal_index : 27; // 信号在继承体系中的偏移值
ushort connectionType : 3;
ushort isSlotObject : 1;
ushort ownArgumentTypes : 1;
};
上面只列出几个相对来说咱们感兴趣的成员,这里就不一一描述每个都是做什么的,只要知道这个类是有关连接的详细信息.
接下来就是存储连接信息数组的有关类,Qt底层会创建一个堆内存数组,这块内存的头部是SignalVector类型,剩余后面就是ConnectionList类型.
SignalVector记录有关这个数组的大小.ConnectionList是一个链表的结点信息,有first和last成员.搞明白这个需要等到下面的addConnection函数解析就明白了.
struct SignalVector : public ConnectionOrSignalVector
{
quintptr allocated;
};
struct ConnectionList
{
QAtomicPointer first;
QAtomicPointer last;
};
ConnectionData这个是QObjectPrivate类的成员.发送者或者接收要获取关于连接的信息,都要获取这个对象,它里面是保存着所有连接信息.
struct ConnectionData
{
QAtomicInteger currentConnectionId;
QAtomicInt ref;
QAtomicPointer signalVector; // 信号向量
Connection *senders = nullptr; // 发送者
Sender *currentSender = nullptr;
QAtomicPointer orphaned;
};
2.2 函数源码分析
qt提供了三种连接方式,分别使用函数指针、宏、lambda,这俩就不再特别细的讲每种的区别,只要最底层核心细节,是怎么进行连接的.
这里就以函数指针的连接方式,使用这个来进行分析讲解,三种方式底层都是一样的.所以看懂了一种就可以了
connectImpl函数细节,代码会有一点长,分段讲解:
[第一段] 检查判断,如果指定是唯一连接判断是否已经重复连接了
QMetaObject::Connection QObjectPrivate::connectImpl(const QObject *sender, int signal_index,
const QObject *receiver, void **slot,
QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type,
const int *types, const QMetaObject *senderMetaObject)
{
// 忽略检查判断代码
QObject *s = const_cast(sender);
QObject *r = const_cast(receiver);
// 给发送者和接收者都上锁,保证线程安全
QOrderedMutexLocker locker(signalSlotLock(sender),
signalSlotLock(receiver));
if (type & Qt::UniqueConnection && slot && QObjectPrivate::get(s)->connections.loadRelaxed())
{
QObjectPrivate::ConnectionData *connections =
QObjectPrivate::get(s)->connections.loadRelaxed();
if (connections->signalVectorCount() > signal_index)
{
const QObjectPrivate::Connection *c2 =
connections->signalVector.loadRelaxed()->at(signal_index).first.loadRelaxed();
while (c2)
{
if (c2->receiver.loadRelaxed() == receiver &&
c2->isSlotObject && c2->slotObj->compare(slot))
{
slotObj->destroyIfLastRef();
return QMetaObject::Connection();
}
c2 = c2->nextConnectionList.loadRelaxed();
}
}
type = static_cast(type ^ Qt::UniqueConnection);
}
}
// 后续代码在下面讲解
第7~8行,去掉const属性,因为后面需要更改这两个指针,这俩指针分别是发送者对象和接收者对象
第11~12行,获取两把锁,用于发送者和接收者,保证连接过程中是线程安全的.
第13~33行,如果指定了是唯一连接UniqueConnetion,那么则只能有一个同样的连接.比如如下代码,是可以重复连接,那么发射信号的时候也就执行两次槽函数
QObject::connect(&aa, &A::Signal_A_one, &bb, &B::Slot_B_one);
QObject::connect(&aa, &A::Signal_A_one, &bb, &B::Slot_B_one);
// 这里会调用两次B::Slot_B_one
emit aa.Signal_A_one();
第13~33行详细分析如下:
13行: 如果连接类型指定了UniqueConnection代表只能有这唯一连接,不能重复. 然后接着判断之前是否已经有个连接信息
15~16行: 获取连接对象信息,该对象保存着所有的连接信息
17行: 判断当前信号向量数组的大小是否大于参数传递的信号下标,如果大于,那么则有可能之前已经有过关于这个信号和某个槽的连接
19行: 获取关于这个信号下标在信号向量数组中对应的连接对象信息Connection
22行: 如果循环条件成立,那么遍历这个链表,看是否以前有过跟当前相同的连接信息,如果有的话最终28行返回退出函数
[第二段] 创建连接新消息,并加到信号向量数组中,这都是在发送者对象数据里进行操作
std::unique_ptr c{new QObjectPrivate::Connection};
c->sender = s; // 设置发送者对象
c->signal_index = signal_index; // 设置信号索引,这个是在整个继承体系中的索引
QThreadData *td = r->d_func()->threadData; // 获取当前线程的数据
td->ref(); // 增加引用计数
c->receiverThreadData.storeRelaxed(td); // 存储线程数据
c->receiver.storeRelaxed(r); // 设置接收者对象
c->slotObj = slotObj; // 设置槽函数指针
c->connectionType = type; // 设置连接类型
c->isSlotObject = true;
if (types) // 如果连接类型是队列或者阻塞类型,那么types就是槽函数中的参数类型列表
{
// 存储参数的类型
c->argumentTypes.storeRelaxed(types);
c->ownArgumentTypes = false;
}
QObjectPrivate::get(s)->addConnection(signal_index, c.get());
QMetaObject::Connection ret(c.release());
locker.unlock();
QMetaMethod method = QMetaObjectPrivate::signal(senderMetaObject, signal_index);
Q_ASSERT(method.isValid());
s->connectNotify(method);
return ret;
第1行: 创建一个Connection对象,用来设置有关此次连接的一些信息,比如发送者、接收者、信号和槽的一些信息
第2~10行: 设置有关连接的信息,每个作用看上面代码注释
第18~24行: 就是底层的核心,用来添加连接对象.其中核心函数就是addConnection,这个函数就是操作上面讲到的那些类
第18行: 调用QObjectPrivate类的addConnection函数,传递两个参数,分别是信号索引和创建的连接对象,我们进去看这个函数具体做了什么
void QObjectPrivate::addConnection(int signal, Connection *c)
{
ensureConnectionData(); // 如果没有ConnectionData对象则创建一个
ConnectionData *cd = connections.loadRelaxed();
cd->resizeSignalVector(signal + 1); // 创建信号向量数组
// signal信号索引作为数组下标,互殴信号向量数组中的ConnectionList对象
ConnectionList &connectionList = cd->connectionsForSignal(signal);
// 下面就是存储Connection的细节了
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行: 如果当前从来没有创建过ConnectionData对象,那么则调用ensureConnectionData创建一个,然后存储到QObjectPrivate类中的connections成员
第4行: 获取这个ConnectionData对象
第5行: 创建一个信号向量数组,大小是信号索引 + 1,这个函数代码细节如下:
第8行: 获取当前索引下标在信号向量数组中的ConnectList对象
第10~16行: 如果之前设置过last,那么将这次的Connection对象存储在connectList.last->nextConnectionList中,否则存储在connectionList.first中
第18行: 设置prevConnectionList指向上一个存储的连接对象信息
第19行: 设置connectionList.last存储为当前连接对象
第21~22行: 获取接收者的QObjectPrivate对象,并为接收者创建ConnectionData对象
第24行: 当前连接对象的prev指向接收者ConnectionData中的senders成员
第25行: 设置当前连接对象的next为接收者ConnectionData中sender指向的对象.如果是第一次连接,这个next会被设置为空.否则,这个next设置就是接收者中的sender
第26行: 这一行代码,就是将接收者的sender指向当前连接对象
第27~28行: 将上一个连接对象的prev指向自己
可能通过上面这些文字看不出来到底怎么连起来的,下面画个图增加清晰的认知,看底层这个信号向量和连接是怎么连接的
根据上面的连接代码
连接信息如下,一个信号和三个槽函数连接
第一次连接:
会给发送者创建信号向量数组,存储在QObjectPrivate类中的成员connections中,该成员类型为QAtomicPointer
根据signal_index获取信号向量数组中对应下标的ConnectionList对象
然后设置相应的指针,第一次连接如下图所示连接:
第二次连接:
这次再次同样的信号连接不同的槽函数
这回就清晰明了了,新连接相当于链表的头,然后和上一次连接连起来.后面关于这个信号的多次连接也都是这样.
注意: 结点的连接也都是线程安全的
所以这里引申出一道面试题,那就是qt的信号和槽底层是怎么实现的.
每个QObject类都有一个类型为QObjectData的指针成员d_ptr.QObjectPrivate继承自这个,所以可以把这个指针转换成QObjectPrivate类型.
我们会在发送者的QObjectPrivate成员对象中,创建一个信号向量数组,这个数组大小是根据信号在整个继承体系中的索引来确定的.数组的每个元素都是ConnectionList类型,该类型就是一个链表结点,这个结点的first存储第一次连接的对象信息, last存储最后一次连接的对象信息.
每次连接都会创建一个Connection对象,该对象存储着有关连接的信息,比如发送者对象、接收者对象、信号索引、发送者的元方法、槽函数等等. 最新创建的Connection相当于链表的头,next指向上一次创建的Connection对象.链表头指向接收者中的sender成员.
根据发送者的信号索引获取信号向量数组中对应下标的ConnectionList对象,然后设置first和last指向第一次连接和最后一次连接的对象.
这也就构成了每个信号都有属于自己的链表,每个结点都是这个信号和什么槽函数绑定的信息.
3. emit发射信号是如何执行绑定的槽函数的
我们有了上面的链表信息,其实触发信号就是遍历这些链表,使用接收者对象指针调用对应的槽函数,如果有参数,则把参数也传递进去.对于跨线程会使用队列保证异步安全的,具体函数细节就不再分析了.
这里只讲下emit到底是什么东西,居然可以使槽函数执行
emit aa.Signal_A_one(1, 1.1);
emit本质上是个空宏,什么都没有.上面这段代码,就是调用Signal_A_one函数,这个函数是moc编译器帮我们生成的.moc编译器emit,就会在moc文件里生成一个一个函数
// SIGNAL 0
void A::Signal_A_one(int _t1, double _t2)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))),
const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t2))) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
最终会调用active函数,这个函数就是遍历发送者的连接链表,然后根据接收者对象指针调用对应的槽函数.
而且信号和槽函数是线程安全的.内部有处理有关不是同一个线程的方法,使用事件队列异步激活槽函数