之前一直停留在使用Qt库的层面,底层的实现也只是了解到一些皮毛而已,现在需要更深的了解它的实现原理,对以后开发会有很大的帮助。
按照我整个深入了解的过程,介绍以下几点主要内容:
Qt中的signals和slots两个宏的源码:
# define slots Q_SLOTS
# define signals Q_SIGNALS
# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)
可以看到的是这两个宏是没有意义的,那编译的时候怎么去处理呢?如何辨别哪个是信号,哪个是槽函数呢?
带着这些问题查找资料… …
标准C++不支持Qt的元对象系统,所以Qt单独提供了MOC工具来解决和C++的兼容问题。
MOC在预处理的时候,读取C++头文件,如果包含Q_OBJECT宏,则将生成一个C++源文件(moc_headername.cpp)。然后将新生成的源文件和其他文件一起进行编译、链接生成程序。
下面列举我创建的简单的Qt项目来看一下moc生成的源文件:
.h:
class QWidgetTest : public QWidget
{
Q_OBJECT
public:
QWidgetTest(QWidget *parent = Q_NULLPTR);
private:
Ui::QWidgetTestClass ui;
private:
int a_;
public:
void func(int a);
signals:
void sigTest();
void sigTttt(int a);
public slots:
void onTest();
void onButtonClicked();
};
.cpp:
QWidgetTest::QWidgetTest(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
connect(this, SIGNAL(sigTest()), this, SLOT(onTest()));
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(onButtonClicked()));
}
void QWidgetTest::func(int a)
{
}
void QWidgetTest::onTest()
{
}
void QWidgetTest::onButtonClicked()
{
emit sigTest();
}
下面将一部分一部分来介绍MOC预处理生成的moc_QWidgetTest.cpp文件。
// 该结构是存储类中的信号、信号参数、槽函数以及类名
struct qt_meta_stringdata_QWidgetTest_t {
QByteArrayData data[7]; // 有7个信息
char stringdata0[54]; // 将这些信息按照顺序组成字符串存储
};
/*
idx: 信息对应的索引值
ofs: 在字符串中的偏移量
len: 偏移长度
*/
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
qptrdiff(offsetof(qt_meta_stringdata_QWidgetTest_t, stringdata0) + ofs \
- idx * sizeof(QByteArrayData)) \
)
static const qt_meta_stringdata_QWidgetTest_t qt_meta_stringdata_QWidgetTest = {
{
QT_MOC_LITERAL(0, 0, 11), // "QWidgetTest"
QT_MOC_LITERAL(1, 12, 7), // "sigTest"
QT_MOC_LITERAL(2, 20, 0), // "" // 因为sigTest信号不带参数,所以为空
QT_MOC_LITERAL(3, 21, 7), // "sigTttt"
QT_MOC_LITERAL(4, 29, 1), // "a" // 信号sigTttt的参数
QT_MOC_LITERAL(5, 31, 6), // "onTest"
QT_MOC_LITERAL(6, 38, 15) // "onButtonClicked"
},
"QWidgetTest\0sigTest\0\0sigTttt\0a\0onTest\0"
"onButtonClicked"
};
#undef QT_MOC_LITERAL
从源代码可以看出,MOC把类名、信号、信号参数以及槽函数以字符串的形式存储在stringdata中,然后放到qt_meta_stringdata_QWidgetTest_t 结构体中。
// 该数组主要存储元对象信息(信号、槽函数、类信息和属性系统相关信息)
static const uint qt_meta_data_QWidgetTest[] = {
// content:
8, // revision
0, // classname // 类名,值的含义是其在qt_meta_stringdata_QWidgetTest_t结构中的索引值
0, 0, // classinfo // 前者0表示有0个classinfo,后者0表示classinfo在qt_meta_data_QWidgetTest中的索引
// classinfo 需要在头文件中使用Q_CLASSINFO,主要用于以Key-Value的形式定义类的附加信息
4, 14, // methods // 4表示有4个method的信息,14表示具体内容在qt_meta_data_QWidgetTest数组中的索引
0, 0, // properties // 含义同上
0, 0, // enums/sets // 含义同上
0, 0, // constructors // 含义同上
0, // flags // flag,具体含义不是很清楚
2, // signalCount // 信号个数
/*
上面methods字段介绍了有4个method信息,具体含义如下
name:对应qt_meta_stringdata_QWidgetTest_t结构中的索引值
argc:参数个数
*/
// signals: name, argc, parameters, tag, flags
1, 0, 34, 2, 0x06 /* Public */,
3, 1, 35, 2, 0x06 /* Public */,
// slots: name, argc, parameters, tag, flags
5, 0, 38, 2, 0x0a /* Public */,
6, 0, 39, 2, 0x0a /* Public */,
// 信号参数类型
// signals: parameters
QMetaType::Void,
QMetaType::Void, QMetaType::Int, 4,
// 槽函数参数类型
// slots: parameters
QMetaType::Void,
QMetaType::Void,
0 // eod
};
qt_meta_data_QWidgetTest数组主要是存储元对象信息的一些索引参数,我个人觉得这种处理方式不是很好,就像是强行约定了一种规则,可能这样做效率会更高,就不继续深究了… …
// 执行对象对应的信号和槽函数,或者是查找信号的索引
void QWidgetTest::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
auto *_t = static_cast<QWidgetTest *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->sigTest(); break;
case 1: _t->sigTttt((*reinterpret_cast< int(*)>(_a[1]))); break;
case 2: _t->onTest(); break;
case 3: _t->onButtonClicked(); break;
default: ;
}
} else if (_c == QMetaObject::IndexOfMethod) {
int *result = reinterpret_cast<int *>(_a[0]);
{
using _t = void (QWidgetTest::*)();
if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&QWidgetTest::sigTest)) {
*result = 0;
return;
}
}
{
using _t = void (QWidgetTest::*)(int );
if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&QWidgetTest::sigTttt)) {
*result = 1;
return;
}
}
}
}
// 初始化静态元对象,所有实例化的对象公用一个静态元对象
QT_INIT_METAOBJECT const QMetaObject QWidgetTest::staticMetaObject = { {
QMetaObject::SuperData::link<QWidget::staticMetaObject>(),
qt_meta_stringdata_QWidgetTest.data,
qt_meta_data_QWidgetTest,
qt_static_metacall,
nullptr,
nullptr
} };
// 获取元对象
const QMetaObject *QWidgetTest::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
// 通过类名获取元对象指针
void *QWidgetTest::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_QWidgetTest.stringdata0))
return static_cast<void*>(this);
return QWidget::qt_metacast(_clname);
}
// 这里应该是通过元对象来调用对应的方法
int QWidgetTest::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QWidget::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 4)
qt_static_metacall(this, _c, _id, _a);
_id -= 4;
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 4)
*reinterpret_cast<int*>(_a[0]) = -1;
_id -= 4;
}
return _id;
}
// 下面就是信号函数的具体实现,其实就是调用了avtivate
// SIGNAL 0
void QWidgetTest::sigTest()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
// SIGNAL 1
void QWidgetTest::sigTttt(int _t1)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
QMetaObject::activate(this, &staticMetaObject, 1, _a);
}
上面的代码主要是实现一些元对象的方法调用以及信号函数。
刚开始接触信号槽的时候还疑惑为什么信号不用实现,emit只是一个宏,怎么去触发槽函数呢。看到这里才有所了解,信号是由MOC预处理时实现。
因为篇幅原因,单独写一篇文章《Qt connect的实现原理》。
上面也介绍了,emit只是一个宏,实际上就是调用了MOC预处理时生成的信号函数。下面具体介绍一下信号函数是如何处理的:
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
void **argv)
{
int signal_index = local_signal_index + QMetaObjectPrivate::signalOffset(m);
if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
doActivate<true>(sender, signal_index, argv);
else
doActivate<false>(sender, signal_index, argv);
}
/*!
\internal
*/
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
int signal_index = signalOffset + local_signal_index;
if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
doActivate<true>(sender, signal_index, argv);
else
doActivate<false>(sender, signal_index, argv);
}
/*!
\internal
signal_index comes from indexOfMethod()
*/
void QMetaObject::activate(QObject *sender, int signal_index, void **argv)
{
const QMetaObject *mo = sender->metaObject();
while (mo->methodOffset() > signal_index)
mo = mo->superClass();
activate(sender, mo, signal_index - mo->methodOffset(), argv);
}
可以看到activate函数有三种重载,主要还是调用了doActivate模板函数,因为该函数篇幅有点长,就单独写一篇文章《Qt 信号如何触发槽函数?》。
概况一下信号与槽的实现原理和流程: