Qt - 元对象系统

522 阅读14分钟

一、 元对象系统

1. 介绍元对象

1.1 什么是元对象

Qt的元对象是一个描述类的结构信息的对象.元对象包含了类的信息,比如类型的名称、继承关系、信号与槽等.它允许在运行时动态的访问和操作类的属性、方法和信号等.

1.2 怎么才能成为元对象

  1. 继承自QObject类
  2. 在类声明中添加Q_OBJECT宏,这样moc才能生成所需代码
  3. 元对象编译器(moc)为每个QObject的派生类,提供元对象特性所需的代码

1.3 QObject类

QObject类是元对象系统的核心,该类提供元对象的创建和访问、信号与槽机制、属性系统等.

image.png QObject类提供了很多重要的函数,比如跟事件相关的event()eventFilter().还有信号槽的connect() disconnect(),定时器相关函数startTimerkillTimer等等.

2. 使用元对象

QMetaObject这个类则存储元对象的信息,比如: 类名称、基类元对象、类的元方法、函数在元对象中的索引等等.要获取QMetaObject这个对象,需要添加Q_OBJECT宏,或者从QObject类继承而来.不过继承而来的对象就只是关于QObject类的信息,通过派生类想得到关于自己的元对象信息则无法获得.

Q_OBJECT宏展开如下: image.png 当一个类定义Q_OBJECT宏时,会在这个类添加几个有关元对象的成员和函数.上面的函数细节在moc生成的文件里,去项目构建目录里寻找moc_xxx.cpp这样的文件.

QObject类也定义了Q_OBJECT宏,如果没有在我们自己的类里定义,则从QObject类继承而来. image.png

3. 元类信息

在编译阶段在元对象存储键值对信息.这个是只读的,在运行期不允许更改.通过获取QMetaObject来获取这个classinfo信息 使用方法

Q_CLASSINFO("author", "张三")
const QMetaObject& mo = staticQtMetaObject;
QMetaClassInfo info = mo.classInfo(0);
info.name();    // 获取key
info.value();   // 获取value

4. 元枚举

class Widget : public QWidget
{
    Q_OBJECT
public:
    enum Color
    {
        Red = 1, Blue, White 
    };
    Q_ENUM(Color)
    
    enum class Level
    {
        A = 0x1,
        B = 0x2,
        C = 0x3
    };
    Q_FLAG(Level)
    Q_DECLARE_FLAGS(LevelFlag, Level)    // Level的别名为LevelFlag
};
const QMetaObject& mo = Widget::staticMetaObject;
int count = mo.enumeratorCount();
    
for (int i = 0; i < count; i++)
{
    QMetaEnum me = mo.enumerator(i);
    qDebug() << "name: " << me.enumName() << " "
        << "isFlag: " << me.isFlag() << " "
        << "socpe: " << me.scope() << " "
        << "isSocpe: " << me.isScoped();
}

enum class的isFlag()返回true,表示可以使用or运算符组合枚举. isScope()返回true表示使用的是c++11枚举类

5. 元方法

在函数名前加上Q_INVOKABLE宏.也可以对函数加上标签,必须在Q_INVOKABLE前面.
注意: 构造、析构函数前不能加标签

#ifndef Q_MOC_RUN
#define METHOD_TAG
#endif 

class Widget : public QWidget
{
    Q_OBJECT
public:
    Q_INVOKABLE Widget(QWidge* parent = nullptr);
    ~Widget();
    
    METHOD_TAG Q_INVOKABLE void print() { qDebug() << "hello"; }
};
const QMetaObject& mo = Widget::staticMetaObject;
// 获取构造函数
for (int j = 0; j < mo.constructorCount(); j++)
{
    QMetaMethod method = mo.constructor(j);
    qDebug() << method.methodSignature();
}
// 获取普通函数
for (int i = mo.methodOffset(); i < mo.methodCount(); i++)
{
    QMetaMethod method = mo.method(i);
    qDebug() << method.tag() << " " << method.methodSignature();
}

6. 元属性

Q_PROPERTY(属性类型 属性名称 READ 获取属性函数 WRITE 修改属性函数 NOTIFY 值修改信号) 按照上述格式定义元属性

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
    
    Q_PROPERTY(int m_age READ age WRITE setAge NOTIFY onAgeChanged)
    
    int age() { return m_age; }
    void setAge(int age) 
    { 
        if (age != m_age) 
            m_age = age; 
        emit onAgeChanged();    // 值修改发射信号
    }
signals:
    void onAgeChanged();
public slots: 
    void print() { qDebug() << "changed"; }
