Qt 核心机制源码分析之元对象系统
2025-3-6
| 2025-4-15
Words 5026Read Time 13 min
type
status
date
slug
summary
tags
category
icon
password

Qt 核心机制源码分析之元对象系统

在 Qt Core 的官方文档中,介绍了几大核心机制:
本文基于 Qt 6.8 分析 Qt 核心机制的实现,部分细节不进行细究。

元对象系统

元对象系统是后面几个核心机制的基础。所谓的元对象系统,我个人理解是在编译期将类的各种元信息(例如类名、方法名等)编码到程序中,在运行时便可以获取到这些元信息。
元对象系统主要基于:
  1. QObject 类,所有需要使用元对象系统的类都需要继承自 QObject 类,像 Qt 中大部分类都是基于 QObject 的。
  1. Q_OBJECT 宏,可以通过 moc 编译器展开并生成各类元信息,这也是本文重点分析部分。
  1. Moc 编译器。
接下来从一个例子出发:
定义一个头文件 test.h
在这个例子中,我使用到了 Qt 的属性系统、信号槽、枚举(之前使用moc生成过文件,了解到主要存储这些元信息)。因为分析的目的是想弄明白到底存有哪些元信息?元信息是如何存储的?元信息是如何给予信号槽机制支持的?
接下来使用 moc 编译器生成 moc 文件:
moc_test.cpp 如下:
我们从头到尾分析一编:

1、 元字符串数据 qt_meta_stringdata_CLASSTestENDCLASS

