详解MOC的生成过程
书接上文
上文我们提到了Q_OBJECT这个宏,其展开如下:
#define Q_OBJECT \
public: \
Q_OBJECT_CHECK \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
QT_TR_FUNCTIONS \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
其中的几个virtual函数是从QObject继承而来。在探究MOC都生成了啥之前,先来看看QMetaObject究竟提供了哪些信息。
QMetaObject
直接看看类的定义:
struct Q_CORE_EXPORT QMetaObject
{
const char *className() const;
const QMetaObject *superClass() const;
struct { // private data
const QMetaObject *superdata; //父类QMetaObject实例的指针
const char *stringdata; // 字符串数组,保存类的 类名,槽函数名 信号函数名等 字符串信息
const uint *data; /该数组是个预定义的复合数据结构,由QMetaObjectPrivate 类提供管理,保存了类的基本信息,和一些索引值
const void *extradata; //额外字段,暂未使用
} d;
...
};
其中这个uint *data,实际上是一个指向 QMetaObjectPrivate 类型的指针, QMetaObjectPrivate是QMetaObject的私有实现类,该数据结构全是int类型,一些是直接的int型信息,比如classInfoCount、 methodCount等,还有一些是用于在QMetaObject的stringdata和data内存块中定位信息的索引值。其定义如下:
struct QMetaObjectPrivate
{
int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
int constructorCount, constructorData; //since revision 2
int flags; //since revision 3
int signalCount; //since revision 4
// revision 5 introduces changes in normalized signatures, no new members
// revision 6 added qt_static_metacall as a member of each Q_OBJECT and inside QMetaObject itself
};
结合QMetaObjectPrivate和 QMetaObject 中的stringdata,我们就可以进行字符串和int索引的双向关联了。
QMetaObjectPrivate 提供了搜索方法,如getIndexOfSlot,传入一个字符串,代表slot的名称,之后它会从slot段开始搜索这个字符串,找到之后就返回索引。
拿到这个索引之后的思路就很简单了,弄一个switch,依照获取的索引调用不同的函数即可。
一个实际的例子
#include <QObject>
class TestObject : public QObject
{
Q_OBJECT
Q_PROPERTY(QString propertyA READ getPropertyA WRITE getPropertyA RESET resetPropertyA DESIGNABLE true SCRIPTABLE true STORED true USER false)
Q_PROPERTY(QString propertyB READ getPropertyB WRITE getPropertyB RESET resetPropertyB)
Q_CLASSINFO("Author", "Long Huihu")
Q_CLASSINFO("Version", "TestObjectV1.0")
Q_ENUMS(TestEnum)
public:
enum TestEnum {
EnumValueA,
EnumValueB
};
public:
TestObject();
signals:
void clicked();
void pressed();
public slots:
void onEventA(const QString &);
void onEventB(int );
}
QT的思路继承了C++的,不为自己不需要的功能买单。
如果需要向一个类添加自定义的MetaData,需要使用宏来显式地添加。
这里就手动添加了五个属性,包括两个字符串属性,两个类的信息属性,以及一个类内的枚举类型属性。
MOC会解析这些宏,并添加到 QMetaObject 中。
下面就是经过MOC处理之后的文件,咱一段一段看:
1、索引信息
static const uint qt_meta_data_TestObject[] = {
// content:
4, // revision
0, // classname
2, 14, // classinfo
4, 18, // methods
2, 38, // properties
1, 44, // enums/sets
0, 0, // constructors
0, // flags
2, // signalCount
// classinfo: key, value
22, 11,
44, 29,
// signals: signature, parameters, type, tag, flags
53, 52, 52, 52, 0x05,
63, 52, 52, 52, 0x05,
// slots: signature, parameters, type, tag, flags
73, 52, 52, 52, 0x0a,
91, 52, 52, 52, 0x0a,
// properties: name, type, flags
113, 105, 0x0a095007,
123, 105, 0x0a095007,
// enums: name, flags, count, data
133, 0x0, 2, 48,
// enum data: key, value
142, uint(TestObject::EnumValueA),
153, uint(TestObject::EnumValueB),
0 // eod
};
这里,static const uint qt_meta_data_TestObject[],这里就是生成了metadata的索引,即 QMetaObjectPrivate 。里头包含了各种索引,函数个数统计,以及一些内部的flag。
2、字符串信息
static const char qt_meta_stringdata_TestObject[] = {
"TestObject\0Long Huihu\0Author\0"
"TestObjectV1.0\0Version\0\0clicked()\0"
"pressed()\0onEventA(QString)\0onEventB(int)\0"
"QString\0propertyA\0propertyB\0TestEnum\0"
"EnumValueA\0EnumValueB\0"
};
static const char qt_meta_stringdata_TestObject[],这里就存着字符串信息,可以看到里头有类的名称,槽函数的名称,手动注册的metaData等。前面的一些索引就是用来指向这里头的。
3、生成QMetaObject对象
const QMetaObject TestObject::staticMetaObject = {
{ &QObject::staticMetaObject,
qt_meta_stringdata_TestObject,
qt_meta_data_TestObject, 0 }
};
利用了前面生成的两个信息,创建一个MetaObject对象,这就是我们这个类的元对象。
4、重写MetaObject操作函数
const QMetaObject &TestObject::getStaticMetaObject() { return staticMetaObject; }
const QMetaObject *TestObject::metaObject() const {
return QObject::d_ptr->metaObject ? QObject::d_ptr->metaObject : &staticMetaObject;
}
这部分不是重点,就是重写了获取对象MetaObject的函数。
5、重写metacast函数
void *TestObject::qt_metacast(const char *_clname) {
if (!_clname) return 0;
if (!strcmp(_clname, qt_meta_stringdata_TestObject))
return static_cast<void*>(const_cast< TestObject*>(this));
return QObject::qt_metacast(_clname);
}
这个转换函数有点不太明白,具体就是传入类的名称,然后他会和自己的MetaObject的字符串部分进行比较。字符串信息的第一个就是类的名称。如果类名称匹配,就返回自己。
6、重点!!!meta_call
int TestObject::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: clicked(); break;
case 1: pressed(); break;
case 2: onEventA((*reinterpret_cast< const QString(*)>(_a[1]))); break;
case 3: onEventB((*reinterpret_cast< int(*)>(_a[1]))); break;
default: ;
}
_id -= 4;
}
#ifndef QT_NO_PROPERTIES
else if (_c == QMetaObject::ReadProperty) {
void *_v = _a[0];
switch (_id) {
case 0: *reinterpret_cast< QString*>(_v) = getPropertyA(); break;
case 1: *reinterpret_cast< QString*>(_v) = getPropertyB(); break;
}
_id -= 2;
} else if (_c == QMetaObject::WriteProperty) {
void *_v = _a[0];
switch (_id) {
case 0: getPropertyA(*reinterpret_cast< QString*>(_v)); break;
case 1: getPropertyB(*reinterpret_cast< QString*>(_v)); break;
}
_id -= 2;
} else if (_c == QMetaObject::ResetProperty) {
switch (_id) {
case 0: resetPropertyA(); break;
case 1: resetPropertyB(); break;
}
_id -= 2;
} else if (_c == QMetaObject::QueryPropertyDesignable) {
_id -= 2;
} else if (_c == QMetaObject::QueryPropertyScriptable) {
_id -= 2;
} else if (_c == QMetaObject::QueryPropertyStored) {
_id -= 2;
} else if (_c == QMetaObject::QueryPropertyEditable) {
_id -= 2;
} else if (_c == QMetaObject::QueryPropertyUser) {
_id -= 2;
}
#endif // QT_NO_PROPERTIES
return _id;
}
这里是整个系统实现的核心。你发现有一堆的switch-case!
先看参数,传了一个QMetaObject::Call,看意思是用来区分call的类型的,一个metacall可以是调用一个函数、读取属性值,写入属性值,重置属性值等等。
之后传了一个int _id,我们怎么知道call的对象呢?就通过这个id。
我们直接看InvokeMetaMethod的部分。其中看到了老朋友,我们之前定义的信号函数和槽函数,这里就很有意思了,swicth根据传入的id,调用不同的函数,仅此而已。
结合之前的 字符串 属性和 索引,一切豁然开朗。通过字符串拿到函数的索引,然后使用meta_call,根据索引就知道要调哪个函数了。
获取属性的时候也是同理,根据字符串找到属性的索引,然后用meta_call,就知道要操作的是哪个了。
最后传入的是一个双重指针,这就对应的是函数的参数,注意这里的参数调用,MOC会根据槽函数的参数类型对这个指向参数的指针进行转换,最后达到传递参数的目的。
7、实现signal
// SIGNAL 0
void TestObject::clicked()
{
QMetaObject::activate(this, &staticMetaObject, 0, 0);
}
// SIGNAL 1
void TestObject::pressed()
{
QMetaObject::activate(this, &staticMetaObject, 1, 0);
}
在定义类的时候,或许会奇怪信号是居然不需要我们实现?这里就解答了,MOC会帮你实现,信号实际上就是调用了一个函数。
至此我们已经对MOC的基本原理有了了解,这只是元对象系统的底层静态部分,至于在程序中动态的调用connect,以及信号和槽之间是如何响应的,请看下集。
叨叨几句... NOTHING