private:
    int m_age;
QObject::connect(&w, &Widget::onAgeChanged, &w, &Widget::print);
const QMetaObject& mo = Widget::staticMetaObject;
   
for (int i = mo.propertyOffset(); i < mo.propertyCount(); i++)
{
    QMetaProperty mp = mo.property(i);
    qDebug() << mp.typeName() << " " 
        << mp.name() << " " 
        << mp.notifySignal().name();
}
    
w.setAge(1);

7. 元对象存储结构

7.1 moc生成的元字符串

有如下代码:

// 元方法标签
#ifndef Q_MOC_RUN
#define METHOD_TAG
#endif 

class Widget : public QWidget
{
    Q_OBJECT
public:
     Widget(QWidget *parent = nullptr);
    ~Widget();
// 元方法 + 标签
    METHOD_TAG Q_INVOKABLE void test() {qDebug() << "test";}
    
// 元枚举
    enum Color
    {
        Red = 1, Blue, Black
    };
    enum class Level
    {
        A = 0x1, B = 0x2, C = 0x4
    };
    Q_ENUM(Color)
    Q_FLAG(Level)
    Q_DECLARE_FLAGS(LevelFlag, Level)

// 元类信息
    Q_CLASSINFO("author", "zhangsan")
    
// 元属性
    Q_PROPERTY(int m_age READ age WRITE setAge NOTIFY onAgeChanged)
    int age() { return m_age; }
    void setAge(int age) 
    { 
        if (age != m_age) 
            m_age = age; 
        emit AgeChanged(); 
    }
signals:
    void AgeChanged();
    void Changed();
public slots: 
    void onAgeChanged() { qDebug() << "onAgeChanged()"; }
    void onChanged() { qDebug() << "onChanged()"; }
private:
    int m_age;
};

moc编译生成的文件代码细节如下:

struct qt_meta_stringdata_Widget_t {
    QByteArrayData data[19];    // 24 * 19 = 456
    char stringdata0[121];      // 121
};

// 计算偏移值,获取字符串
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_Widget_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) )
    
static const qt_meta_stringdata_Widget_t qt_meta_stringdata_Widget = {
    {
        QT_MOC_LITERAL(0, 0, 6), // "Widget"
        QT_MOC_LITERAL(1, 7, 6), // "author"
        QT_MOC_LITERAL(2, 14, 8), // "zhangsan"
        QT_MOC_LITERAL(3, 23, 10), // "AgeChanged"
        QT_MOC_LITERAL(4, 34, 0), // ""
        QT_MOC_LITERAL(5, 35, 7), // "Changed"
        QT_MOC_LITERAL(6, 43, 12), // "onAgeChanged"
        QT_MOC_LITERAL(7, 56, 9), // "onChanged"
        QT_MOC_LITERAL(8, 66, 4), // "test"
        QT_MOC_LITERAL(9, 71, 10), // "METHOD_TAG"
        QT_MOC_LITERAL(10, 82, 5), // "m_age"
        QT_MOC_LITERAL(11, 88, 5), // "Color"
        QT_MOC_LITERAL(12, 94, 3), // "Red"
        QT_MOC_LITERAL(13, 98, 4), // "Blue"
        QT_MOC_LITERAL(14, 103, 5), // "Black"
        QT_MOC_LITERAL(15, 109, 5), // "Level"
        QT_MOC_LITERAL(16, 115, 1), // "A"
        QT_MOC_LITERAL(17, 117, 1), // "B"
        QT_MOC_LITERAL(18, 119, 1) // "C"
    },
    "Widget\0author\0zhangsan\0AgeChanged\0\0"
    "Changed\0onAgeChanged\0onChanged\0test\0"
    "METHOD_TAG\0m_age\0Color\0Red\0Blue\0Black\0"
    "Level\0A\0B\0C"    // 该字符串总共120大小
};    

上述代码,是moc编译器生成的字符串数组,存储有关类信息的字符串信息.比如类名、函数名、枚举名等. 最开始定义一个结构体qt_meta_stringdata_Widget_t,这个结构体的总大小为577.成员stringdata0用来存储有关类信息的字符串. 成员data存储相对于整个结构体的偏移值,用来定位到具体的字符串.

然后就是定义了一个对象qt_meta_stringdata_Widget,成员data使用了宏QT_MOC_LITERAL来设置相应的偏移值.qt_meta_stringdata_Widget_t结构体的data成员类型是QByteArrayData,下面看一下这个类的细节,有哪些成员变量:

struct QByteArrayData
{
    QtPrivate::RefCount ref;
    int size;    // 存储字符串长度
    uint alloc : 31;
    uint capacityReserved : 1;

    qptrdiff offset;    // 存储相对于qt_meta_stringdata_Widget对象的偏移值
};

然后接下来看一下QT_MOC_LITERAL宏是怎么做的:

  1. 内部的offsetofstringdata0偏移值,QByteArrayData data[19];,因为QByteArrayData的大小为24,所以,stringdata0的偏移值为: 456
  2. 在这个偏移值上形成公式:456 + ofs - idx * 24
  3. 然后最外层宏Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET展开为:
    { {-1}, size, 0, 0, offset }这个用来初始化QByteArrayData对象

所以,最终qt_meta_stringdata_Widget对象如下面这样:

static const qt_meta_stringdata_Widget_t qt_meta_stringdata_Widget = {
    {
        { {-1}, 6,  0, 0, 456},    // "Widget"
        { {-1}, 6,  0, 0, 439},    // "author"
        { {-1}, 8,  0, 0, 422},    // "zhangsan"
        { {-1}, 10, 0, 0, 407},    // "AgeChanged"
        { {-1}, 0,  0, 0, 394},    // ""
        { {-1}, 7,  0, 0, 371},    // "Changed"
        { {-1}, 12, 0, 0, 355},    // "onAgeChanged"
        { {-1}, 9,  0, 0, 344},    // "onChanged"
        { {-1}, 4,  0, 0, 330},    // "test"
        { {-1}, 10, 0, 0, 311},    // "METHOD_TAG"
        { {-1}, 5,  0, 0, 298},    // "m_age"
        { {-1}, 5,  0, 0, 280},    // "Color"
        { {-1}, 3,  0, 0, 262},    // "Red"
        { {-1}, 4,  0, 0, 242},    // "Blue"
        { {-1}, 5,  0, 0, 223},    // "Black"
        { {-1}, 5,  0, 0, 205},    // "Level"
        { {-1}, 1,  0, 0, 187},    // "A"
        { {-1}, 1,  0, 0, 165},    // "B"
        { {-1}, 1,  0, 0, 143}     // "C"
    },
    "Widget\0author\0zhangsan\0AgeChanged\0\0"
    "Changed\0onAgeChanged\0onChanged\0test\0"
    "METHOD_TAG\0m_age\0Color\0Red\0Blue\0Black\0"
    "Level\0A\0B\0C"    // 该字符串总共120大小
};

我们得到了偏移值和字符串长度,那么怎么通过这俩去获取到相应的字符串呢?如下这样使用:

// 获取这个data的地址
void *dataPtr = (void*)&(qt_meta_stringdata_Widget.data[0]);
int off = qt_meta_stringdata_Widget[0].offset;    // 获取偏移值

// 相对于整个qt_meta_stringdata_Widget的偏移值,这样定位到具体字符串了
const char* str = (const char*)dataPtr + off;     

代码可能一时间不太能看懂,那么下面就画幅图:
image.png 给每个前加上相当于整个对象的地址.字符串的起始地址是456.如果我们想要获取author字符串,那么这个地址就是456 + widget字符串长度 + \0,也就是456 + 7 = 463.然后上面关于author的data,它的相对地址是24,所以根据这个data的偏移值439,我们可以这样计算:
456 + 7 - 24 = 439,也就是等于这个data里存储的偏移值.这个偏移值就是相对于整个对象的偏移地址,也就是指向author字符串首地址. 其他的也是同理.所以,data数组里每个偏移值越往下越小.

7.2 moc生成的元数据

moc生成的代码如下:

static const uint qt_meta_data_Widget[] = {

 // content:
       8,       // revision
       0,       // classname
       1,   14, // classinfo
       5,   16, // methods
       1,   46, // properties
       2,   50, // enums/sets
       0,    0, // constructors
       0,       // flags
       2,       // signalCount   [13]

 // classinfo: key, value
       1,    2,    // [15]

 // signals: name, argc, parameters, tag, flags
       3,    0,   41,    4, 0x06 /* Public */,  // [20]
       5,    0,   42,    4, 0x06 /* Public */,  // [25]

 // slots: name, argc, parameters, tag, flags
       6,    0,   43,    4, 0x0a /* Public */,  // [30]
       7,    0,   44,    4, 0x0a /* Public */,  // [35]

 // methods: name, argc, parameters, tag, flags
       8,    0,   45,    9, 0x02 /* Public */,  // [40]

 // signals: parameters
    QMetaType::Void,  
    QMetaType::Void,  // [42]

 // slots: parameters
    QMetaType::Void,
    QMetaType::Void,  // [44]

 // methods: parameters
    QMetaType::Void,  // [45]

 // properties: name, type, flags
      10, QMetaType::Int, 0x00495003,  // [48]

 // properties: notify_signal_id
    1879048198,  // [49]

 // enums: name, alias, flags, count, data
      11,   11, 0x0,    3,   60,  // [54]
      15,   15, 0x3,    3,   66,  // [59]

 // enum data: key, value
      12, uint(Widget::Red),      // [61]
      13, uint(Widget::Blue),     // [63]
      14, uint(Widget::Black),    // [65]
      16, uint(Widget::Level::A), // [67]
      17, uint(Widget::Level::B), // [69]
      18, uint(Widget::Level::C), // [71]

       0        // eod
};

