1. 程式人生 > >Qt高級——Qt元對象系統源碼解析

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

無法 進程 面向 watermark 元數據 tex 函數指針 意圖 argument

Qt高級——Qt元對象系統源碼解析
    基於Qt4.8.6版本

一、Qt元對象系統簡介

1、元對象系統簡介

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的實現代碼,會被編譯、鏈接到類的二進制代碼中,作為類的完整的一部分。

2、元對象系統的功能

元對象系統除了提供信號槽機制在對象間進行通訊的功能,還提供了如下功能:
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的支持。

3、Q_PROPERTY()的使用

#define Q_PROPERTY(text)

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

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:

#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();
}

4、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元對象系統源碼解析

1、Q_OBJECT宏的定義

任何從QObject派生的類都包含自己的元數據模型,一般通過宏Q_OBJECT定義。
Q_OBJECT定義在/src/corelib/kernel/Qobjectdefs.h文件中。

#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")))

2、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 的函數指針。

3、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

4、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,用以支持新的語法。

三、元對象編譯器MOC

1、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

2、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、信號槽不能有缺省參數

3、自定義類型的註冊

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&");

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