前言
之前一直停留在使用Qt庫的層面,底層的實作也隻是了解到一些皮毛而已,現在需要更深的了解它的實作原理,對以後開發會有很大的幫助。
概述
按照我整個深入了解的過程,介紹以下幾點主要内容:
- signals和slots宏
- MOC 元對象編譯器
- connect連接配接實作
- emit發送實作
signals和slots宏
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)
可以看到的是這兩個宏是沒有意義的,那編譯的時候怎麼去處理呢?如何辨識哪個是信号,哪個是槽函數呢?
帶着這些問題查找資料… …
MOC 元對象編譯器
标準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預處理時實作。
connect連接配接實作
因為篇幅原因,單獨寫一篇文章《Qt connect的實作原理》。
emit發送實作
上面也介紹了,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 信号如何觸發槽函數?》。
總結
概況一下信号與槽的實作原理和流程:
-
MOC預處理:
1.1. 把類名、信号、槽函數以及對應的參數以字元串的形式存儲在結構體中
1.2. 把上面的資訊對應的索引、偏移量和偏移長度存儲到結構體中的二維數組中
1.3. 然後實作一系列方法和信号函數
- connect操作将發送者、信号索引、發送者元對象、接收者、槽函數索引、接收者元對象等資訊存到Connection結構體中
-
emit操作時執行預處理生成的信号函數
3.1. 如果是直連則直接調用槽函數或回調函數
3.2. 如果是跨線程走隊列模式則放入事件隊列中
3.3. 如果是阻塞隊列連結則先阻塞然後通過事件系統異步調用(post)槽函數