惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LangChain Blog
G
Google Developers Blog
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
Hugging Face - Blog
Hugging Face - Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
DataBreaches.Net
C
Check Point Blog
月光博客
月光博客
MongoDB | Blog
MongoDB | Blog
F
Fortinet All Blogs
MyScale Blog
MyScale Blog
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
博客园_首页
S
SegmentFault 最新的问题
The Cloudflare Blog
NISL@THU
NISL@THU
T
The Exploit Database - CXSecurity.com
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Help Net Security
大猫的无限游戏
大猫的无限游戏
Spread Privacy
Spread Privacy
Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
U
Unit 42
S
Schneier on Security
博客园 - 司徒正美
Help Net Security
Help Net Security
O
OpenAI News
PCI Perspectives
PCI Perspectives
T
Troy Hunt's Blog
Microsoft Azure Blog
Microsoft Azure Blog
Forbes - Security
Forbes - Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News | PayPal Newsroom
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threatpost
V
Vulnerabilities – Threatpost
美团技术团队
P
Palo Alto Networks Blog
TaoSecurity Blog
TaoSecurity Blog
V2EX - 技术
V2EX - 技术
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ

涛哥的博客

玩转QtQuick(2)-默认渲染器 | 涛哥的博客 玩转QtQuick(2)-默认渲染器 | 涛哥的博客 玩转QtQuick(1)-SceneGraph场景图简介 | 涛哥的博客 玩转QtQuick(1)-SceneGraph场景图简介 | 涛哥的博客 QQuickWidget中文输入法问题的正确解法 | 涛哥的博客 玩转Qml(18)-用户向导 | 涛哥的博客 玩转Qml(18)-用户向导 | 涛哥的博客 玩转Qml(17)-树组件的定制 | 涛哥的博客 玩转Qml(17)-树组件的定制 | 涛哥的博客 玩转Qt(15)-操控Web小车案例 | 涛哥的博客 玩转Qt(15)-操控Web小车案例 | 涛哥的博客 玩转Qt(14)-Qt与Web混合开发 | 涛哥的博客 玩转Qt(14)-Qt与Web混合开发 | 涛哥的博客 玩转Qt(13)-安卓5.12.4环境搭建 | 涛哥的博客 玩转Qt(13)-安卓5.12.4环境搭建 | 涛哥的博客 玩转Qt(12)-github-Actions缓存优化 | 涛哥的博客 玩转Qt(12)-github-Actions缓存优化 | 涛哥的博客 玩转Qt(11)-github-Actions自动化发行 | 涛哥的博客 玩转Qt(11)-github-Actions自动化发行 | 涛哥的博客 玩转Qt(10)-github-Actions自动化编译 | 涛哥的博客 玩转Qt(10)-github-Actions自动化编译 | 涛哥的博客 玩转Qt(9)-程序发布指南 | 涛哥的博客 玩转Qt(9)-程序发布指南 | 涛哥的博客 玩转Qt(8)-掌握信号槽使用细节 | 涛哥的博客 玩转Qt(8)-掌握信号槽使用细节 | 涛哥的博客 玩转Qt(7)-窥探信号槽的实现细节 | 涛哥的博客 玩转Qt(6)-认清信号槽的本质 | 涛哥的博客 玩转Qt(6)-认清信号槽的本质 | 涛哥的博客 玩转Qt(5)-理解事件循环 | 涛哥的博客 玩转Qt(5)-理解事件循环 | 涛哥的博客 玩转Qml(16)-移植ShaderToy | 涛哥的博客 玩转Qml(16)-移植ShaderToy | 涛哥的博客 玩转Qml(15)-着色器效果ShaderEffect | 涛哥的博客 玩转Qml(15)-着色器效果ShaderEffect | 涛哥的博客 玩转Qml(14)-动画特效-梯度 | 涛哥的博客 玩转Qml(14)-动画特效-梯度 | 涛哥的博客 玩转Qml(13)-动画特效-飞入 | 涛哥的博客 玩转Qml(13)-动画特效-飞入 | 涛哥的博客
玩转Qt(7)-窥探信号槽的实现细节 | 涛哥的博客
2019-08-30 · via 涛哥的博客
  • 简介
  • 猫和老鼠的故事
  • 声明与实现
  • Q_OBJECT宏
  • 信号的moc生成
  • 信号的触发
  • 槽和moc生成
  • 第三方信号槽实现

