Qt 信号和槽源码分析

作者:曹群
原文:https://mp.weixin.qq.com/s/Mp...
欢迎关注学而思网校技术团队公众号:

clipboard.png

引言:

Qt 是一个1991年由Qt Company开发的跨平台C++图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。Qt是面向对象的框架,使用特殊的代码生成扩展(称为元对象编译器(Meta Object Compiler, moc))以及一些宏,Qt很容易扩展,并且允许真正地组件编程。Qt是跨平台开发框架,支持Windows、Linux、MacOS等不同平台;Qt有大量的开发文档和丰富的API,给开发者带来了很大的方便;Qt的使用者也越来越多,有很多优秀的产品都基于Qt开发,如:WPS Offic 、Opera浏览器、Qt Creator等。Qt的核心机制就是信号和槽,接下来我们通过源代码分析一下实现原理。

基本概念:

信号:当对象改变其状态时,信号就由该对象发射 (emit) 出去,而且对象只负责发送信号,它不知道另一端是谁在接收这个信号。
槽:用于接收信号,而且槽只是普通的对象成员函数。一个槽并不知道是否有任何信号与自己相连接。
信号与槽的连接:所有从 QObject 或其子类 ( 例如 QWidget ) 派生的类都能够包含信号和槽。是通过静态方法:QObject::connect(sender, SIGNAL(signal), receiver, SLOT(slot)); 来进行管理的,其中 sender 与 receiver 是指向对象的指针,SIGNAL() 与 SLOT() 是转换信号与槽的宏。

实现原理:

1、首先我们搭建好环境,如在Windows系统上:安装Qt5.7(包括源码) + VS2013 及 对应的插件,我们主要是通过VS来进行编译调试的。

2、我们写一个简单实例,然后进行构建,再把Qt安装目录中的QtCored的pdb拷贝到我们的可执行文件目录下面,如下图所示:

下面是我们要分析的Demo代码:
// MainWindow.h

// MainWindow.cpp

我们可以创建一个Qt工程,名称为Demo,编写上面的代码,进行构建,在VS下可以把Qt工程导成VS工程,编译生成,运行结果如下:

点击中间的按钮,我们可以看到控制台打印如下信息:

第一步:基本结构:

我们分析代码,可以看到在头文件Test和MainWindow类中,都有Q_OBJECT这样的宏,然后我们可以看到上面的可执行文件夹下多出来一个moc_MainWindow.cpp文件,那么我们可以尝试把这两个宏去掉,再进行构建,发现加上了信号和槽的就无法编译过去,我们去掉这些信号和槽后,就不会生成moc开头的这个文件了,当然我们就无法实现信号和槽机了,那么这个宏到底是什么,有了它编译器又会做什么?让我们看看这个宏:

原来这个宏就是一些静态方法和虚方法,但是如果我们加入到类中,不进行实现,那一定会报错的,为什么还可以正常运行呢?原来Qt帮我们做了很多事情,在编译器编译Qt代码之前,Qt先将Qt自身扩展的语法进行翻译,这个操作是通过moc(Meta-Object Compiler)又称“元对象编译器”完成的。首先moc会分析源代码,把包含Q_OBJECT的头文件生成为一个C++源文件,这个文件的名字会是源文件名前面加上moc_,之后和原文件一起通过编译器处理,那我们想到,这个moc开头的cpp中一定实现了上面宏里面的方法,以及数据的赋值;接下来我们看看moc_MainWindow.cpp这个文件:

我们从上面的代码中可以看到,是对Q_OBJECT中的静态数据进行了赋值,并且实现了那些方法,这些都是Qt的moc编译器帮我们生成的,对代码进行了分析,对信号和槽生成了符号,以及特定的数据结构,下面这个主要是记录了类、信号、槽的引用计数、大小、偏移,后面会用到。

通过把QT_MOC_LITERAL这个宏进行替换后,得到如下数据 :

接下来我们看看下面qt_meta_data_MainWindow这个数组结构:content有两列,第一列是总数,第二列是在这个数组中描述开始的索引,如1, 14, // methods,说明有一个methods,我们可以看到slots就是从索引14开始的。

