Qt高級——Qt元對象系統源碼解析

基於Qt4.8.6版本

1、Qt元對象系統簡介

一、元對象系統簡介

Qt 的信號槽和屬性系統基於在運行時進行內省的能力,所謂內省是指面嚮對象語言的一種在運行期間查詢對象信息的能力, 好比若是語言具備運行期間檢查對象型別的能力,那麼是型別內省(type intropection)的,型別內省能夠用來實施多態。
C++的內省比較有限,僅支持型別內省, C++的型別內省是經過運行時類型識別(RTTI)(Run-Time Type Information)中的typeid 以及 dynamic_cast關鍵字來實現的。
Qt拓展了C++的內省機制,但並無採用C++的RTTI,而是提供了更爲強大的元對象(meta object)機制,來實現內省機制。基於內省機制,能夠列出對象的方法和屬性列表,而且可以獲取有關對象的全部信息,如參數類型。若是沒有內省機制,QtScript和 QML是難以實現的。
Qt中的元對象系統全稱Meta Object System,是一個基於標準C++的擴展,爲Qt提供了信號與槽機制、實時類型信息、動態屬性系統。元對象系統基於QObject類、Q_OBJECT宏、元對象編譯器MOC實現。
A、QObject 類
做爲每個須要利用元對象系統的類的基類。
B、Q_OBJECT宏
定義在每個類的私有數據段,用來啓用元對象功能,好比動態屬性、信號和槽。
在一個QObject類或者其派生類中,若是沒有聲明Q_OBJECT宏,那麼類的metaobject對象不會被生成,類實例調用metaObject()返回的就是其父類的metaobject對象,致使的後果是從類的實例得到的元數據其實都是父類的數據。所以類所定義和聲明的信號和槽都不能使用,因此,任何從QObject繼承出來的類,不管是否認義聲明瞭信號、槽和屬性,都應該聲明Q_OBJECT 宏。
C、元對象編譯器MOC (Meta Object Complier),
MOC分析C++源文件,若是發如今一個頭文件(header file)中包含Q_OBJECT 宏定義,會動態的生成一個moc_xxxx命名的C++源文件,源文件包含Q_OBJECT的實現代碼,會被編譯、連接到類的二進制代碼中,做爲類的完整的一部分。javascript

二、元對象系統的功能

元對象系統除了提供信號槽機制在對象間進行通信的功能,還提供了以下功能:
QObject::metaObject() 方法
得到與一個類相關聯的 meta-object
QMetaObject::className() 方法
在運行期間返回一個對象的類名,不須要本地C++編譯器的RTTI(run-time type information)支持
QObject::inherits() 方法
用來判斷生成一個對象類是否是從一個特定的類繼承出來,必須是在QObject類的直接或者間接派生類當中。
QObject::tr() and QObject::trUtf8()
爲軟件的國際化翻譯字符串
QObject::setProperty() and QObject::property()
根據屬性名動態的設置和獲取屬性值
  使用qobject_cast()方法在QObject類之間提供動態轉換,qobject_cast()方法的功能相似於標準C++的dynamic_cast(),但qobject_cast()不須要RTTI的支持。css

三、Q_PROPERTY()的使用

#define Q_PROPERTY(text)

Q_PROPERTY定義在/src/corelib/kernel/Qobjectdefs.h文件中,用於被MOC處理。java

Q_PROPERTY(type name READ getFunction [WRITE setFunction] [RESET resetFunction] [NOTIFY notifySignal] [REVISION int] [DESIGNABLE bool] [SCRIPTABLE bool] [STORED bool] [USER bool] [CONSTANT] [FINAL])

Type:屬性的類型
Name:屬性的名稱
READ getFunction:屬性的訪問函數
WRITE setFunction:屬性的設置函數
RESET resetFunction:屬性的復位函數
NOTIFY notifySignal:屬性發生變化的地方發射的notifySignal信號
REVISION int:屬性的版本,屬性暴露到QML中
DESIGNABLE bool:屬性在GUI設計器中是否可見,默認爲true
SCRIPTABLE bool:屬性是否能夠被腳本引擎訪問,默認爲true
STORED bool:
USER bool:
CONSTANT:標識屬性的值是常量,值爲常量的屬性沒有WRITE、NOTIFY
FINAL:標識屬性不會被派生類覆寫
注意:NOTIFY notifySignal聲明瞭屬性發生變化時發射notifySignal信號,但並無實現,所以程序員須要在屬性發生變化的地方發射notifySignal信號。
Object.h:程序員