简介

这次讨论Qt信号-槽的实现细节。

上次的文章《认清信号槽的本质》中介绍过,信号-槽是一种对象之间的通信机制,是

Qt在标准C++之外,使用元对象编译器(MOC)实现的语法糖。

这次通过一个简单的案例,学习一些信号-槽的实现细节。

猫和老鼠的故事

预览

还是拿上次的设定来说明:Tom有个技能叫”喵”,就是发出猫叫,而正在偷吃东西的Jerry,听见猫叫声就会逃跑。

我们用信号-槽的方式写出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

#pragma once

#include <QObject>
#include <QDebug>
class Tom : public QObject
{
Q_OBJECT
public:
Tom(QObject *parent = nullptr) : QObject(parent)
{
}
void miaow()
{
qDebug() << u8"喵!" ;
emit miao();
}
signals:
void miao();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

#pragma once

#include <QObject>
#include <QDebug>
class Jerry : public QObject
{
Q_OBJECT
public:
Jerry(QObject *parent = nullptr) : QObject(parent)
{
}
public slots:
void runAway()
{
qDebug() << u8"那只猫又来了,快溜!" ;
}
};

以上面的代码为例,要使用信号-槽功能,先决条件是继承QObject类,并在类声明中增加Q_OBJECT宏。

之后在”signals:” 字段之后声明一些函数,这些函数就是信号。

在”public slots:” 之后声明的函数,就是槽函数。

接下来看看我们的main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

#include <QCoreApplication>
#include "Tom.h"
#include "Jerry.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

Tom tom;
Jerry jerry;

QObject::connect(&tom, &Tom::miao, &jerry, &Jerry::runAway);
tom.miaow();


return a.exec();
}

信号-槽都准备好了,接下来创建两个对象实例,并使用QObject::connect将信号和槽连接起来。

最后使用emit发送信号,就会自动触发槽函数了。

运行结果:

预览

声明与实现

信号和槽的本质都是函数。

我们知道C++中的函数要有声明(declare),也要有实现(implement),

而信号只要声明,不需要写实现。这是因为moc会为我们自动生成。

另外触发信号时,不写emit关键字,直接调用信号函数,也是没有问题的。

Q_OBJECT宏

我们来看一下Q_OBJECT宏,展开如下:

(不同的Qt版本有些差异,涛哥这里用的是5.12.4,以此为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public: \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS \
private: \
Q_OBJECT_NO_ATTRIBUTES_WARNING \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
QT_WARNING_POP \
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject, "")

我们看到,关键的地方,是声明了一个只读的静态成员变量staticMetaObject,以及3个public的成员函数

1
2
3
4
5
6
7
static const QMetaObject staticMetaObject; 

virtual const QMetaObject *metaObject() const;

virtual void *qt_metacast(const char *);

virtual int qt_metacall(QMetaObject::Call, int, void **);

还有一个private的静态成员函数qt_static_metacall

1
static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **)

那么声明的这些成员变量/函数,在哪里实现?答案是moc生成的cpp文件。

信号的moc生成

预览

如上图所示目录结构,项目编译完成后,在build文件夹中,自动生成了moc_Jerry.cpp 和 moc_Tom.cpp两个文件

其中moc_Tom.cpp内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133








#include "../../TomJerry/Tom.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qmetatype.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'Tom.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 67
#error "This file was generated using the moc from 5.12.4. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif

QT_BEGIN_MOC_NAMESPACE
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
struct qt_meta_stringdata_Tom_t {
QByteArrayData data[3];
char stringdata0[10];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
qptrdiff(offsetof(qt_meta_stringdata_Tom_t, stringdata0) + ofs \
- idx * sizeof(QByteArrayData)) \
)
static const qt_meta_stringdata_Tom_t qt_meta_stringdata_Tom = {
{
QT_MOC_LITERAL(0, 0, 3),
QT_MOC_LITERAL(1, 4, 4),
QT_MOC_LITERAL(2, 9, 0)

},
"Tom\0miao\0"
};
#undef QT_MOC_LITERAL