上面这些各种数字,要不就是qt_meta_stringdata_Widget的下标,指向某个字符串.要不就是当前数组里下标.

介绍conetent:
这个对照QMetaObjectPrivate结构体

struct QMetaObjectPrivate
{
    int revision;
    int className;
    int classInfoCount, classInfoData;
    int methodCount, methodData;
    int propertyCount, propertyData;
    int enumeratorCount, enumeratorData;
    int constructorCount, constructorData;
    int flags;
    int signalCount;
};

对照后content如下: image.png

signals、slots、methods:
image.png 剩下的也都和上图的注释同理,自己查相应下标就能明白.

在moc生成的文件里,初始化了staticMetaObject对象.QMetaObject有个类型为d的结构体:

struct // private data
{ 
    SuperData superdata;
    const QByteArrayData *stringdata;
    const uint *data;
    typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
    StaticMetacallFunction static_metacall;
    const SuperData *relatedMetaObjects;
    void *extradata; //reserved for future use
} d;

初始化代码:

QT_INIT_METAOBJECT const QMetaObject Widget::staticMetaObject = { {
    QMetaObject::SuperData::link<QWidget::staticMetaObject>(),
    qt_meta_stringdata_Widget.data,
    qt_meta_data_Widget,
    qt_static_metacall,
    nullptr,
    nullptr
} };

上面代码则大致如下图所示: image.png

7.3 反射机制

7.3.1 介绍反射
  1. 什么是反射?

在运行时获取对象的属性、方法和信号等信息的能力.这种机制使得开发者可以在运行时动态的获取和修改对象的属性和方法,从而实现更加灵活的编程.

  1. Qt中的反射

通过获取QMeatObject对象来获取关于对象的属性、方法等信息

7.3.2 反射创建对象实例原理分析

使用newInstance()创建对象实例:
注意: 要使用newInstance()创建对象,必须用Q_INVOKABLE修饰函数成为元方法,否则找不到该方法,无法生成对象实例

class Widget : public QWidget
{
    Q_OBJECT    // 必须加上这个宏才能生成元对象
public:
    Widget(QWidget* parent = nullptr) {}
    Q_INVOKABLE Widget(int i) {}    // 必须加上Q_INVOKABLE才能生成元方法
    ~Widget() {}
};

创建对象实例:

const QMetaObject& mo = Widget::staticMetaObject;

int i = 1;
QObject* obj = mo.newInstance(QGenericArgument("int", &i));

delete obj;

接下来分析newInstance()函数创建对象实例的原理:

image.png newInstance一共接收10个参数,QGenericArgument类的细节如下:
image.png QGenericArgument底层封装了两个void*指针,用来从存储参数值参数名称

newInstance函数代码先检查当前类是否是QObject的派生类,调用inherits函数将当前对象的地址或基类的地址和staticMetaObject地址比较是否相同.
image.png 根据这个得知,要使用newInstance()函数创建对象实例,必须要继承自QObject

接下来获取类名构造出构造函数签名,因为构造函数名和类名一样,所以获取出类名来.
image.png image.png image.png 这里将m->d.data转换成QMetaObjectPrivate对象,该类细节如下:

struct QMetaObjectPrivate
{
    int revision;
    int className;
    int classInfoCount, classInfoData;
    int methodCount, methodData;
    int propertyCount, propertyData;
    int enumeratorCount, enumeratorData;
    int constructorCount, constructorData;
    int flags;
    int signalCount;
};

m->d这个在QMetaObject对象中,细节如下:

struct 
{ 
    SuperData superdata;
    const QByteArrayData *stringdata;
    const uint *data;
    typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
    StaticMetacallFunction static_metacall;
    const SuperData *relatedMetaObjects;
    void *extradata; // reserved for future use
} d; 