#ifndef OBJECT_H #define OBJECT_H #include <QObject> #include <QString> #include <QDebug> class Object : public QObject { Q_OBJECT Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged) Q_PROPERTY(int score READ score WRITE setScore NOTIFY scoreChanged) Q_CLASSINFO("Author", "Scorpio") Q_CLASSINFO("Version", "1.0") Q_ENUMS(Level) protected: QString m_name; QString m_level; int m_age; int m_score; public: enum Level { Basic, Middle, Advanced }; public: explicit Object(QString name, QObject *parent = 0):QObject(parent) { m_name = name; setObjectName(m_name); connect(this, SIGNAL(ageChanged(int)), this, SLOT(onAgeChanged(int))); connect(this, SIGNAL(scoreChanged(int)), this, SLOT(onScoreChanged(int))); } int age()const { return m_age; } void setAge(const int& age) { m_age = age; emit ageChanged(m_age); } int score()const { return m_score; } void setScore(const int& score) { m_score = score; emit scoreChanged(m_score); } signals: void ageChanged(int age); void scoreChanged(int score); public slots: void onAgeChanged(int age) { qDebug() << "age changed:" << age; } void onScoreChanged(int score) { qDebug() << "score changed:" << score; } }; #endif // OBJECT_H

Main.cpp:shell

#include <QCoreApplication> #include "Object.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Object ob("object"); //設置屬性age ob.setProperty("age", QVariant(30)); qDebug() << "age: " << ob.age(); qDebug() << "property age: " << ob.property("age").toInt(); //設置屬性score ob.setProperty("score", QVariant(90)); qDebug() << "score: " << ob.score(); qDebug() << "property score: " << ob.property("score").toInt(); //內省intropection,運行時查詢對象信息 qDebug() << "object name: " << ob.objectName(); qDebug() << "class name: " << ob.metaObject()->className(); qDebug() << "isWidgetType: " << ob.isWidgetType(); qDebug() << "inherit: " << ob.inherits("QObject"); return a.exec(); }

四、Q_INVOKABLE使用

#define Q_INVOKABLE

Q_INVOKABLE定義在/src/corelib/kernel/Qobjectdefs.h文件中,用於被MOC識別。
Q_INVOKABLE宏用於定義一個成員函數能夠被元對象系統調用,Q_INVOKABLE宏必須寫在函數的返回類型以前。以下:
Q_INVOKABLE void invokableMethod();
invokableMethod()函數使用了Q_INVOKABLE宏聲明,invokableMethod()函數會被註冊到元對象系統中,可使用 QMetaObject::invokeMethod()調用。
Q_INVOKABLE與QMetaObject::invokeMethod均由元對象系統喚起,在Qt C++/QML混合編程、跨線程編程、Qt Service Framework以及 Qt/ HTML5混合編程以及裏普遍使用。
A、在跨線程編程中的使用
如何調用駐足在其餘線程裏的QObject方法呢?Qt提供了一種很是友好並且乾淨的解決方案:向事件隊列post一個事件,事件的處理將以調用所感興趣的方法爲主(須要線程有一個正在運行的事件循環)。而觸發機制的實現是由MOC提供的內省方法實現的。所以,只有信號、槽以及被標記成Q_INVOKABLE的方法纔可以被其它線程所觸發調用。若是不想經過跨線程的信號、槽這一方法來實現調用駐足在其餘線程裏的QObject方法。另外一選擇就是將方法聲明爲Q_INVOKABLE,而且在另外一線程中用invokeMethod喚起。
B、Qt Service Framework
Qt服務框架是Qt Mobility 1.0.2版本推出的,一個服務(service)是一個獨立的組件提供給客戶端(client)定義好的操做。客戶端能夠經過服務的名稱,版本號和服務的對象提供的接口來查找服務。 查找到服務後,框架啓動服務並返回一個指針。
服務經過插件(plug-ins)來實現。爲了不客戶端依賴某個具體的庫,服務必須繼承自QObject,保證QMetaObject 系統能夠用來提供動態發現和喚醒服務的能力。要使QmetaObject機制充分的工做,服務必須知足,其全部的方法都是經過 signal、slot、property或invokable method和Q_INVOKEBLE來實現。編程

