Qt信号槽原理(链接)

一个简单的例子

Sender

class TestSender : public QObject
{
    Q_OBJECT
public:
    TestSender(QObject *parent);
    ~TestSender();
signals:
    void test_sender(int);
};

Sloter

class TestSlot : public QObject
{
    Q_OBJECT
public:
    TestSlot(QObject *parent);
    ~TestSlot();
public slots:
    void test_slot(int);
};

Connection

    TestSender sender(nullptr);
    TestSlot sloter(nullptr);
    QObject::connect(&sender, &TestSender::test_sender, &sloter, &TestSlot::test_slot);
    emit sender.test_sender(1);

那么Qt是怎么将这两个信号槽链接起来的呢?接下来看一看connect的源码:
在Qt中connect有很多个重载,大致可以分为2类:一种是qt4的Sender(),Slot()方式,另一种是qt5的模板方式进行链接

1618825499(1).png

1、qt4的链接方式

    TestSender sender;
    TestSlot receiver;
    QObject::connect(&sender, SIGNAL(test_Signal(int)), &receiver, SLOT(test_Slot(int)));
    sender.test_Signal(1);

用这种方式链接会调用上图中的第一个connect函数,其内部的实现如下(太多了我们分段看看)

1)、
1619078740(1).jpg

这一部分主要是在检测传入的信号是否属于sender,不是的话会告警并直接返回,这也是这种方式不好的地方,必须写正确,而且写错了如果不看告警的话是不知道的。
这里面两个主要的函数:


1619079418(1).jpg

我们看看他们是如何实现的


1619079739(1).jpg

第一个主要是检查和获取信号的名字,同时获取形参的个数,都是通过字符串解析的,(也就是SIGNAL转换完成后的字符串,比如这个地方是“test_Signal(int)”)
1619079815(1).jpg

这个主要是获取信号在信号列表里的索引位置,如何获取的呢?
1619081043(1).jpg

通过Qt的元对象(QMetaObject)获取,到这里需要讲一讲QMetaObject是个什么东西,它是个类,内部有一个结构体d如下:


1619081227(1).jpg

那这个元对象实在哪生成的呢,答案在moc生成的文件里:
image.png

这个staticMetaObject就是元对象的根本,这个东西咋来的又是啥,答案在Q_OBJECT宏里面,看看Q_OBJECT宏是如何实现的,为什么一定要定义这个宏才能实现Qt的一些特性
image.png

看到了吧,这是一个静态成员,所有的继承类共享这个成员,也是Qt元对象的核心(也是为什么所有的类都要继承自QObject)。那么不继承自QObject能用Qt的元对象吗,答案肯定可以,Qt又提供了另一个宏Q_GADGET,其实也就是自己定义了一个类似QObject的类,最重要的是要有元对象(QMetaObject)
image.png

OK,继续回去看staticMetaObject这个成员的初始化,我们在这里先只关注stringdata和data这个两个参数,这里用到了,后面会继续有其他参数的意义,在这个地方stringdata被初始化成了qt_meta_stringdata_TestSender.data,data被初始化成了qt_meta_data_TestSender。它们是啥,还是在moc文件里


1619082162(1).jpg

1619082393(1).jpg

这两个一个保存着信号的名字,一个保存着信号的返回值,参数类型和参数个数。
到这里就可以通过这两个参数和链接时传入的signal字符串对比找到信号在sender中的的位置,当然因为继承的存在,所以这个位置不是真是的位置,还需要找到signalOffset这个函数找到在这个类中的位置。怎么找的呢?其实MetaObject中的data最终会被转换成这个


image.png

仔细看看就能和moc文件里的注释对应上,每个类都保存了有多少个信号和对应的参数。

因为QObject有2个信号,所有这个地方获取到的索引位置为3

2)OK,我们回到connect函数继续放下看
image.png

这一段跟上面差不多,用来检车receiver和槽函数是否对应

3)继续往下看
image.png

这个地方主要是检查信号槽的参数(比如信号的参数不能比槽的参数少等)
接下来就会进入另一个connect函数,他的实现


image.png

到这里就要看到信号槽是如何链接起来的了,每个QObject的对象都会维护着多个Connection的链接对象,这里面存储着信号的发送者、信号的接收者等一系列信息


image.png

image.png

这里会用的元对象的另一个成员static_metacall,它又是啥呢,答案还在moc文件里
image.png

这个最终会在触发信号的时候调用到这个函数,最终调用到我们的槽函数,如果不记得这东西怎么传进去的往前翻一番。

至此信号差就链接起来了,最终会以Connection对象的方式存放在每个QObject对象的connectionlist里面,这种是Qt4老的链接方式

2、qt5的链接方式

Qt5的新链接方式相比Qt4要简单很多,因为是基于模板实现的,因此可以直接找到函数的地址,最终也会放到链接列表里,来看看与Qt4的区别吧。
不知道是否还记得最开始connect函数的几种重载,这里用到的是下面红框里的方式(模板)
让我们先看一种


1619084610(1).jpg

它会调用这个实例化的模板函数


image.png

由于基于模板实现的,因此在编译的时候就能发现错误,如果参数不对,或者函数不是对象的成员函数都会在编译期间发现错误。具体模板是怎么推导的这里就不多讲了。
上面的connect函数会调用到connectImpl,它是这样的
image.png

这里面有个与之前讲的一样的地方是需要获取信号函数在对象中的偏移量,在Qt5中它是怎么获取的呢,答案肯定还是在元对象上,这里就用到了元对象的另一个成员static_matacall,它是啥呢,答案肯定还是在moc文件里
image.png

看到了吧,这个函数有2个功能,一个是执行信号(因为信号可能会很信号链接,具体怎么执行到的后面会讲),另一个是获取信号函数在对象的偏移量(如何获取的呢,其实就是对比传入的信号函数的地址和实际信号的地址,这里a0就是要获取的偏移量,a1就是信号函数的地址)

这个地方需要注意的是connecImpl与connect不同的是吧receiver对象包装成了一个QSlotObject的对象,它是这样的


image.png

connect

这个类接受一个QtPrivate::FunctionPointer的对象,这个是个啥?着其实是个模板类,用来识别槽函数的地址,在connect的时候也用到了这个,具体怎么实现的可以放出来,就是模板的推导比较麻烦,可以简单的理解成通过这个可以判断槽函数是否是receiver的成员函数


image.png

我们的槽函数实际是存储在 QSlotObject的function对象,最终调用的时候是通过call函数调用到了槽函数,这个后面在讲。

接着往下讲,不知道是否还记得之前的connectImpl函数,它获取到了信号的偏移量,接着又调用了一个connectImpl,它是这样的


image.png

看到这里有没有熟悉的感觉,跟Qt4的其实差不多,也是将信号槽存储到connection对象内,在添加到sender的connectionList里,不过这里存储的槽不是一个函数,而是一个QSlotObject对象的地址,看看Connection结构的内容可以发现使用Union实现的,存储的都是一个地址
到这里就讲完了Qt4和Qt5信号槽的链接部分,不知道是否讲的明白,moc文件里生成的东西基本就是Qt信号槽的核心,每一个都有其作用,都对应着QObjcet底层的数据,也是Qt元对象的核心。

你可能感兴趣的:(Qt信号槽原理(链接))