而这个m->d成员对象在moc生成的文件中进行初始化了

QT_INIT_METAOBJECT const QMetaObject Widget::staticMetaObject = { {
    QMetaObject::SuperData::link<QWidget::staticMetaObject>(),
    qt_meta_stringdata_Widget.data,
    qt_meta_data_Widget,
    qt_static_metacall,
    nullptr,
    nullptr
} };

m->d的成员data被初始化了元数据,元数据里存储着有关元对象信息.
而元数据头部又对应着QMetaObjectPrivate,所以将其转换:
image.png 然后回到函数代码里 image.png 然后调用rawStringData函数从元字符串中获取字符串
image.png

回到newInstance函数,接下来拼接构造函数签名:
image.png 接下来获取在元方法中构造函数的下标位置 image.png indexOfConstructor函数细节如下: image.png image.png image.png

这里使用的是元数据里的 image.png

找到了并且返回不小于0的下标之后,开始准备参数值和返回值,调用构造函数创建对象实例 image.png 这里将对象实例指针放在参数列表第一个位置,创建对象的时候,对象地址设置到这个返回值上.然后这里调用static_metacall函数.这个函数调用函数指针static_metacall指针的函数.在moc生成的文件里初始化指向 qt_static_metacall函数.
传给static_metacall函数第一个参数是一个枚举值.表示创建对象实例,还支持其他功能:
image.png

image.png image.png 接下来调用qt_static_metacall函数 image.png

7.3.3 反射机制调用方法原理

有如下类:

class Widget : public QWidget
{
    Q_OBJECT
public:
    Q_INVOKABLE Widget(QWidget *parent = nullptr);
    ~Widget();
    
    Q_INVOKABLE int sum(int num1, int num2) 
    {
        return num1 + num2;
    }
signals:
    void ageChanged();
public slots:
    void onAgeChanged() {} 
};

调用invokeMethod函数

int sum = 0, num1 = 1, num2 = 2;
const QMetaObject& mo = Widget::staticMetaObject;
QObject* obj = mo.newInstance();
bool ret = QMetaObject::invokeMethod(obj, "sum", 
                Qt::DirectConnection, 
                Q_RETURN_ARG(int, sum),
                Q_ARG(int, num1),
                Q_ARG(int, num2));

qDebug() << "invokeMethod: " << ret << "; method result: " << sum;

首先还是获取元对象,然后创建对象实例.然后执行invokeMethod将对象实例传进去,参数名、返回值、参数等都传入进入.

Q_ARG

#define Q_ARG(type, data) QArgument<type >(#type, data)

该宏使用QArgument模板类,该模板类又继承自QGenericArgumentimage.png QGenericArgument类细节就不再说一遍了,上面已经介绍过一遍.

Q_RETURN_ARG

#define Q_RETURN_ARG(type, data) QReturnArgument<type >(#type, data)

该宏使用QReturnArgument模板类,最终还是继承自QGenericArgument
image.png image.png 开始介绍inovkeMethod代码细节:
image.png 接下来开始获取该函数的序号 image.png 该函数在当前类的序号为2,因为有信号和槽函数,这两个排在前面.
image.png 构造函数和这些函数分开,不参与这些函数的序号查找.

最终调用QMetaMethodinvoke函数调用 image.png 在执行meta->method函数,这里面设置了QMetaMethod的两个成员变量:
image.png image.png 这个handle变量就是存储下图这个位置:
image.png

下面看invoke函数细节:
首先检查返回值以及返回值类型 image.png 然后检查参数个数 image.png

接下来判断newInstance()创建的对象所在的线程invoke()这个函数所在的线程是否是同一个,用来确定连接类型. image.png

准备调用函数的参数数组和函数下标:
image.png


DirectConnection连接方式:
image.png image.png


QueuedConnection连接方式: 创建一个QMetatCallEvent对象,这里分配堆内存用来拷贝传递进来的参数值和参数类型,然后做一些关于参数类型的检查,比如是否未知类型(则调用metacall函数注册这个类型).
最终参数类型检查通过后,参数值也已拷贝,则调用QCoreApplication::postEventQMetaCallEvent对象事件添加到事件队列中,等待下一次事件循环处理.
image.png


BlockingQueuedConnection连接方式: image.png 这里不能在同一线程使用阻塞队列方式,因为既要等待事件循环处理完该事件后,当前线程才能继续执行.等待事件处理就需要当前线程继续这样。这两个是互相矛盾的,会发生死锁,所以不允许这个方式.