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高级——Qt元对象系统源码解析

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

4、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`