QServiceManager manager;
QObject *storage ;  
storage = manager.loadInterface("com.nokia.qt.examples.FileStorage"); if(storage) QMetaObject::invokeMethod(storage, "deleteFile", Q_ARG(QString, "/tmp/readme.txt")); 

上述代碼經過service的元對象提供的invokeMethod方法,調用文件存儲對象的deleteFile() 方法。客戶端不須要知道對象的類型,所以也沒有連接到具體的service庫。 固然在服務端的deleteFile方法,必定要被標記爲Q_INVOKEBLE,纔可以被元對象系統識別。
Qt服務框架的一個亮點是它支持跨進程通訊,服務能夠接受遠程進程。在服務管理器上註冊後,進程經過signal、slot、invokable method和property來通訊,就像本地對象同樣。服務能夠設定爲在客戶端間共享,或針對一個客戶端。 在Qt服務框架推出以前,信號、槽以及invokable method僅支持跨線程。 下圖是跨進程的服務/客戶段通訊示意圖。invokable method和Q_INVOKEBLE 是跨進城、跨線程對象之間通訊的重要利器。
Qt高級——Qt元對象系統源碼解析數組

2、Qt元對象系統源碼解析

一、Q_OBJECT宏的定義

任何從QObject派生的類都包含本身的元數據模型,通常經過宏Q_OBJECT定義。
Q_OBJECT定義在/src/corelib/kernel/Qobjectdefs.h文件中。bash

#define Q_OBJECT \ public: \ Q_OBJECT_CHECK \ static const QMetaObject staticMetaObject; \ Q_OBJECT_GETSTATICMETAOBJECT \ virtual const QMetaObject *metaObject() const; \ virtual void *qt_metacast(const char *); \ QT_TR_FUNCTIONS \ virtual int qt_metacall(QMetaObject::Call, int, void **); \ private: \ Q_DECL_HIDDEN static const QMetaObjectExtraData staticMetaObjectExtraData; \ Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);

QMetaObject類型的靜態成員變量staticMetaObject是元數據的數據結構。metaObject,qt_metacast,qt_metacall、qt_static_metacall四個虛函數由MOC在生成的moc_xxx.cpp文件中實現。metaObject的做用是獲得元數據表指針;qt_metacast的做用是根據簽名獲得相關結構的指針,返回void*指針;qt_metacall的做用是查表而後調用調用相關的函數;qt_static_metacall的做用是調用元方法(信號和槽)。
#define Q_DECL_HIDDEN __attribute__((visibility("hidden")))數據結構

二、QMetaObject類型

QMetaObject類定義在/src/corelib/kernel/Qobjectdefs.h文件。框架