从最上面的源代码中我们可以看到再关联信号和槽的时候,用到了SIGNAL和SLOT这两个宏,那么这两个宏到底有什么作用呢?我们分析一下:

分析:
从上面我们可以看到其实这两个就是一个字符串拼接的宏,会在信号(signal)前面拼接"2",如”2clean()“;会在槽(slots)前面拼接"1",如”1onClean()“; 其中,qFlagLocation这个方法主要是把method存储在QThreadData里面FlaggedDebugSignatures中的const char* locations[Count];表中,用于定位代码对应的行信息。

预编译后如下:

通过上面的一些基本宏、数据结构的介绍,我们知道Qt给我们做了很多工作,帮我们生成了moc代码,给我们提供了一些宏,让我们开发简洁方便,那么Qt又是如何把信号和槽进行关联的呢,就是两个不同的实例,又是如何进行通过信号槽机制进行通信的呢?接下来我们看看信号和槽关联的实现原理:

第二步、信号和槽的关联:

1、检先对信号和槽的字符串进行检查,QSIGNAL_CODE 是 1 ;SIGNAL_CODE 是 2。

2、获取元数据(sender和receiver同理)。

这个方法就是我们上面moc_MainWindow.cpp中。

我们根据调试可以看到QObject::d_ptr->metaObject是空的,所以这样smeta就是上面这个staticMetaObject变量了。

// 首先我们得了解一下这个QMetaObject 和 QMetaObjectPrivate 的定义:

在Qt中为了实现二进制兼容性,一般会定义一个私有类,QMetaObjectPrivate就是QMetaObject的私有类,QMetaObject负责一些接口实现,QMetaObjectPrivate具体进行实现,这两个类一般是通过P指针和D指针进行组合式的访问,有一个宏:

我们看上面的staticMetaObject是一个QMetaObject类型的变量,其中QMetaObject进行了赋值:

1)&QWidget::staticMetaObject(父对象的MetaObject)-> superdata
2)qt_meta_stringdata_Test.data -> stringdata
3)qt_meta_stringdata_Test() -> data
4)qt_static_metacall(回调函数)->static_metacall

其中QMetaObject 是对外的结构,里面的connect方法最终调用的还是QMetaObjectPrivate里面的connect进行实现的。QMetaObject里的d成员填充了上面的staticMetaObject数据,而QMetaObjectPrivate里面的成员填充qt_meta_stringdata_Test数组中的数据,我们可以看到填充前14个数据,这也是moc生成methodData时以14为基数的原因了,转换方法如下:

3、对信号参数、名称进行获取和保存,如下,把信号的参数保存起来,返回方法名称。

4、计算索引(包括基类)。

具体实现如下:

其中int handle = priv(m->d.data)->methodData + 5i; 我们可以分析,其实就是14+5i ,那为什么是5呢?因为:
// signals: name, argc, parameters, tag, flags
1, 0, 24, 2, 0x06 / Public /,
// slots: name, argc, parameters, tag, flags
3, 0, 25, 2, 0x08 / Private /,
我们可以看到每一个signals或者slots都有5个整形表示。

5、对掩码进行检查。

// MethodFlags是一个枚举类型,我们可以看到MethodSignal = 0x04, MethodSlot = 0x08;

// slots: name, argc, parameters, tag, flags
3, 0, 25, 2, 0x08 / Private /,

6、判断链接类型,默认是Qt::AutoConnection。

enum ConnectionType {

AutoConnection,

DirectConnection,

QueuedConnection,

BlockingQueuedConnection,

UniqueConnection = 0x80

};

我们介绍一些连接类型:
1、AutoConnection:自动连接:默认的方式,信号发出的线程和糟的对象在一个线程的时候相当于:DirectConnection, 如果是在不同线程,则相当于QueuedConnection。
2、DirectConnection:直接连接:相当于直接调用槽函数,但是当信号发出的线程和槽的对象不再一个线程的时候,则槽函数是在发出的信号中执行的。
3、QueuedConnection :队列连接:内部通过postEvent实现的。不是实时调用的,槽函数永远在槽函数对象所在的线程中执行。如果信号参数是引用类型,则会另外复制一份的。线程安全的。
4、BlockingQueuedConnection:阻塞连接:此连接方式只能用于信号发出的线程 和 槽函数的对象不再一个线程中才能用,通过信号量+postEvent实现的,不是实时调用的,槽函数永远在槽 函数对象所在的线程中执行,但是发出信号后,当前线程会阻塞,等待槽函数执行完毕后才继续执行。
5、UniqueConnection :防止重复连接。如果当前信号和槽已经连接过了,就不再连接了。