static const uint qt_meta_data_Tom[] = {


8,
0,
0, 0,
1, 14,
0, 0,
0, 0,
0, 0,
0,
1,


1, 0, 19, 2, 0x06 ,


QMetaType::Void,

0
};

void Tom::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
auto *_t = static_cast<Tom *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->miao(); break;
default: ;
}
} else if (_c == QMetaObject::IndexOfMethod) {
int *result = reinterpret_cast<int *>(_a[0]);
{
using _t = void (Tom::*)();
if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Tom::miao)) {
*result = 0;
return;
}
}
}
Q_UNUSED(_a);
}

QT_INIT_METAOBJECT const QMetaObject Tom::staticMetaObject = { {
&QObject::staticMetaObject,
qt_meta_stringdata_Tom.data,
qt_meta_data_Tom,
qt_static_metacall,
nullptr,
nullptr
} };


const QMetaObject *Tom::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

void *Tom::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_Tom.stringdata0))
return static_cast<void*>(this);
return QObject::qt_metacast(_clname);
}

int Tom::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 1)
qt_static_metacall(this, _c, _id, _a);
_id -= 1;
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 1)
*reinterpret_cast<int*>(_a[0]) = -1;
_id -= 1;
}
return _id;
}


void Tom::miao()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
QT_WARNING_POP
QT_END_MOC_NAMESPACE

可以大致看出,生成的cpp文件中,就是变量staticMetaObject以及 那几个函数的实现。

staticMetaObject是一个结构体,用来存储Tom这个类的信号、槽等元信息,并把

qt_static_metacall静态函数作为函数指针存储起来。

因为是静态成员,所以实例化多少个Tom对象,它们的元信息都是一样的。

qt_static_metacall函数提供了两种“元调用的实现”:

如果是InvokeMetaMethod类型的调用,则直接 把参数中的QObject对象,

转换成Tom类然后调用其miao函数

如果是IndexOfMethod类型的调用,即获取元函数的索引号,则计算miao函数的偏移并返回。

而moc_Tom.cpp末尾的

1
2
3
4
5

void Tom::miao()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

就是信号函数的实现。

信号的触发

miao信号的实现,直接调用了QMetaObject::activate函数。其中0代表miao这个函数的索引号。

QMetaObject::activate函数的实现,在Qt源码的QObject.cpp文件中,略微复杂一些,

且不同版本的Qt,实现差异都比较大,这里总结一下大致的实现:

先找出与当前信号连接的所有对象-槽函数,再逐个处理:

这里处理的方式,分为三种:

1
2
3
4
5
6
7
8
9
if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {

} else if (c->connectionType == Qt::BlockingQueuedConnection) {


} else {

}

receiverInSameThread表示当前线程id和接收信号的对象的所在线程id是否相等。

如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。

如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。

如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。

(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,

本身就是阻塞的,直接调用就好了,如果走阻塞队列,则多了加锁的过程。如果槽中又发了

同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)

队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent

放进了事件循环, 等到下一次事件分发,相应的线程才会去调用槽函数。

关于事件循环,可以参考之前的文章《Qt实用技能3-理解事件循环》

槽和moc生成

slot函数我们自己实现了,moc不会做额外的处理,所以自动生成的moc_Jerry.cpp文件中,

只有Q_OBJECT宏的展开,和前面的moc_Tom.cpp是一致的,不赘述了。

第三方信号槽实现

信号-槽是非常优秀的通信机制,但Qt的moc实现方式,被一些人诟病,所以他们造了新的轮子,比如:

https://woboq.com/blog/verdigris-qt-without-moc.html

http://sigslot.sourceforge.net/

https://github.com/NoAvailableAlias/nano-signal-slot

https://github.com/pbhogan/Signals