struct Q_CORE_EXPORT QMetaObject { ... enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, ResetProperty, QueryPropertyDesignable, QueryPropertyScriptable, QueryPropertyStored, QueryPropertyEditable, QueryPropertyUser, CreateInstance }; int static_metacall(Call, int, void **) const; static int metacall(QObject *, Call, int, void **); struct { // private data const QMetaObject *superdata; const char *stringdata; const uint *data; const void *extradata; } d; };

QMetaObject中有一個嵌套結構封裝了全部的數據:
const QMetaObject superdata;//元數據表明的類的基類的元數據
const char
stringdata;//元數據的簽名標記
const uint *data;//元數據的索引數組的指針
const QMetaObject **extradata;//擴展元數據表的指針,指向QMetaObjectExtraData數據結構。

struct QMetaObjectExtraData { #ifdef Q_NO_DATA_RELOCATION const QMetaObjectAccessor *objects; #else const QMetaObject **objects; #endif typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **); //from revision 6 //typedef int (*StaticMetaCall)(QMetaObject::Call, int, void **); //used from revison 2 until revison 5 StaticMetacallFunction static_metacall; };

static_metacall是一個指向Object::qt_static_metacall 的函數指針。

三、QT_TR_FUNCTIONS宏定義

宏QT_TR_FUNCTIONS是和翻譯相關的。

#define QT_TR_FUNCTIONS \ static inline QString tr(const char *s, const char *c = 0) \ { return staticMetaObject.tr(s, c); } \ #endif

四、Qt中其它宏的定義

Qt在/src/corelib/kernel/Qobjectdefs.h文件中定義了大量的宏。

#ifndef Q_MOC_RUN # if defined(QT_NO_KEYWORDS) # define QT_NO_EMIT # else # define slots # define signals protected # endif # define Q_SLOTS # define Q_SIGNALS protected # define Q_PRIVATE_SLOT(d, signature) # define Q_EMIT #ifndef QT_NO_EMIT # define emit #endif #define Q_CLASSINFO(name, value) #define Q_INTERFACES(x) #define Q_PROPERTY(text) #define Q_PRIVATE_PROPERTY(d, text) #define Q_REVISION(v) #define Q_OVERRIDE(text) #define Q_ENUMS(x) #define Q_FLAGS(x) #define Q_SCRIPTABLE #define Q_INVOKABLE #define Q_SIGNAL #define Q_SLOT

Qt中的大部分宏都無實際的定義,都是提供給MOC識別處理的,MOC工具經過對類中宏的解析處理生成moc_xxx.cpp文件。
在 Qt4 及以前的版本中,signals被展開成protected。Qt5則變成public,用以支持新的語法。

3、元對象編譯器MOC

一、MOC功能

A、處理Q_OBJECT宏和signals/slots關鍵字,生成信號和槽的底層代碼
B、處理Q_PROPERTY()和Q_ENUM()生成property系統代碼
C、處理Q_FLAGS()和Q_CLASSINFO()生成額外的類meta信息
D、不須要MOC處理的代碼能夠用預約義的宏括起來,以下:

#ifndef Q_MOC_RUN … #endif

二、MOC限制

A、模板類不能使用信號/槽機制
B、MOC不擴展宏,因此信號和槽的定義不能使用宏, 包括connect的時候也不能用宏作信號和槽的名字以及參數
C、從多個類派生時,QObject派生類必須放在第一個。 QObject(或其子類)做爲多重繼承的父類之一時,須要把它放在第一個。 若是使用多重繼承,moc在處理時假設首先繼承的類是QObject的一個子類,須要確保首先繼承的類是QObject或其子類。
D、函數指針不能做爲信號或槽的參數, 由於其格式比較複雜,MOC不能處理。能夠用typedef把它定義成簡單的形式再使用。
E、用枚舉類型或typedef的類型作信號和槽的參數時,必須fully qualified。這個詞中文不知道怎麼翻譯才合適,簡單的說就是, 若是是在類裏定義的, 必須把類的路徑或者命名空間的路徑都加上, 防止出現混淆。如Qt::Alignment之類的,前面的Qt就是Alignment的qualifier, 必須加上,並且有幾級加幾級。
F、信號和槽不能返回引用類型
G、signals和slots關鍵字區域只能放置信號和槽的定義,不能放其它的如變量、構造函數的定義等,友元聲明不能位於信號或者槽聲明區內。
H、嵌套類不能含有信號和槽 
MOC沒法處理嵌套類中的信號和槽,錯誤的例子: 
class A:public QObject
{
Q_OBJECT
public:
class B
{
public slots://錯誤用法

};

};
I、信號槽不能有缺省參數

三、自定義類型的註冊

Qt線程間傳遞自定義類型數據時,本身定義的類型若是直接使用信號槽來傳遞的話會產生下面這種錯誤:
          QObject::connect: Cannot queue arguments of type 'XXXXX' (Make sure 'XXXXX' is registed using qRegisterMetaType().)
         緣由:當一個signal被放到隊列中(queued)時,參數(arguments)也會被一塊兒一塊兒放到隊列中,參數在被傳送到slot以前須要被拷貝、存儲在隊列中;爲了可以在隊列中存儲參數(argument),Qt須要去construct、destruct、copy參數對象,而爲了讓Qt知道怎樣去做這些事情,參數的類型須要使用qRegisterMetaType來註冊。
步驟:(以自定義XXXXX類型爲例)
A、自定義類型時在類的頂部包含:#include <QMetaType>
B、在類型定義完成後,加入聲明:Q_DECLARE_METATYPE(XXXXX);
C、在main()函數中註冊自定義類類型:qRegisterMetaType<XXXXX>("XXXXX");
若是但願使用類型的引用,一樣要註冊:qRegisterMetaType<XXXXX>("XXXXX&");

四、MOC的使用

查看工程的Makefile文件能夠查找到MOC生成moc_xxx.cpp文件的命令:

moc_Object.cpp: ../moc/Object.h /usr/local/Trolltech/Qt-4.8.6/bin/moc $(DEFINES) $(INCPATH) ../moc/Object.h -o moc_Object.cpp 所以命令行能夠簡化爲:
`moc Object.h -o moc_Object.cpp`