最后到了信号和槽关联核心的地方了:

首先,我们先得了解以下数据结构:

上面的这三个数据结构很重要,QObject是我们最熟悉的基类,QObjectPrivate是它的私有类,进行具体实现,QObjectPrivate继承自QObjectData,在QObject里面以组合的形式也进行P指针和D指针的方式进行访问的。在信号和槽关联过程中,数据结构Connection是很重要的数据结构,下面的这个结构是ConnectionList的一个Vector:

有了上面的数据结构,我们就可以分析下面的链接过程了, 我们看到下面的先是调用的QMetaObjectPrivate的connect, 之后又用QMetaObject::Connection进行了指针包装:

QObjectPrivate::get(s) 方法其实就是获取了一个QObjec里面的QObjectPrivate实例,之后调用addConnection方法添加到链表中:

结构如下:

分析:
1、每个QObject对象都有一个QObjectConnectionListVector结构,这是一个Vector容器,它里面的基本单元都是ConnectionList类型的数据,ConnectionList的个数与该QObject对象的signal个数相同。每个ConnectionList对应一个信号,它记录了连接到这个信号上的所有连接。前面已经看到ConnectionList的定义中有两个重要成员:first和last,他们都是Connection 类型的指针,分别指向连接到这个信号上的第一个和最后一个连接。所有连接到这个信号上的连接以单向链表的方式组织了起来,Connection结构体中的nextConnectionList成员就是用来指向这个链表中的下一个连接的。
2、同时,每个QObject对象还有一个senders成员,senders是一个Connection类型的指针,senders本身也是一个链表的头结点,这个链表中的所有结点都是连接到这个QObject对象上的某个槽的连接。不过这个链表跟上一段提到的链表可不是同一个,虽然他们可能有一些共同结点。
3、每一个Connection对象都同时处于两个链表当中。其中一个是以Connection的nextConnectionList成员组织起来的单向链表,这个单项链表中每个结点的共同点是,他们都依赖于同一个QObject对象的同一个信号,这个链表的头结点就是这个信号对应的ConnectionList结构中的first;另一个链表是以Connection的next和prev成员组织起来的双向链表,这个双向链表中每个结点的共同点是,他们的槽都在同一个QObject对象上,这个链表的头结点就是这个Qobject对象的sender。这两个链表会有交叉(共同结点),但他们有不同的链接指针,所以不是同一个链表。

4、在Connect的时候,就是先new一个Connection对象出来,设置好这个连接的信息后,将它分别添加到上面提到的两个链表中;disconnect的时候,就从从这两个链表中将它移除,然后delete掉。而当一个QObject对象被销毁的时候,它的sender指针指向的那个双向链表中的所有连接都会被逐个移除!

第三步、发送信号到接受信号:

1、我们点击上面的button后,然后调用到onDestory槽里面, 这是我们写的信号触发的地方:

2、接下来就进入了moc_MainWindow.cpp里面的代码,调用了QMetaObject的静态方法activate:

// 然后进入真正的QMetaObject::activate

我们的例子是Autoconntion模式,所以就会执行下面的代码进行回调:

我们终于看到了,函数进行了回调到moc_MainWindow.cpp里面,然后调用对应的槽onClean ;

最终调用到这里后,打印输出:"MainWindow::onClean"

最后就是调用完后,会回到onDestory这里:

注意:如果我们在onClean中进行了对m_testWidget对象的释放操作(delete m_testWidget),再到onDestory()中 emit clean(); 后面进行访问成员,那么一定崩溃,所以要注意。

参考文献:

1、https://woboq.com/blog/how-qt...

2、Qt5.7源码

3、自己用C++实现的信号和槽demo:http://note.youdao.com/notesh...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章