目录
一, 问题
二, Moc
1, 变量
2,Q_OBJECT 展开后的函数声明
3, 自定义信号
三, connect
四, 信号触发
1, 直连
2, 队列连接
五, 总结
一, 问题
学习 Qt 有一段时间了, 信号槽用的也是 666, 可是对信号槽的机制还是一知半解, 总觉着不是那么得劲儿, 万一哪天面试被问到了还说不清楚, 那岂不是很尴尬. 最近抽空研究了下 Qt 的信号和槽进制, 结果发现也不是那么难嘛! 不管是同步还是异步, 说白了都是函数回调, 只是回调的地方变了而已
首先, 我们先看如下几个问题, 认真的思考下, 从以前的知识储备中尝试回答他们, 如果说这几个问题你都很清楚, 那么恭喜你, 你不适合看这篇文章.
moc 预编译在干嘛
signals 和 slots 关键字产生的理由
信号槽连接方式有什么区别
信号和槽函数有什么区别
connect 到底干了什么
信号触发原理
下面我们就分模块来讲述下 Qt 的信号槽, 首先分析下 Moc 他到底干了什么, 如果没有他信号槽还能行吗? 接着我们在来分析下最常用的 connect 函数, 最后在看下信号执行后是怎么触发槽函数的?
二, Moc
qt 中的 moc 全称是 Meta-Object Compiler, 也就是 "元对象编译器", 当我们编译 C++
文件时, 如果类声明中包含了宏 Q_OBJECT, 则会生成另外一个 C++ 源文件, 也就是我们经常看到的 moc_xxx.cpp 文件, 执行流程可能会像这样.
Q_OBJECT 是一个非常重要的宏, 他是 Qt 实现元编译系统的一个关键宏, 这个宏展开后, 里边包含了很多 Qt 帮助我们写的代码, 包括了变量定义, 函数声明等等, 下边是一个测试例子, 是我用 moc 命令生成的一个 moc 文件.
分析下面这个几个变量和函数, 将有助于我们更好的理解元编译系统
1, 变量
- static const qt_meta_stringdata_completerTst_t qt_meta_stringdata_completerTst: 存储函数列表
- static const uint qt_meta_data_completerTst: 类文件描述
2,Q_OBJECT 展开后的函数声明
以下 5 个函数都是使用 Q_OBJECT 宏自动生成的
- - void xxx::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
- - const QMetaObject xxx::staticMetaObject
- - const QMetaObject *xxx::metaObject()
- - void *xxx::qt_metacast(const char *_clname)
- - int xxx::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
为了更好的理解这 5 个函数, 我们首先需要引入一个 Qt 元对象, 也就是 QMetaObject, 这个类里边存储了父类的源对象, 我们当前类描述, 函数描述和 qt_static_metacall 函数地址.
a,qt_static_metacall
很重要, 根据函数索引进行调用槽函数, 这块需要注意一个很大的细节问题, 这个回调中, 信号和槽都是可以被回调的, 自动生成代码如下
- if (_c == QMetaObject::InvokeMetaMethod) {
- completerTst *_t = static_cast<completerTst *>(_o);
- Q_UNUSED(_t)
- switch (_id) {
- case 0: _t->lanuch(); break;
- case 1: _t->test(); break;
- default: ;
- }
- }
lanch 是一个信号声明, 但是却也可以被回调, 这也间接的说明了一个问题, 信号是可以当槽函数一样使用的.
b,staticMetaObject
构造一个 QMetaObject 对象, 传入当前 moc 文件的动态信息
c,metaObject
返回当前 QMetaObject, 一般而言, 虚函数 metaObject() 仅返回类的 staticMetaObject 对象.
d,qt_metacast
是否可以进行类型转换, 被 QObject::inherits 直接调用, 用于判断是否是继承自某个类. 判断时, 需要传入父类的字符串名称.
e,qt_metacall
调用函数回调, 内部还是调用了 qt_static_metacall 函数, 该函数被异步处理信号时调用, 或者 Qt 规定的有一定格式的槽函数 (on_xxx_clicked()) 触发, 异步调用代码如下所示
- void QMetaCallEvent::placeMetaCall(QObject *object)
- {
- if (slotObj_) {
- slotObj_->call(object, args_);
- } else if (callFunction_ && method_offset_ <= object->metaObject()->methodOffset()) {
- callFunction_(object, QMetaObject::InvokeMetaMethod, method_relative_, args_);
- } else {
- QMetaObject::metacall(object, QMetaObject::InvokeMetaMethod, method_offset_ + method_relative_, args_);
- }
- }
3, 自定义信号
下面这个函数是我们自己定义的一个信号, moc 命令帮我们生成了一个信号函数实现, 由此可见, 信号其实也是一个函数, 只是我们只管写信号声明, 而信号实现 Qt 会帮助我们自动生成; 槽函数我们不仅仅需要写函数声明, 函数实现也必须自己写.
- void xxx::lanuch(): 自定义信号
这里 Qt 怎么会知道我们定义了信号呢? 这个也是文章开头我们提出的第 2 个问题. 答案就是 signals, 当 Qt 发现这个标志后, 默认我们是在定义信号, 它则帮助我们生产了信号的实现体, slots 标志是同样的道理, Qt 元系统用来解析槽函数时用的.
我们在 C++ 文件中添加了编译器不认识的关键字, 这个时候编译为什么会没有报错呢?
因为我们使用了 define 宏定义, 定义了这个关键字
# define signals
三, connect
上面我们分析了 moc 系统帮助我们生成的 moc 文件, 他是实现信号槽的基础, 也是关键所在, 这一小节我们来了解下我们平时使用最多的 connect 函数, 看看他到底干了些什么.
当我们执行 connect 时, 实际上他可能像这样的执行流程
从这张图上我们可以看到, connect 干的事情并不多, 好像就是构造了一个 Connection 对象, 然后存储在了发送者的内存中, 具体存储了哪些内容, 可以看下面代码, 这是我从 Qt 源码中沾出来的部分代码.
- QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection);
- c->sender = s; // 发送者
- c->signal_index = signal_index;// 信号索引
- c->receiver = r;// 接收者
- c->method_relative = method_index;// 槽函数索引
- c->method_offset = method_offset;// 槽函数偏移 主要是区别于多个信号
- c->connectionType = type;// 连接类型
- c->isSlotObject = false;// 是否是槽对象 默认是 true
- c->argumentTypes.store(types);// 参数类型
- c->nextConnectionList = 0;// 指向下个连接对象
- c->callFunction = callFunction;// 静态回调函数, 也就是 qt_static_metacall
- QObjectPrivate::get(s)->addConnection(signal_index, c.data());
上述代码中我只把关键代码贴出来了, Qt 的源码实现有很多异常判断我们这里不需要考虑
四, 信号触发
一切准备就绪, 接下来我们看看信号触发后, 是怎么关联到槽函数的
Qt 为我们提供了 5 种类型的连接方式, 如下
Qt::AutoConnection 自动连接, 根据 sender 和 receiver 是否在一个线程里来决定使用哪种连接方式, 同一个线程使用直连, 否则使用队列连接
Qt::DirectConnection 直连
Qt::QueuedConnection 队列连接
Qt::BlockingQueuedConnection 阻塞队列连接, 顾名思义, 虽然是跨线程的, 但是还是希望槽执行完之后, 才能执行信号的下一步代码
Qt::UniqueConnection 唯一连接
一般情况下, 我们都使用默认的连接方式, 除非一些特殊的需求, 我们才会主动指定连接方式. 当我们执行信号时, 函数的调用关系可能会像下面这样
emit testSignal(); 执行信号
信号触发后, 就相当于调用 QMetaObject::activate 函数, 信号的函数体是 moc 帮助我们自动生成的.
下面我们来分析下几个关键的连接方式, 他们都是怎么工作的
1, 直连
对于大多数的开发工作来说, 我们可能都是在同一个线程里进行的, 因此直连也是我们使用连接方式最多的一种, 直连说白了就是函数回调. 还记得我们第三小节讲的 connect 吗, 他构造了一个 Connection 对象, 存储在了发送者的内存中, 直连其实就是调用了我们之前存储在 Connection 中的函数地址.
如下图所示, 是一个直连时, 回调到槽函数中的一个内存堆栈.
讲 connect 函数时, 我们分析到, 该函数内部其实就是构造了一个 Connection 对象存储在了发送者内存中, 其中有一个变量是 isSlotObject, 默认是 true. 当我们使用 connect 连接信号槽时, 该参数默认就是一个 true, 但是 Qt 还提供了了另外一种规定格式的槽函数, 此时 isSlotObject 就是 false 啦.
如下图所示, 这是一个使用 Qt 规定格式的槽函数. 格式: on_objectname_clicked();.
2, 队列连接
connect 连接信号槽时, 我们使用 Qt::QueuedConnection 作为连接类型时, 槽函数的执行是通过抛出 QMetaCallEvent 事件, 经过 Qt 的事件循环达到异步的效果
如下图所示, 是使用队列连接时, 槽函数的回调堆栈
下面代码摘自 Qt 源码, queued_activate 函数即是处理队列请求的函数, 当我们使用自动连接并且接受者和发送者不在一个线程时使用队列连接; 或者当我们指定连接方式为队列时使用队列连接.
- // 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 ? argv : empty_argv, locker);
- continue;
五, 总结
讲了这么多, Qt 信号槽的实现原理其实就是函数回调, 不同的是直连直接回调, 队列连接使用 Qt 的事件循环隔离了一次达到异步, 最终还是使用函数回调
moc 预编译帮助我们构建了信号槽回调的开头 (信号函数体) 和结尾(qt_static_metacall 回调函数), 中间的回调过程 Qt 已经在 QOjbect 函数中实现
signals 和 slots 就是为了方便 moc 解析我们的 C++ 文件, 从中解析出信号和槽
信号槽总共有 5 种连接方式, 前四种是互斥的, 可以表示为异步和同步. 第五种唯一连接时配合前 4 种方式使用的
信号和槽本质上是一样的, 但是对于使用者来说, 信号只需要声明, moc 帮你实现, 槽函数声明和实现都需要自己写
connect 方法就是把发送者, 信号, 接受者和槽存储起来, 供后续执行信号时查找
信号触发就是一系列函数回调
来源: https://www.cnblogs.com/swarmbees/p/10816139.html