關鍵詞:Qt 原始碼 QObject QMetaObject 元物件系統 屬性 事件 訊號 槽
概述
官方文件第二章內容就是元物件系統,它在介紹裡描述到:
Qt
的元物件系統提供了訊號和槽機制(用於物件間的通訊)、執行時型別資訊和動態屬性系統。
元物件系統基於三個要素:
QObject
類為那些可以利用元物件系統的物件提供了一個基類。- 在類宣告的私有部分中使用
Q_OBJECT
宏用於啟用元物件特性,比如動態屬性、訊號和槽。 - 元物件編譯器(
moc
)為每個QObject子類提供必要的程式碼來實現元物件特性。
moc
工具讀取C++
原始檔,如果發現一個或多個包含Q_OBJECT
宏的類宣告,它會生成另一個C++
原始檔,其中包含了這些類的每個元物件的程式碼。這個生成的原始檔被#include進入類的原始檔,更常見的是被編譯並連結到類的實現中。
引入這個系統的主要原因是訊號和槽機制,此外它還提供了一些額外功能:
QObject::metaObject()
返回與該類相關聯的元物件。QMetaObject::className()
在執行時以字串形式返回類名,而無需透過 C++ 編譯器提供本地執行時型別資訊(RTTI)支援。QObject::inherits()
函式返回一個物件是否是在 QObject 繼承樹內繼承了指定類的例項。QObject::tr()
和QObject::trUtf8()
用於國際化的字串翻譯。QObject::setProperty()
和QObject::property()
動態地透過名稱設定和獲取屬性。QMetaObject::newInstance()
構造該類的新例項。
上面說到的元物件系統三要素,第3點moc
會在後面用單獨篇章分析,下面就不再展開,第1點我們在上一篇中做了簡單的分析,本篇我們看看第2點——Q_OBJECT
到底怎麼啟用了元物件系統(然而啟用非常複雜,我們先瀏覽個大概,所以標題叫熱身)。
staticMetaObject
找到原始碼中出現QMetaObject
的地方:
//qobject.h
class Q_CORE_EXPORT Qobject{
Q_OBJECT
//...
protected:
static const QMetaObject staticQtMetaObject;
//...
}
和QMetaObject
相關的變數只有2個地方出現,既然前面說了Q_OBJECT
和元物件系統相關,那我們就直接看Q_OBJECT
的定義:
//qobjectdefs.h
#define Q_OBJECT \
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, "")
我們關注變數static const QMetaObject staticMetaObject
,這是一個QMetaObject
型別的靜態變數,它應該是和元物件系統相關,文件對QMetaObject
的描述:
QMetaObject
類包含有關Qt
物件的元資訊。每個在應用程式中使用的QObject
子類都會建立一個QMetaObject
例項,該例項儲存了該QObject
子類的所有元資訊。此物件可透過QObject::metaObject()
方法獲得。
QMetaObject
就是元物件系統的關鍵了,檢視QMetaObject
的定義:
//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject{
//...
struct { // private data
const QMetaObject *superdata;
const QByteArrayData *stringdata;
const uint *data;
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall;
const QMetaObject * const *relatedMetaObjects;
void *extradata; //reserved for future use
} d;
}
QMetaObject
是個結構體,沒有建構函式。忽略掉所有方法宣告,只剩一個結構體變數,而且我們在qobject.cpp
中也沒有看到staticMetaObject
對應的初始化。那會不會在子類中初始化了?我們新建一個空的QMainWindow
工程,繼承關係是這樣的:
//MainWindow->QMainWindow->QWidget->QObject
遺憾的是我們並沒有在MainWindow
、QMainWindow
、QWidget
的構造器中找到staticMetaObject
初始化的痕跡。
moc_mainwindow.cpp
想起來官方文件說moc
會處理Q_OBJECT
宏,那就去moc
檔案找找——果然找到了staticMetaObject
相關的語句:
//moc_mainwindow.cpp
QT_INIT_METAOBJECT const QMetaObject MainWindow::staticMetaObject = { {
&QMainWindow::staticMetaObject,
qt_meta_stringdata_MainWindow.data,
qt_meta_data_MainWindow,
qt_static_metacall,
nullptr,
nullptr
} };
結合QMetaObject
的宣告,我們很容易看出這是在對QMetaObject
的變數賦值:
變數名 | 值 |
---|---|
const QMetaObject *superdata |
&QMainWindow::staticMetaObject |
const QByteArrayData *stringdata |
qt_meta_stringdata_MainWindow.data |
const uint *data |
qt_meta_data_MainWindow |
StaticMetacallFunction static_metacall |
qt_static_metacall |
const QMetaObject * const *relatedMetaObjects |
nullptr |
void *extradata |
nullptr |
對於const QMetaObject *superdata = &QMainWindow::staticMetaObject;
MainWindow
的staticMetaObject
的superdata
持有了QMainWindow
的staticMetaObject``,說明MainWindow
可以訪問QMainWindow
的staticMetaObject
。由於並不能看到moc_qmainwindow.cpp
等,我們只能從變數名合理猜測任何類的staticMetaObject
都持有了父類的staticMetaObject
。
做個實驗測試一下:
//mainwindow.cpp
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
//...
const QMetaObject *metaDta = staticMetaObject.d.superdata;
while(metaDta){
qDebug() << metaDta->className();
metaDta = metaDta->d.superdata;
}
}
/*
輸出結果:
QMainWindow
QWidget
QObject
*/
果不其然,列印結果是輸出了MainWindow
所有父類的className
。那麼我們基本可以斷定,繼承鏈中staticMetaObject
的持有關係如下圖所示:
對於const QByteArrayData *stringdata = qt_meta_stringdata_MainWindow.data;
在moc
檔案裡找到qt_meta_stringdata_MainWindow
變數:
//moc_mainwindow.cpp
static const qt_meta_stringdata_MainWindow_t qt_meta_stringdata_MainWindow = {
{
QT_MOC_LITERAL(0, 0, 10) // "MainWindow"
},
"MainWindow"
};
qt_meta_stringdata_MainWindow
是一個qt_meta_stringdata_MainWindow_t
型別,這裡對它進行了初始化。繼續找到qt_meta_stringdata_MainWindow_t
的定義:
//moc_mainwindow.cpp
struct qt_meta_stringdata_MainWindow_t {
QByteArrayData data[1];
char stringdata0[11];
};
也就是說stringdata
的值為QT_MOC_LITERAL(0, 0, 10) // "MainWindow"
。
繼續找到QT_MOC_LITERAL
的定義:
//moc_mainwindow.cpp
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
qptrdiff(offsetof(qt_meta_stringdata_MainWindow_t, stringdata0) + ofs \
- idx * sizeof(QByteArrayData)) \
)
這個宏的作用是建立一個靜態的 QByteArrayData
結構體,該結構體包含了字串字面值的後設資料。再結合註釋我們推斷stringdata
代表"MainWindow"
字串,這裡似乎是儲存的類名MainWindow
。從變數名qt_meta_stringdata_MainWindow
推斷,這個變數應該就是儲存的元物件相關的字串字面量,但我們預設工程沒有元物件,我們在程式碼中加一個signal
:
//mainwindow.h
signals:
void testSignal();
重新編譯,可以看到,qt_meta_stringdata_MainWindow
變數的初始化有所改變,從註釋看明顯包含了我們所加訊號的名稱:
//moc_mainwindow.cpp
static const qt_meta_stringdata_MainWindow_t qt_meta_stringdata_MainWindow = {
{
QT_MOC_LITERAL(0, 0, 10), // "MainWindow"
QT_MOC_LITERAL(1, 11, 10), // "testSignal"
QT_MOC_LITERAL(2, 22, 0) // ""
},
"MainWindow\0testSignal\0"
};
對於const uint *data = qt_meta_data_MainWindow;
在moc
檔案中找到qt_meta_data_MainWindow
定義,它是一個uint
陣列,目前還看不出它的作用。
//moc_mainwindow.cpp
static const uint qt_meta_data_MainWindow[] = {
// content:
8, // revision
0, // classname
0, 0, // classinfo
0, 0, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
0, // signalCount
0 // eod
};
對於StaticMetacallFunction static_metacall = qt_static_metacall;
在moc
檔案裡找到qt_static_metacall
定義,如果是預設工程,似乎也不做什麼:
//moc_mainwindow.cpp
void MainWindow::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
Q_UNUSED(_o);
Q_UNUSED(_id);
Q_UNUSED(_c);
Q_UNUSED(_a);
}
對於const QMetaObject * const *relatedMetaObjects = nullptr;
和void *extradata = nullptr;
暫時不討論。
我們目前找到了staticMetaObject
初始化的位置,知道它被賦值了一些資料結構,這些資料結構都和moc
相關。
QMetaObject其他成員
回過頭來,我們看看QMetaObject
的其他成員。
//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
class Connection;
//...
}
class Q_CORE_EXPORT QMetaObject::Connection {
//...
};
Connection
,QMetaObject
的內部類,文件描述:
Represents a handle to a signal-slot (or signal-functor) connection.
它代表了訊號-槽的連線,那就是說我們平常使用的connect
都和它相關,是個非常重要的角色。
我們可以看看我們一般使用的connect
的定義:
//qobject.h
template <typename Func1, typename Func2>
static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::type
connect(/*...*/)
{
//...
return connectImpl(/*...*/);
}
呼叫了connectImpl()
:
//qobject.h
static QMetaObject::Connection connectImpl(/*...*/);
的確是返回了QMetaObject::Connection
,由此可見Connection
是訊號-槽系統的關鍵角色,它代表了一個建立的連線。
再看看其他介面:
//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
//...
//基本資訊
const char *className() const;
const QMetaObject *superClass() const;
bool inherits(const QMetaObject *metaObject) const Q_DECL_NOEXCEPT;
//和類資訊相關
int classInfoOffset() const;
int classInfoCount() const;
int indexOfClassInfo(const char *name) const;
QMetaClassInfo classInfo(int index) const;
//和方法相關
int methodOffset() const;
int methodCount() const;
int indexOfMethod(const char *method) const;
QMetaMethod method(int index) const;
//和列舉相關
int enumeratorOffset() const;
int enumeratorCount() const;
int indexOfEnumerator(const char *name) const;
QMetaEnum enumerator(int index) const;
//和屬性相關
int propertyOffset() const;
int propertyCount() const;
int indexOfProperty(const char *name) const;
QMetaProperty property(int index) const;
QMetaProperty userProperty() const;
//和構造器相關
int constructorCount() const;
int indexOfConstructor(const char *constructor) const;
QMetaMethod constructor(int index) const;
//和訊號、槽相關
int indexOfSignal(const char *signal) const;
int indexOfSlot(const char *slot) const;
static bool checkConnectArgs(const char *signal, const char *method);
static bool checkConnectArgs(const QMetaMethod &signal,
const QMetaMethod &method);
static QByteArray normalizedSignature(const char *method);
static QByteArray normalizedType(const char *type);
//...
}
這些方法幾乎提供了獲取所有"元成員"資訊的方式(好玩的是原始碼作者強迫症一樣地把功能類似的方法放到了一起),包括構造器、方法、屬性等,之所以說“元成員”,是因為被Q_INVOKABLE
、Q_PROPERTY
等宏修飾的成員才具有"元能力"(當然,這也是後話了)。熟悉其他語言中反射特性的同學應該對這些方法的構成和名字比較熟悉,元物件系統的確為Qt提供了類似反射的能力。
接下來是和訊號-槽相關的介面:
//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
// internal index-based connect
static Connection connect(const QObject *sender, int signal_index,
const QObject *receiver, int method_index,
int type = 0, int *types = nullptr);
// internal index-based disconnect
static bool disconnect(const QObject *sender, int signal_index,
const QObject *receiver, int method_index);
//...
// internal index-based signal activation
static void activate(QObject *sender, int signal_index, void **argv);
//...
}
從註釋來看,這些介面用於內部,是以索引為基礎的一些方法,暫時沒接觸到它們使用的場景。
接下來是很多過載或者模板的invokeMethod()
:
//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
//...
invokeMethod(/*...*/);
//...
}
官方文件說明:
Invokes the member (a signal or a slot name) on the object obj
看來是用於呼叫obj
的訊號或者槽。
接下來是newInstance()
:
//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
//...
QObject *newInstance(/*...*/);
//...
}
它是用來呼叫建構函式的。
總結
熱身就到這裡,總結一下,Q_OBJECT
宏用於啟用元物件特性,其中staticMetaObject
的初始化在moc_xxx.cpp
中進行,moc_xxx.cpp
包含了許多“元成員”的字串資訊和實現。QMetaObject
是元物件系統的關鍵成員,提供了元資訊的介面。