Qt 信号和槽源码分析

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

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() 是转换信号与槽的宏。

实现原理:

  • 一、首先咱们搭建好环境,如在Windows系统上:安装Qt5.7(包括源码) + VS2013 及 对应的插件,咱们主要是经过VS来进行编译调试的。
  • 二、咱们写一个简单实例,而后进行构建,再把Qt安装目录中的QtCored的pdb拷贝到咱们的可执行文件目录下面,以下图所示:

clipboard.png

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

clipboard.png

// MainWindow.cpp浏览器

clipboard.png

咱们能够建立一个Qt工程,名称为Demo,编写上面的代码,进行构建,在VS下能够把Qt工程导成VS工程,编译生成,运行结果以下:安全

clipboard.png

点击中间的按钮,咱们能够看到控制台打印以下信息:服务器

clipboard.png

第一步:基本结构:

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

clipboard.png

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

clipboard.png

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

clipboard.png

经过把QT_MOC_LITERAL这个宏进行替换后,获得以下数据 :工具

clipboard.png

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

clipboard.png

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

clipboard.png

分析:

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

clipboard.png

预编译后以下:

clipboard.png

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

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

clipboard.png

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

clipboard.png

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

clipboard.png

clipboard.png

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

咱们根据调试能够看到QObject::d_ptr->metaObject是空的,因此这样smeta就是上面这个staticMetaObject变量了。

clipboard.png

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

clipboard.png

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

clipboard.png

clipboard.png

咱们看上面的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为基数的缘由了,转换方法以下:

clipboard.png

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

clipboard.png

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

clipboard.png

具体实现以下:

clipboard.png

其中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个整形表示。

  • 五、对掩码进行检查。

clipboard.png

// MethodFlags是一个枚举类型,咱们能够看到MethodSignal = 0x04, MethodSlot = 0x08;

clipboard.png

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

  • 六、判断连接类型,默认是Qt::AutoConnection。
enum ConnectionType {

  AutoConnection,

  DirectConnection,

  QueuedConnection,

  BlockingQueuedConnection,

  UniqueConnection =  0x80

};

咱们介绍一些链接类型:

  • 一、AutoConnection:自动链接:默认的方式,信号发出的线程和糟的对象在一个线程的时候至关于:DirectConnection, 若是是在不一样线程,则至关于QueuedConnection。
  • 二、DirectConnection:直接链接:至关于直接调用槽函数,可是当信号发出的线程和槽的对象再也不一个线程的时候,则槽函数是在发出的信号中执行的。
  • 三、QueuedConnection :队列链接:内部经过postEvent实现的。不是实时调用的,槽函数永远在槽函数对象所在的线程中执行。若是信号参数是引用类型,则会另外复制一份的。线程安全的。
  • 四、BlockingQueuedConnection:阻塞链接:此链接方式只能用于信号发出的线程 和 槽函数的对象再也不一个线程中才能用,经过信号量+postEvent实现的,不是实时调用的,槽函数永远在槽 函数对象所在的线程中执行,可是发出信号后,当前线程会阻塞,等待槽函数执行完毕后才继续执行。
  • 五、UniqueConnection :防止重复链接。若是当前信号和槽已经链接过了,就再也不链接了。

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

首先,咱们先得了解如下数据结构:

clipboard.png

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

clipboard.png

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

clipboard.png

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

clipboard.png

结构以下:

clipboard.png

clipboard.png

分析:

  • 一、每一个QObject对象都有一个QObjectConnectionListVector结构,这是一个Vector容器,它里面的基本单元都是ConnectionList类型的数据,ConnectionList的个数与该QObject对象的signal个数相同。每一个ConnectionList对应一个信号,它记录了链接到这个信号上的全部链接。前面已经看到ConnectionList的定义中有两个重要成员:first和last,他们都是Connection 类型的指针,分别指向链接到这个信号上的第一个和最后一个链接。全部链接到这个信号上的链接以单向链表的方式组织了起来,Connection结构体中的nextConnectionList成员就是用来指向这个链表中的下一个链接的。
  • 二、同时,每一个QObject对象还有一个senders成员,senders是一个Connection类型的指针,senders自己也是一个链表的头结点,这个链表中的全部结点都是链接到这个QObject对象上的某个槽的链接。不过这个链表跟上一段提到的链表可不是同一个,虽然他们可能有一些共同结点。
  • 三、每个Connection对象都同时处于两个链表当中。其中一个是以Connection的nextConnectionList成员组织起来的单向链表,这个单项链表中每一个结点的共同点是,他们都依赖于同一个QObject对象的同一个信号,这个链表的头结点就是这个信号对应的ConnectionList结构中的first;另外一个链表是以Connection的next和prev成员组织起来的双向链表,这个双向链表中每一个结点的共同点是,他们的槽都在同一个QObject对象上,这个链表的头结点就是这个Qobject对象的sender。这两个链表会有交叉(共同结点),但他们有不一样的连接指针,因此不是同一个链表。
  • 四、在Connect的时候,就是先new一个Connection对象出来,设置好这个链接的信息后,将它分别添加到上面提到的两个链表中;disconnect的时候,就从从这两个链表中将它移除,而后delete掉。而当一个QObject对象被销毁的时候,它的sender指针指向的那个双向链表中的全部链接都会被逐个移除!

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

  • 一、咱们点击上面的button后,而后调用到onDestory槽里面, 这是咱们写的信号触发的地方:

clipboard.png

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

clipboard.png

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

clipboard.png

咱们的例子是Autoconntion模式,因此就会执行下面的代码进行回调:

clipboard.png

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

clipboard.png

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

clipboard.png

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

clipboard.png

注意:若是咱们在onClean中进行了对m_testWidget对象的释放操做(delete m_testWidget),再到onDestory()中 emit clean(); 后面进行访问成员,那么必定崩溃,因此要注意。

参考文献:

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

二、Qt5.7源码

三、本身用C++实现的信号和槽demo:http://note.youdao.com/notesh...

相关文章
相关标签/搜索