这是一个存储类的元信息的数据结构,它在编译期确定,并编码进基类 QObject 中(稍后可以看到)。
元字符串数据通过qt_meta_stringdata_CLASSTestENDCLASS结构存储所有标识符的字符串字面量,包括: - 类名、信号与槽名称 - 参数名称(如信号signal_3的参数ij) - 属性名testProperty - 枚举项(如TestEnum_0TestFlag_1
存储优化:
字符串以\0分隔连续存储于stringdata0数组,通过offsetsAndSizes记录各字符串的偏移量和长度,实现高效内存访问。
我们看一下它构造时候传递的参数:
其生成的对象结构如下:
stringdata0 为元信息存储的地方,以字符串的方式进行存储。
大致如下:
notion image
我们可以看到的是,这个数据结构高效地存储了:
1、类名 (class) : “Test”
2、信号(signal)名 : “signal_1”, ““,”signal_2”, “i”, “signal_3”, “j” , “testPropertyChanged”
3、槽(slot)名 : “slot_1”, “slot_2”, “slot_3”
4、信号和槽的参数名称: “”, “i”, “j”
5、属性(property)名 : “testProperty”
6、枚举(Enum)和标志(Flags,也是一种枚举)名 : “TestEnum”, “TestEnum_0”, “TestEnum_1”, “TestFlags”, “TestFlag”, “TestFlag_1”, “TestFlag_2”` , “TestFlag_4”。
offsetsAndSizes 存储了这些信号槽、枚举标志等一些名称在 stringdata0 中的位置。我们可以通过索引获取到各个元信息。

2、元数据数组 qt_meta_data_CLASSTestENDCLASS

qt_meta_data_CLASSTestENDCLASS以紧凑的整型数组编码结构化元信息
在前面,我们介绍了元字符串数据,但是元字符串数据仅仅是存储“名称”,但是作为”现代”的反射系统,当然不止这些信息,不然 C++ 原生的 RTTI 足矣。
因此这里的元数据数组存储的是另一些额外的元信息,从生成代码的注释我们可以看出,这个数组还存储了参数的类型、属性的类型等信息。但是一个整形数组是怎么存储如此多的信息的呢?接下来一一分析:
这里我们结合注释看。

类信息内容 content

对应的数据结构为:

信号元信息 signals: name, argc, parameters, tag, flags, initial metatype offsets

对应的数据结构为:

槽元信息 slots: name, argc, parameters, tag, flags, initial metatype offsets

同上面的信号一样,其实本质上都是 methods

信号和槽的参数元信息 signals: parameters 和 // slots: parameters

前面也介绍过了,就是表示信号和槽的参数类型,以及参数名字在元字符串数据中的偏移索引。

属性元信息 properties: name, type, flags, notifyId, revision

这个 properties 如果不看源码真的很难猜,而且看源码也只能看其调用或解析的方式,只能说有点吃力,其实这只是Qt自己定的规则,其实没必要了解的太多,就不详细分析了。

枚举元信息 enums: name, alias, flags, count, data

枚举数据键值对元信息 enum data: key, value

enum data 就是上面说的枚举的元信息

小结

元字符串数据元数据数组 相互交织为反射机制提供了足够的元信息包括:信号和槽名、信号和槽参数及参数名、属性名、属性值、枚举名和值名等等。

3、静态元对象 staticMetaObject

最终的元信息是存储在 staticMetaObject 中的,这也是 Q_OBJECT 宏展开的对象。
作为元对象系统的入口,整合了:
  • 继承链:通过SuperData::link关联父类QObject的元对象
  • 类型系统qt_incomplete_metaTypeArray提供类型擦除支持,确保模板类型安全
  • 跨平台支持QMetaTypeInterface抽象了类型操作,实现元信息的平台无关性
这里用了一个列表初始化的方法,QMetaObject 中并没有声明自定义的构造器,而下面的 Data 是 public 属性的。
我们可以看到,所有数据项都是一一对应的。
其中,有一个特别重要的点是 qt_incomplete_metaTypeArray,这是一个 QMetaTypeInterface 类型的集合。
我们可以从名字中看出,这是一个类似与 Java 的 Interface 的东西,我认为这个类型是为了跨平台而存在的,统一元类型的各种操作。
这个数组提供了属性、枚举、类本身、方法(信号和槽)及其参数的类型信息。 它列出了完整的类型,并使用 QtPrivate::TypeAndForceComplete,这可能有助于在模板实例化期间管理完整和不完整类型。

4、静态元调用 qt_static_metacall

这可以说是整个元对象系统中最为重要的东西了。无论是信号的发射、槽的调用、属性的操作都是通过这个函数实现的。
我们先看一下这个函数的签名(signature)
这是个类内的静态方法,也就是说他是属于类的,而不是属于对象的。对象的信息通过 QObject* _o 来访问。
QMetaObject::Call 表示的是元调用的方式,我们从moc生成的代码上看,他一共生成了一下方式:
1、InvokeMetaMethod 通过信号和槽的元信息索引调用对应的信号和槽
我们可以看到, _id 就是信号和槽名字的索引,而参数 _a 明显就是调用时候的实参。
2、IndexOfMethod 根据传入的函数指针获取其在元信息中的索引
我们可以看到,这里的 _a 是一个指针数组。其中
  • _a[0] 为返回的索引的指针
  • _a[1] 为查找的信号或槽的函数指针
3、ReadProperty 、WriteProperty、ResetProperty、BindableProperty
是属性的读写接口等操作的元调用

5、动态元类型转换和元调用 qt_metacastqt_metacall

qt_metacast 动态类型转换:支持运行时类名检查,实现安全的动态类型转换
可以看出,该类接受一个字符串,该字符串表示要进行转换的目标类的名称,比较当前我们自定义的 Test 类与目标类的类名元信息是否相同。
如果相同则直接转换(返回this)指针。
否则则退化到 QObject 类这个共同的基类。
 
qt_metacall 动态元调用:负责动态调用信号槽、属性访问、类型注册等功能。

6、信号发射的实现

信号本质是编译器生成的胶水代码,其核心逻辑为:QMetaObject::activate 的调用
关于 QMetaObject::activate ,后续在信号和槽的篇章中介绍。

总结

元对象系统通过编译期代码生成(Moc)和运行时元信息查询,在C++静态类型系统上实现了动态反射能力。这种”代码生成+静态数据”的设计,在性能与灵活性之间取得了巧妙平衡,是Qt框架的核心基石。
单例模式Qt源码剖析之事件循环系统(一)—事件系统
Loading...