接着上一节的内容,本节讲解元对象系统综合示例在构建过程中生成的 moc_*.cpp代码 ,深入学习元对象系统(The Meta-Object System,下文简称 MOS)内部工作原理。总共分为 7 个小节来讲解,4.6.1 讲解类声明里的 Q_OBJECT 宏;4.6.2 到 4.6.4 节对 moc_widget.cpp 里的代码进行分块讲解;4.6.5 节讲解 connect 函数工作原理,4.6.6 节讲解上一节自动关联函数 QMetaObject::connectSlotsByName() 的原理;最后 4.6.7 节我们从 Qt 核心源码抽取一些函数代码,来讲解从信号触发到槽函数被调用的整个过程。
本节内容比较复杂,如果是 Qt 初学者可以大致翻一下,不要求学会,可以等将来想了解 Qt 技术内幕时再学。
对于 npcomplete 综合示例,构建过程产生了 moc_showchanges.cpp 和 moc_widget.cpp 两个 moc 代码文件,位于
D:\QtProjects\ch04\build-npcomplete-Desktop_Qt_5_4_0_MinGW_32bit-Debug\debug
文件夹里面。本节以窗体类的 moc_widget.cpp 为主来讲解元对象系统原理,moc_showchanges.cpp 里的代码结构类似,并且简单一些。对于 moc_widget.cpp 和 moc_showchanges.cpp 里的完整代码,大家打开这两个文件即可看到,下面我们按照代码分块来讲解,不集中贴它们的代码了。
1. MOS原理之一:Q_OBJECT 宏
本小节主要是讲解头文件如 widget.h、showchanges.h 里面声明的宏 Q_OBJECT,这个宏已经遇到多次了,这回我们来揭开它的面纱。前面章节提到使用元对象系统不仅需要从 QObject 类派生,还需要在类声明开头添加 Q_OBJECT 宏,那么这个宏到底是什么呢?
我们打开 widget.h 文件,然后找到 Q_OBJECT 宏,右击这个宏,右键菜单里点击“Follow Symbol Under Cursor”,如 下图所示:
QtCreator 会自动打开 Q_OBJECT 宏所在文件 C:\Qt\Qt5.4.0\5.4\mingw491_32\include\QtCore\qobjectdefs.h ,并跳转到该宏定义位置第 142 行。 Q_OBJECT 宏定义为:
/* qmake ignore 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 **); \ private: \ Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \ struct QPrivateSignal {};
|
上面行尾的反斜杠是行拼接的意思,上面 11 行代码拼接后其实只有一行宏定义。下面逐行来看看它的代码:
Q_OBJECT_CHECK 也是一个宏,它的用处是在编译时检查当前类从 QObject 继承树上的所有基类是不是都有 Q_OBJECT 宏,如果有基类没定义 Q_OBJECT 宏,那么这个宏在编译时会报错。
static const QMetaObject staticMetaObject; \
|
这句定义了关键的静态元对象 staticMetaObject,这个对象会保存该类的元对象系统信息。使用静态元对象,说明该类的所有实例都会共享这个静态元对象,而不需要重复占用内存。
virtual const QMetaObject *metaObject() const; \
|
这个虚函数是获取当前类对象里面内部元对象的公开接口,通常情况下都会返回类的静态元对象 staticMetaObject,如果当前类的对象内部使用了动态元对象(仅 QML 程序才有),才会出现返回非静态元对象。
virtual void *qt_metacast(const char *); \
|
qt_metacast 是程序运行时的对象指针转换,它可以将派生类对象的指针安全地转为基类对象指针,这是 Qt 不依赖编译器特性,自己实现的运行时类型转换。qt_metacast 参数是基类名称字符串,返回值是转换后的基类对象指针,如果转换不成功,返回 NULL。
这个宏声明用于定义翻译的 tr() 和 trUtf8() 两个内联函数,其实本质都是调用 staticMetaObject.tr() 函数。
virtual int qt_metacall(QMetaObject::Call, int, void **); \
|
qt_metacall 是非常重要的虚函数,在信号到槽的执行过程中,qt_metacall 就是负责槽函数的调用,属性系统的读写等也是靠 qt_metacall 实现。后面专门会讲它的源码。
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
|
这一个私有的静态函数,Q_DECL_HIDDEN_STATIC_METACALL 是个空宏,没啥用,就是提醒程序员这是一个隐蔽的私有静态函数。前面的 qt_metacall 会调用该私有静态函数实现槽函数调用,真正调用槽函数的就是 qt_static_metacall。
struct QPrivateSignal {};
|
QPrivateSignal 是一个私有的空结构体,对函数功能来说没啥用,就是在信号被触发时,挂在参数里提醒程序员这是一个私有信号的触发。
上面有函数的声明,就有函数的实体代码,还有静态数据的赋值等代码,这些函数实体代码都由 moc 工具自动生成,保存在 moc_*.cpp 里面,也就是当前类支持元对象系统的关键内部代码。下面三个小节我们来分块解析 moc_*.cpp。
2. MOS原理之二:信号是什么
我们在 widget.h、showchanges.h 里声明信号和槽函数时用到了关键字 signals 和 slots,这两个关键字也可以用上面右键菜单 “Follow Symbol Under Cursor”方法找到它们的定义,这两个关键字也是宏:
# ifndef QT_NO_SIGNALS_SLOTS_KEYWORDS # define slots # define signals public # endif
|
上面宏定义是说,如果没有定义不能使用 Qt 信号和槽关键字的情况下,启用 slots 和 signals 关键字定义,slots 根本就是空宏,什么都没有。signals 就是 C++ 关键字 public。
注意头文件里的 slots 和 signals 有两种意义,第一种是 moc 工具扫描这些宏,根据这些宏来生成 moc_*.cpp 里的代码;
第二种意义才是 g++、VC 等编译器来编译标准 C++ 代码,在编译时,slots 没有用,signals 就是 public。
头文件里的关键字 slots 和 signals 是必须要的,因为 moc 工具需要扫描这些关键字来生成对应的元对象系统代码。
在极罕见情况下,可能某些编译器或相关库占用了关键字 slots 和 signals,那么 Qt 可以使用两个等价的宏来声明信号和槽:
# define Q_SLOTS # define Q_SIGNALS public
|
无论有没有关键字 slots 和 signals,Q_SLOTS 和 Q_SIGNALS 这两个宏都是一直生效的,所以 Qt 源码里是优先使用 Q_SLOTS 和 Q_SIGNALS 宏的。对程序员来说,关键字 slots 和 signals 比 Q_SLOTS 和 Q_SIGNALS 宏字母少,写起来更方便,可以放心用关键字形式。
我们现在知道 signals 就是 public,那么 widget.h 里面声明的信号 nickNameChanged、countChanged、valueChanged 到底是什么呢?
对于标准 C++ 来说,信号声明就是一个函数声明,虽然对于程序员来说,没有编写信号的函数实体,信号看起来像是个空壳,但是信号真的就是函数!
我们在 moc_widget.cpp 末尾能找到三个信号函数的实体代码,是由 moc 工具自动生成的:
// SIGNAL 0 void Widget::nickNameChanged(const QString & _t1) { void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 0, _a); } // SIGNAL 1 void Widget::countChanged(int _t1) { void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 1, _a); } // SIGNAL 2 void Widget::valueChanged(double _t1) { void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 2, _a); }
|
之所以程序员不能给信号编写函数实体代码,那是因为必须由 moc 工具为信号生成实体代码,如果程序员自作聪明编一个信号函数实体,程序编译时会报错,因为重名的函数参数还一样,会报重定义错误。
因此程序员只需要声明信号,而不用管信号函数的实体代码,交给 moc 工具生成就行了。
三个信号函数里的代码是类似的,首先是指针数组 _a 的定义,_a 里面第一个指针是 Q_NULLPTR(就是 NULL),这个指针是预留给 元对系统内部注册元方法参数类型时使用;第二个指针是参数_t1 的指针;如果有更多的参数,那么会继续将指针填充到 _a 里面。_a 是用于传递从信号到槽函数的参数的。
为什么信号的参数可以比槽函数的参数多?因为槽函数接收到 _a 指针数组时,只需要取出自己需要的前面几个参数就够了,槽函数不管多余的参数。信号里的参数不能比槽函数里的少,那样槽函数访问指针数组时会越界,造成内存访问错误。
以 valueChanged 信号实体代码为例,QMetaObject::activate 函数是负责联络接收方槽函数的,它根据源头对象指针 this、源头的元对象指针 &staticMetaObject、信号序号 2、信号参数数组 _a 去找寻需要激活的槽函数,最终会调用每个关联到该信号的槽函数。最后的小节会讲如何从信号触发一步步走到槽函数调用的。
对于信号的触发,我们使用的是 emit 关键字,可以用右键菜单 “Follow Symbol Under Cursor”方法找到它的定义:
# define Q_EMIT #ifndef QT_NO_EMIT # define emit #endif
|
关键字 emit 是一个空的宏,也有等价的宏名称 Q_EMIT,无论使用 emit 关键字或者 Q_EMIT 触发信号都是可以的。由于 emit 本身是空宏,所以直接调用信号函数与使用 emit 触发信号,是没有区别的。 emit 的作用其实就是提醒程序员,这里正在触发一个信号,对编译器而言 emit 跟不存在一样。
程序员编写代码应该使用 emit 或 Q_EMIT 触发信号,这样代码的可读性更好,也更规范。
3. MOS原理之三:静态数据
打开 moc_widget.cpp ,可以看到里面有三个静态数据块,开头的第一个是 qt_meta_stringdata_Widget 结构体实例,第二个是 qt_meta_data_Widget 无符号整数数组,第三个是位于 qt_static_metacall() 函数之后的 Widget::staticMetaObject 静态元对象。我们来看看这些数据代码的意义。
(1)qt_meta_stringdata_Widget 静态字符串数据
moc_widget.cpp 文件开头是这样的:
QT_BEGIN_MOC_NAMESPACE struct qt_meta_stringdata_Widget_t { QByteArrayData data[13]; char stringdata[125]; };
|
QT_BEGIN_MOC_NAMESPACE 宏和文件末尾的 QT_END_MOC_NAMESPACE 都是空宏,仅用于提示程序员,这两个宏中间是 MOC 里的代码。
然后 struct 关键字声明了结构体类型 qt_meta_stringdata_Widget_t ,里面有两个成员,data 是 13 个 QByteArrayData 数组,stringdata 是一个长度 125 的字节数组。这里仅仅是结构体声明(相当于类的声明),没有定义结构体实例,后面涉及存储数据时才定义实例。qt_meta_stringdata_Widget_t 名字里 Widget 是类名,不同的类使用的结构体名称不一样。
如果跟踪 QByteArrayData 声明,可以发现它也是一个结构体,等同于 QArrayData,我们摘取 QArrayData声明开头的一部分,与我们本节无关的东西忽略掉了(声明文件 C:\Qt\Qt5.4.0\5.4\mingw491_32\include\QtCore\qarraydata.h ):
struct Q_CORE_EXPORT QArrayData { QtPrivate::RefCount ref; int size; uint alloc : 31; uint capacityReserved : 1; qptrdiff offset; // in bytes from beginning of header void *data() { Q_ASSERT(size == 0 || offset < 0 || size_t(offset) >= sizeof(QArrayData)); return reinterpret_cast<char *>(this) + offset; } const void *data() const { Q_ASSERT(size == 0 || offset < 0 || size_t(offset) >= sizeof(QArrayData)); return reinterpret_cast<const char *>(this) + offset; } //后面无关的都忽略掉 };
|
结构体声明里的 Q_CORE_EXPORT 是用于动态链接库导出的关键字,目前可以不用管它。
C++ 中结构体与类其实差不多,只是结构体成员默认为公有的,类成员默认为私有的,其他是一样的。所以 QArrayData 有自己的成员函数,两个重载的 data() 都是用于获取数据的函数,第一个返回非常量指针,第二个返回常量指针。
QArrayData 有五个普通成员变量,本人在 32 位系统上对这个结构体进行了实测,QArrayData 占用总长度是 16 字节。
- ref 是指引用计数,ref 自己占用结构体 4 字节,在浅拷贝里面可能用到(见 3.3.3 节),我们这里没用到,可以不管。
- size 记录数据块长度,size 为 int 类型,自己占用结构体 4 字节。
- alloc 占用 uint 类型 31 bit,而 capacityReserved 占用 uint 类型 1 bit,这二者加起来是 4 字节,这个与我们本节无关,也不管它。
- offset 是指数据块从起始位置的字节数偏移,offset 自己也占用结构体 4 字节。
上面列举的 5 个普通成员里面,我们后面用到的其实只有 size 和 offset ,其他的无视,每个 QArrayData 实例占用 16 字节。
data() 函数内部首先判断 数据块大小 size 是否为 0,以及 offset 是否超出数据块存储范围,然后它返回的数据指针是 this 指针位置 加上 offset 偏移之后的位置。data() 函数返回值,是从本结构体对象起始位置 this 开始算起,加了 offset 偏移,就是返回的数据指针。
讲 QArrayData 内容是为了解释下面这个宏的意义,我们回到 moc_widget.cpp 文件,接着往下看:
#define QT_MOC_LITERAL(idx, ofs, len) \ Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \ qptrdiff(offsetof(qt_meta_stringdata_Widget_t, stringdata) + 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, 15), // "nickNameChanged" QT_MOC_LITERAL(2, 23, 0), // "" QT_MOC_LITERAL(3, 24, 10), // "strNewName" QT_MOC_LITERAL(4, 35, 12), // "countChanged" QT_MOC_LITERAL(5, 48, 9), // "nNewCount" QT_MOC_LITERAL(6, 58, 12), // "valueChanged" QT_MOC_LITERAL(7, 71, 11), // "dblNewValue" QT_MOC_LITERAL(8, 83, 11), // "setNickName" QT_MOC_LITERAL(9, 95, 8), // "setCount" QT_MOC_LITERAL(10, 104, 8), // "nickName" QT_MOC_LITERAL(11, 113, 5), // "count" QT_MOC_LITERAL(12, 119, 5) // "value"
},
"Widget\0nickNameChanged\0\0strNewName\0"
"countChanged\0nNewCount\0valueChanged\0"
"dblNewValue\0setNickName\0setCount\0"
"nickName\0count\0value"
};
#undef QT_MOC_LITERAL |
QT_MOC_LITERAL 宏的生命期非常短,它就是为了填充结构体实例 qt_meta_stringdata_Widget 而定义的,填充完,这个宏就 被取消定义了 #undef 。
qt_meta_stringdata_Widget 是结构体类型 qt_meta_stringdata_Widget_t 的实例,它前半部分是是 13 个 QByteArrayData 组成的数组 data,后半部分是一堆字符串组成的字节数组 stringdata。
stringdata 里面是多个字符串的拼接,从 "Widget\0" 一直到 "value" 。使用 '\0' 分隔的字符串个数与 data 数组是一一对应的,都是 13 个。
QT_MOC_LITERAL 宏接收 3 个参数,idx 是 stringdata 里面字符串的序号,ofs 是 stringdata 里面字符串的起始偏移,len 是指 stringdata 里面字符串的长度。举例来说:
- 第 0 号字符串 "Widget\0" (类名),在字符串数组里偏移是 0,长度是 6;
- 第 1 号字符串 "nickNameChanged\0"(信号名称),在字符串数组里偏移是 7,长度是 15;
- 第 2 号字符串 "\0"(占位字符串,空的),在字符串数组里偏移是 23,长度是 0;
- 第 3 号字符串 "strNewName\0"(信号里的参数名),在字符串数组里偏移是 24, 长度是 10;
- 以此类推。
QT_MOC_LITERAL 宏就是根据三元组 (idx, ofs, len) 构造一个 QByteArrayData 对象填充到对应的数组位置。因为 QByteArrayData 里面并没有类似的三元组 (idx, ofs, len),QByteArrayData 其实只用了里面两个成员 size 和 offset :
size 就是 QT_MOC_LITERAL 宏参数里的 len,代表字符串长度。
offset 计算方式比较特别:
qptrdiff(offsetof(qt_meta_stringdata_Widget_t, stringdata) + ofs \ - idx * sizeof(QByteArrayData)) \
|
外层的 qptrdiff 就是把里面的数据长度变得与系统平台指针长度一样,对于 32 位系统把里面数据变成 32 位长度 qint32,对于 64 位系统把里面的数据变成 64 位长度 qint64。本人测试用的 32 位系统,所以不用管外层的 qptrdiff() ,当 qint32 类型数值就行了。
qptrdiff() 里面的计算公式是:
offsetof(qt_meta_stringdata_Widget_t, stringdata) + ofs - idx * sizeof(QByteArrayData)
|
offsetof(qt_meta_stringdata_Widget_t, stringdata) 比较好理解,就是后半截的字符数组 stringdata 在结构体 qt_meta_stringdata_Widget_t 里的偏移。
对于我们这个例子,qt_meta_stringdata_Widget_t 前半部分是 13 个 QByteArrayData 实例,而之前测试 QByteArrayData 是 16 字节长度,13*16 == 208,那么上面公式简化之后:
根据这个简化公式,计算得到 QByteArrayData 里面成员 offset 的数值。
其实 208 + ofs 就是每个字符串在大结构体 qt_meta_stringdata_Widget_t 里的相对偏移。那为什么要在这数值里面减去 idx *16 呢?
这与 QByteArrayData(同 QArrayData )里面的函数 data() 有关,之前说过 QArrayData::data() 函数返回值,是从本结构体对象起始位置 this 开始算起,加了 offset 偏移,就是返回的数据指针。
大结构体 qt_meta_stringdata_Widget_t 开头是 13 个 QByteArrayData 实例:
- 对于 0 号的 QByteArrayData 实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 0*16;
- 对于 1 号的 QByteArrayData 实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 1*16;
- 对于 2 号的 QByteArrayData 实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 2*16;
- 对于 3 号的 QByteArrayData 实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 3*16;
- 依次类推。
简化公式
里面的 idx *16 是当前 QByteArrayData 实例位置相对于 qt_meta_stringdata_Widget_t 起始位置的偏移,简单说就是 一减一加 抵消掉。然后 data() 函数返回值才会指向正确的 idx 序号的字符串起始内存位置。
idx *16 的减法是在 QT_MOC_LITERAL 宏定义提前做好,然后每个 QByteArrayData 实例的 this 指针自动把偏移加上了,这样就抵消了,最后 data() 函数返回的就是序号为 idx 的字符串内存地址指针。
QByteArrayData 两个成员 size 和 offset 的计算方式清楚了之后,QT_MOC_LITERAL 宏定义里出现的巨长的 Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET,跟踪之后其实就是下面这一句:
#define Q_STATIC_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(size, offset) \ { Q_REFCOUNT_INITIALIZE_STATIC, size, 0, 0, offset } \ /**/
|
这里面的五条数据与 QByteArrayData 结构体五个成员:ref 、size 、alloc 、capacityReserved 、offset 一一对应,填充进去就生成了 QByteArrayData 实例。
QT_MOC_LITERAL 宏的意义就是根据三元组 (idx, ofs, len) 计算并填充 QByteArrayData 实例里面 size 和 offset 两个成员变量(其他成员用固定值),然后通过 QByteArrayData::data() 函数就能获取 stringdata 里序号为 idx 的字符串起始位置指针。
搞那么复杂的 QT_MOC_LITERAL 宏,其实就是为了获取 stringdata 字符数组里每个字符串起始位置指针。
我们人类用肉眼直接看,按照 '\0' 切分字符串,很容易知道每个字符串的起始位置。
但是对于计算机程序而言,字符串处理函数都以 '\0' 为字符串结束标志,所以处理不了多个以 '\0' 分隔的字符串,因此要用这么复杂的宏配合结构体来实现。
对于结构体实例 qt_meta_stringdata_Widget,感兴趣的同学可以复制 moc_widget.cpp 开头相关代码,然后用下面简单的 for 循环打印各个字符串:
qDebug()<<sizeof(QByteArrayData); //打印字符串 for (int i=0; i<13; i++) { //把 void* 转成 char* 打印 qDebug()<< (char *)(qt_meta_stringdata_Widget.data[i].data()); }
|
打印结果如下图所示:
可见 qt_meta_stringdata_Widget_t 结构体设计得很精巧,虽然 QT_MOC_LITERAL 宏比较复杂,但是使用起来是非常简单方便的。通过 data[] 数组,可以直接打印 stringdata 里保存的每个字符串,而不用再去猜每个字符串起始位置。
代码里定义的 qt_meta_stringdata_Widget 静态实例,说白了一切都是为了字符串,结构体里的 stringdata 保存了关键的字符串信息:
- 首先是类名 "Widget";
- nickNameChanged 信号名;
- 空串,元方法标签 tag 描述占一个空位,例子的信号和槽都没有 tag。
- nickNameChanged 信号参数名;
- countChanged 信号名;
- countChanged 信号参数名;
- valueChanged 信号名;
- valueChanged 信号参数名;
- setNickName 槽函数名称;
- setCount 槽函数名称;
- 属性名 nickName;
- 属性名 count;
- 属性名 value。
基于这些字符串,Qt 程序才能在运行时自行查询类的名称、根据元方法名称字符串调用元方法、根据属性名称查询设置属性值等。讲完第一个结构体实例 qt_meta_stringdata_Widget,我们下面来看第二个数据块 qt_meta_data_Widget,这两个名字有点像,第一个是 stringdata,第二个是 data。
(2)qt_meta_data_Widget 数组
接着看 moc_widget.cpp 代码,紧跟上面字符串数据的就是 uint 数组 qt_meta_data_Widget:
static const uint qt_meta_data_Widget[] = { // content: 7, // revision 0, // classname 0, 0, // classinfo 5, 14, // methods 3, 54, // properties 0, 0, // enums/sets 0, 0, // constructors 0, // flags 3, // signalCount // signals: name, argc, parameters, tag, flags 1, 1, 39, 2, 0x06 /* Public */, 4, 1, 42, 2, 0x06 /* Public */, 6, 1, 45, 2, 0x06 /* Public */, // slots: name, argc, parameters, tag, flags 8, 1, 48, 2, 0x0a /* Public */, 9, 1, 51, 2, 0x0a /* Public */, // signals: parameters QMetaType::Void, QMetaType::QString, 3, QMetaType::Void, QMetaType::Int, 5, QMetaType::Void, QMetaType::Double, 7, // slots: parameters QMetaType::Void, QMetaType::QString, 3, QMetaType::Void, QMetaType::Int, 5, // properties: name, type, flags 10, QMetaType::QString, 0x00495103, 11, QMetaType::Int, 0x00495103, 12, QMetaType::Double, 0x00495003, // properties: notify_signal_id 0, 1, 2, 0 // eod };
|
Qt5 版本的 qt_meta_data_Widget 要比 Qt4 时候的数组复杂一些,因为 Qt5 加入了参数类型检查,因此多出来带有 QMetaType 字样的几行。我们对这个数组里的代码分块来看看:
① 目录数据条目
// content: 7, // revision 0, // classname 0, 0, // classinfo 5, 14, // methods 3, 54, // properties 0, 0, // enums/sets 0, 0, // constructors 0, // flags 3, // signalCount |
content 是目录的意思,前面 14 个数就是 qt_meta_data_Widget 里面所有数据的目录结构,前面目录部分长度是固定的,而后面关于信号、槽、参数类型等数据条目个数是不确定的,根据前面的目录里 14 个数才能知道后面有多少东 西。
目录对应一个固定的结构体,在 Qt 核心源码里可以找到结构体声明,文件为
C:\Qt\Qt5.4.0\5.4\Src\qtbase\src\corelib\kernel\qmetaobject_p.h,行号 160:
struct QMetaObjectPrivate { enum { OutputRevision = 7 }; // Used by moc, qmetaobjectbuilder and qdbus 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 // revision 7 is Qt 5 //后面的一大堆函数省略 };
|
我们逐个看看它们的意思:
- revision 为 7,这是 moc_*.cpp 文件格式的修订版本号,之前 Qt 4.8.* 用的是修订版 6,而目前 Qt 5 用的是修订版 7。
- className 总是为 0,这是前面 qt_meta_stringdata_Widget.data[0].data() 指向的字符串。
- classInfoCount 计数为 0,这是 4.4.4 节类的附加信息对应的计数,因为我们这个综合示例没有加额外信息声明,所以是 0。
- classInfoData 为 0,因为附加信息计数为零,在本数组 qt_meta_data_Widget 里面没有 classinfo 条目。
- methodCount 计数为 5,元方法计数,总共有 3 个信号,2 个 set 槽函数。
- methodData 为 14,表示元方法的数据条目从本数组 qt_meta_data_Widget[14] 开始。
- propertyCount 计数为 3,有 3 个属性声明。
- propertyData 为 54,表示属性的数据条目从本数组 qt_meta_data_Widget[54] 开始.
- enumeratorCount 计数为 0 ,因为我们没有用 Q_ENUMS(...) 声明枚举类型。
- enumeratorData 为 0 ,没有枚举类型的数据条目。
- constructorCount 计数为 0,因为我们没有用 Q_INVOKABLE 声明元构造函数。
- constructorData 为 0,没有元构造函数的数据条目。
- flags 为 0,因为我们没有用 Q_FLAGS(...) 声明标志位。
- signalCount 计数为 3,元方法的数据条目以信号函数打头,信号函数需要 moc 工具生成代码,所以需要信号计数,其他元方法的函数实体是由程序员编写的,不用额外计数。
QMetaObjectPrivate 结构体就是对 qt_meta_data_Widget 等数组(Widget是类名)的统一封装,结构体还声明了一大堆函数,就是用于处理该数组的数据条目。qt_meta_data_Widget 数组的目录部分就是为了描述后面不定长度的数据条目,如 信号、槽、属性条目等。
②元方法数据条目
讲完目录部分,下面看看信号和槽等元方法数据条目:
// signals: name, argc, parameters, tag, flags 1, 1, 39, 2, 0x06 /* Public */, 4, 1, 42, 2, 0x06 /* Public */, 6, 1, 45, 2, 0x06 /* Public */, // slots: name, argc, parameters, tag, flags 8, 1, 48, 2, 0x0a /* Public */, 9, 1, 51, 2, 0x0a /* Public */, // signals: parameters QMetaType::Void, QMetaType::QString, 3, QMetaType::Void, QMetaType::Int, 5, QMetaType::Void, QMetaType::Double, 7, // slots: parameters QMetaType::Void, QMetaType::QString, 3, QMetaType::Void, QMetaType::Int, 5,
|
上面条目对应 5 个元方法的函数特征和参数类型,目录里显示元方法条目从 qt_meta_data_Widget[14] 开始,就是指这部分数据条目,下面分别介绍信号和槽函数的数据条目:
◆ 信号 void nickNameChanged(const QString& strNewName) //moc工具生成信号函数实体
- name 为 1,对应前面 qt_meta_stringdata_Widget.data[1].data() 指向的字符串,即 "nickNameChanged"。
- argc 为 1,代表 1 个参数。
- parameters 为 39,它的参数类型位于本数组 qt_meta_data_Widget[39] 开始的位置。
- tag 为 2,没有元方法标签描述,指向空串,即 qt_meta_stringdata_Widget.data[2].data() 指向的字符串。
- flags 为 0x06,是指 MethodFlags::AccessPublic | MethodFlags::MethodSignal,公有信号,其实信号都是公有的。
解释一下 tag,Qt 可以给元方法加标签描述,运行时可以通过 QMetaMethod::tag() 函数查询,详细说明见 Qt 帮助文档,找到 QMetaMethod 类的文档,然后搜 tag() 函数即可。我们例子没有加,所以 5 个元方法的 tag 都是 qt_meta_stringdata_Widget.data[2].data() 指向的空字符串。
再就是元方法的标志位 flags,在 qmetaobject_p.h 里面也有元方法标志位声明:
enum MethodFlags { AccessPrivate = 0x00, AccessProtected = 0x01, AccessPublic = 0x02, AccessMask = 0x03, //mask MethodMethod = 0x00, MethodSignal = 0x04, MethodSlot = 0x08, MethodConstructor = 0x0c, MethodTypeMask = 0x0c, MethodCompatibility = 0x10, MethodCloned = 0x20, MethodScriptable = 0x40, MethodRevisioned = 0x80 };
|
本节例子代码元方法就是涉及前面两部分的标志位,访问权限 AccessPublic 和 方法类型 MethodSignal 、MethodSlot ,其他的都没用到。公有信号的 flags 就是两个标志位的二进制或运算,0x02 | 0x04 得到 0x06,公有槽函数标志位就是 0x02 | 0x08,得到 0x0a 。
nickNameChanged 信号的参数类型条目是从 qt_meta_data_Widget[39] 开始,也就是这一行三个:
// signals: parameters QMetaType::Void, QMetaType::QString, 3, |
Void 是返回值类型,QString 是参数类型,3 代表 qt_meta_stringdata_Widget.data[3].data() 指向的字符串,就是参数的名称 "strNewName"。
QMetaType 类是 Qt 对自己知道的所有数据类型做的辅助类,包含了各种参数类型的枚举,可用于元对象系统的参数类型识别。
剩下两个信号函数与 nickNameChanged 是类似的,只有 name 字符串序号和 parameters 参数数据有区别,就不重复列举了。
◆ 槽 void Widget::setNickName(const QString &strNewName) //程序员自己编写的函数实体
// slots: name, argc, parameters, tag, flags 8, 1, 48, 2, 0x0a /* Public */, 9, 1, 51, 2, 0x0a /* Public */, // signals: parameters QMetaType::Void, QMetaType::QString, 3, QMetaType::Void, QMetaType::Int, 5, QMetaType::Void, QMetaType::Double, 7, // slots: parameters QMetaType::Void, QMetaType::QString, 3, QMetaType::Void, QMetaType::Int, 5,
|
- name 为 8,对应前面 qt_meta_stringdata_Widget.data[8].data() 指向的字符串,即 "setNickName"。
- argc 为 1,信号有一个参数
- parameters 为 48,它的参数类型数据条目是从本数组 qt_meta_data_Widget[48] 开始。
- tag 为 2,也是 qt_meta_stringdata_Widget.data[2].data() 指向的空字符串。
- flags 为 0x0a,是公有槽函数标志位 MethodFlags::AccessPublic | MethodFlags::MethodSlot 。
该槽函数的参数类型数据条目是从 qt_meta_data_Widget[48] 开始的一行:
// slots: parameters QMetaType::Void, QMetaType::QString, 3,
|
Void 是返回值类型,QString 是参数类型,3 是指 qt_meta_stringdata_Widget.data[3].data() 指向的字符串,就是参数名字 "strNewName" 。我们在 widget.h 头文件 nickNameChanged 信号和 setNickName 槽函数的参数名都一样,所以这里槽函数的参数名也指向序号为 3 的字符串。
另一个槽函数 setCount 与 setNickName 的数据条目是类似的,只有 name 和 parameters 不一样,不重复列举了。
③属性条目
现在 qt_meta_data_Widget 数组的目录数据条目、元方法数据条目讲完了,最后一部分是关于属性的数据条目:
// properties: name, type, flags 10, QMetaType::QString, 0x00495103, 11, QMetaType::Int, 0x00495103, 12, QMetaType::Double, 0x00495003, // properties: notify_signal_id 0, 1, 2, 0 // eod //eod 代表定义结束
|
我们在头文件 widget.h 定义了三个属性 nickName、count、value,三个属性修改时发出相应的三个信号,与上面属性的数据条目一一对应。以属性 nickName 为例:
// properties: name, type, flags 10, QMetaType::QString, 0x00495103,
|
- name 为 10,是指属性名是 qt_meta_stringdata_Widget.data[10].data() 指向的字符串,就是 "nickName"。
- type 为 QMetaType::QString,是 QString 类型。
- flags 为 0x00495103,这个比较复杂,是多个基本标志位的或运算结果。
我们在 4.4.1 节属性系统简介里列的是简化属性声明,实际上属性系统很复杂,它的完整声明如下:
Q_PROPERTY(type name (READ getFunction [WRITE setFunction] | MEMBER memberName [(READ getFunction | WRITE setFunction)]) [RESET resetFunction] [NOTIFY notifySignal] [REVISION int] [DESIGNABLE bool] [SCRIPTABLE bool] [STORED bool] [USER bool] [CONSTANT] [FINAL])
|
属性声明里的行详细情况请搜 Qt 帮助文档,这里不讲了。
属性声明里每行都有它对应的标志位,所以属性的 flags 比较复杂,属性标志位也可以在 qmetaobject_p.h 里面找到:
enum PropertyFlags { Invalid = 0x00000000, Readable = 0x00000001, Writable = 0x00000002, Resettable = 0x00000004, EnumOrFlag = 0x00000008, StdCppSet = 0x00000100, // Override = 0x00000200, Constant = 0x00000400, Final = 0x00000800, Designable = 0x00001000, ResolveDesignable = 0x00002000, Scriptable = 0x00004000, ResolveScriptable = 0x00008000, Stored = 0x00010000, ResolveStored = 0x00020000, Editable = 0x00040000, ResolveEditable = 0x00080000, User = 0x00100000, ResolveUser = 0x00200000, Notify = 0x00400000, Revisioned = 0x00800000 };
|
nickName 属性标志位 0x00495103 是上面基本标志位的或运算组合,大致的意义如下:
- 4 代表 Notify ,有触发信号(nickNameChanged)。
- 9 代表 Stored 和 ResolveEditable 。
- 5 代表 Scriptable 和 Designable 。
- 1 代表 StdCppSet ,有 C++设置函数(setNickName)。
- 3 代表 Readable 和 Writable ,可读可写。
因为 nickName 属性有相关的触发信号,所以有下面的 notify_signal_id 行:
// properties: notify_signal_id 0, 1, 2,
|
nickName 属性对应 SIGNAL 0,count 属性对应 SIGNAL 1,value 属性对应 SIGNAL 2 。
介绍完属性对应的数据条目之后,qt_meta_data_Widget 数组里面的内容就都讲完了。
qt_meta_data_Widget 数组其实就是一个索引数据块,记录了元对象系统里的信号、槽、属性等信息、对应的函数名称字符串序号、参数名称字符串序号、参数类型等等。
元对象系统里面凡是字符串类型的都保存在之前的 qt_meta_stringdata_Widget 结构体实例里面,凡是索引数值之类的保存在 qt_meta_data_Widget 数组里。这两个数据块都是全局类型,而类通常要通过自己的元对象来使用这些数据,就是 moc_widget.cpp 里面定义的第三个静态数据:类的静态元对象。
(3)Widget::staticMetaObject 类的静态元对象
在 widget.h 头文件里 Q_OBJECT 宏内部声明了一个静态元对象:
static const QMetaObject staticMetaObject; \
|
moc 工具生成 moc_widget.cpp 时会为它添加赋值代码,这个赋值代码是在 qt_static_metacall() 函数代码后面:
const QMetaObject Widget::staticMetaObject = { { &QWidget::staticMetaObject, qt_meta_stringdata_Widget.data, qt_meta_data_Widget, qt_static_metacall, Q_NULLPTR, Q_NULLPTR} };
|
这就是 Widget 类的静态元对象,普通的 Qt 窗体程序都会使用这个静态元对象,它就是元对象系统的核心数据。QMetaObject 就是封装和处理元对象系统数据的核心类,它的内部有一个关键的私有数据块 d,与上面大括号里的赋值一一对应,它的声明位于:
struct Q_CORE_EXPORT QMetaObject { // 忽略前面关系不 大的 struct { // private data const QMetaObject *superdata; const QByteArrayData *stringdata; const uint *data; typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **); StaticMetacallFunction static_metacall; const QMetaObject * const *relatedMetaObjects; void *extradata; //reserved for future use } d; };
|
里面 typedef 一句是声明函数指针类型,实际函数指针变量是下面一行的 static_metacall。
私有数据 d 里面有 6 个成员变量,与 moc_widget.cpp 里面静态元对象赋值代码一一对应:
- superdata 是基类元对象指针,赋值为 &QWidget::staticMetaObject。
- stringdata 是 QByteArrayData 数组指针,赋值为前面介绍的 qt_meta_stringdata_Widget.data,用于获取元对象系统静态字符串。
- data 是元对象系统索引数值的数据块,赋值为前面介绍的 qt_meta_data_Widget。
- static_metacall 是私有静态函数指针,赋值为 qt_static_metacall,因为用到这个函数指针,所以静态元对象赋值代码放在该函数之后。
- relatedMetaObjects 是相关元对象指针,这里没有,赋值为空指针 Q_NULLPTR。
- extradata 是保留做将来用途,这里也没有,赋值为空指针 Q_NULLPTR。
元对象系统使用的静态数据就是这些,两个全局数据块 qt_meta_stringdata_Widget、qt_meta_data_Widget 以及类自己的静态元对象 Widget::staticMetaObject,设置这些数据都是在做准备工作,最终都是要靠函数来运转的。
4.MOS原理之四:暗藏的函数
这部分内容与 widget.h 里面 Q_OBJECT 宏内部声明的函数是相对应的,Q_OBJECT 宏声明了三个虚函数和一个私有静态成员函数:
virtual const QMetaObject *metaObject() const; \ virtual void *qt_metacast(const char *); \ virtual int qt_metacall(QMetaObject::Call, int, void **); \ private: \ Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
|
在 moc_widget.cpp 里面,除了前面小节介绍的信号函数实体、静态数据,就剩下这几个函数了。 moc_widget.cpp 就是对 Q_OBJECT 宏声明内容的补充源代码,与 Q_OBJECT 宏一起实现元对象系统。下面我们来看看这几个函数的定义代码,首先是获取元对象的虚函数 Widget::metaObject():
const QMetaObject *Widget::metaObject() const { return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject; } |
d_ptr 是通用基类 QObject 里特殊的 QObjectData 对象指针,QObjectData 类对象里有保存动态元对象数据的指针 metaObject:
class Q_CORE_EXPORT QObjectData { public: virtual ~QObjectData() = 0; QObject *q_ptr; QObject *parent; QObjectList children; uint isWidget : 1; uint blockSig : 1; uint wasDeleted : 1; uint isDeletingChildren : 1; uint sendChildEvents : 1; uint receiveChildEvents : 1; uint isWindow : 1; //for QWindow uint unused : 25; int postedEvents; QDynamicMetaObjectData *metaObject; QMetaObject *dynamicMetaObject() const; };
|
对于普通的 Qt 图形界面程序,QObject::d_ptr->metaObject 总是为 NULL,只有 QML 界面程序才会使用动态元对象。所以例子中的 Widget::metaObject() 函数不会返回动态元对象,在不使用 QML 的情况下,Widget::metaObject() 函数总是返回我们上面小节介绍的静态元对象指针 &staticMetaObject 。
Widget::metaObject() 是虚函数,如果我们在程序运行时获得了一个 QObject* 指针 pObj,不知道它原本是什么派生类的,那么就可以执行:
pObj->metaObject()->className();
|
由于类继承的多态性,虚函数就总能找到 pObj 原本类的静态元对象,并返回正确的类名,比如为 "Widget"。
再来看第二个虚函数 Widget::qt_metacast():
void *Widget::qt_metacast(const char *_clname) { if (!_clname) return Q_NULLPTR; if (!strcmp(_clname, qt_meta_stringdata_Widget.stringdata)) return static_cast<void*>(const_cast< Widget*>(this)); return QWidget::qt_metacast(_clname); }
|
这个函数可以在运行时把 QObject 派生类对象指针转成合法的基类对象指针,它的参数是基类名称字符串。函数里先判断 _clname 是否为空,如果是空串就返回空指针 Q_NULLPTR。
如果 _clname 不是空串,那么将字符串 _clname 与 qt_meta_stringdata_Widget.stringdata 做比较,当二者相同时,strcmp 返回值为 0,那么 Widget::qt_metacast() 函数将自身的 this 指针转成非常量指针用于返回。
qt_meta_stringdata_Widget.stringdata 里面其实有很多个以 '\0' 分隔的字符串,打头的是类名,由于 strcmp 以 '\0' 作为结束标志,所以 strcmp 只能看到打头的类名字符串,而看不到后面的一大堆东西,因此能直接将 _clname 与 qt_meta_stringdata_Widget.stringdata 做比较。
如果 _clname 与当前类名不一样,那么就继续调用基类的 QWidget::qt_metacast(_clname),这个过程一直迭代到根上的基类 QObject::qt_metacast(_clname) 为止,如果 _clname 不在类的继承树上,那么返回值就是 NULL。
qt_metacast() 函数的作用就是能在运行时根据字符串名,将当前对象转为相应的基类对象指针,如果转换不成功就是 NULL。这是 Qt 自己的运行时类型转换,而不用要求编译器的特性。
前面两个虚函数代码都很简单,第三个虚函数 Widget::qt_metacall() 就比较复杂了,它也是非常重要的调用函数,无论是属性的 get/set,还是从信号到槽函数的调用过程等,都可能用到这个函数。 Widget::qt_metacall() 内部会调用一个私有的静态成员函数,就是 qt_static_metacall()。我们先看虚函数 Widget::qt_metacall():
int Widget::qt_metacall(QMetaObject::Call _c, int _id, void **_a) { _id = QWidget::qt_metacall(_c, _id, _a); if (_id < 0) return _id; if (_c == QMetaObject::InvokeMetaMethod) { if (_id < 5) qt_static_metacall(this, _c, _id, _a); _id -= 5; } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) { if (_id < 5) *reinterpret_cast<int*>(_a[0]) = -1; _id -= 5; } #ifndef QT_NO_PROPERTIES else if (_c == QMetaObject::ReadProperty) { void *_v = _a[0]; switch (_id) { case 0: *reinterpret_cast< QString*>(_v) = nickName(); break; case 1: *reinterpret_cast< int*>(_v) = count(); break; case 2: *reinterpret_cast< double*>(_v) = m_value; break; default: break; } _id -= 3; } else if (_c == QMetaObject::WriteProperty) { void *_v = _a[0]; switch (_id) { case 0: setNickName(*reinterpret_cast< QString*>(_v)); break; case 1: setCount(*reinterpret_cast< int*>(_v)); break; case 2: if (m_value != *reinterpret_cast< double*>(_v)) { m_value = *reinterpret_cast< double*>(_v); Q_EMIT valueChanged(m_value); } break; default: break; } _id -= 3; } else if (_c == QMetaObject::ResetProperty) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyDesignable) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyScriptable) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyStored) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyEditable) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyUser) { _id -= 3; } else if (_c == QMetaObject::RegisterPropertyMetaType) { if (_id < 3) *reinterpret_cast<int*>(_a[0]) = -1; _id -= 3; } #endif // QT_NO_PROPERTIES return _id; }
|
下面对这个函数代码进行拆解,首先看函数头:
int Widget::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
|
例子中 qt_metacall 函数主要有两种用途,第一种是进行信号和槽函数、invokable 元方法等函数调用;另一种是进行属性相关的操作。
函数头有三个参数,_c 是元调用方式,而 _id 和 _a 的意义根据用途有区别:
- 对于元方法调用,_id 是元方法的绝对序号, _a 与信号函数里封装的参数指针数组 _a 是对应的;
- 对于属性操作,_id 是属性的绝对序号,_a 是属性操作需要的参数指针数组。
QMetaObject::Call 是元对象系统内部使用的元调用类型枚举,可以 Follow 它的符号名,位于文件
C:\Qt\Qt5.4.0\5.4\mingw491_32\include\QtCore\qobjectdefs.h
在 QMetaObject 类声明内部定义了这个枚举类型,用于表示元对象系统的调用方式:
enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, ResetProperty, QueryPropertyDesignable, QueryPropertyScriptable, QueryPropertyStored, QueryPropertyEditable, QueryPropertyUser, CreateInstance, IndexOfMethod, RegisterPropertyMetaType, RegisterMethodArgumentMetaType };
|
- InvokeMetaMethod 代表元方法调用,比如信号、槽函数、其他 invokable 元方法。
- 然后从 ReadProperty 到 QueryPropertyUser 一堆枚举,是属性操作相关的,因为属性声明的东西多,它的调用方式枚举也很多。
- CreateInstance 是用元构造函数生成新实例的调用方式。
- IndexOfMethod 是 Qt5 新增的调用方式,在新式语法 connect 函数内部,需要先确认 connect 函数里源头的函数指针是不是真的信号函数指针,再进行关联,后面 qt_static_metacall() 私有静态函数会根据这个调用方式,查询匹配的 信号函数的相对序号 。
- RegisterPropertyMetaType 是注册属性类型的调用方式,与 qt_meta_data_Widget 数组属性条目的 QMetaType::* 对应。
- RegisterMethodArgumentMetaType 是注册元方法参数类型的调用方式,与 qt_meta_data_Widget 数组元方法参数类型条目的 QMetaType::* 对应。
解释一下绝对序号和相对序号:基类和当前类都有一大堆元方法,这些所有的元方法都有它的绝对序号和相对序号,绝对序号是从顶层基类 QObject 开始计数,相对序号从当前类开始计数。属性的序号也是有绝对序号和相对序号,计数原理也是类似的。
讲完函数头,下面来看 qt_metacall 函数里面第一句:
_id = QWidget::qt_metacall(_c, _id, _a);
|
这一句直接调用了基类 QWidget::qt_metacall 函数,参数里的 _id 都是绝对序号,但是经过层层基类函数迭代处理之后,每个基类都会把自己的元方法计数或属性计数减掉,然后返回新的 _id ,当把所有基类的计数都减掉之后,我们得到新的 _id 就是我们当前类 Widget 里面元方法或属性的相对计数了。
开头这一句代码同时完成了两个任务:第一,如果参数里的绝对序号 _id 是属于基类的,那么基类会调用相应的元方法或进行属性操作;第二,如果参数里的绝对序号 _id 是当前类 Widget 自己声明的元方法或属性,那么绝对序号 _id 经过基类处理做减法,那么基类 QWidget::qt_metacall 返回的新 _id 就是我们当前类的元方法或属性的相对 _id ,这样就能根据当前的元方法或属性进行操作。
请分清楚,在这第一句执行前,函数头里的 _id 是绝对序号,经过第一句基类函数处理后,会得到新的相对序号 _id 。
后面的代码都是根据从本级类开始计算的相对序号来处理。
接着看 qt_metacall 函数代码:
当相对序号 _id < 0 时,说明以前的绝对序号由基类处理完了,我们这一层类就不需要干活,直接返回就可以了。_id < 0 是说明活都干完了。
接下来就是元方法调用的代码:
if (_c == QMetaObject::InvokeMetaMethod) { if (_id < 5) qt_static_metacall(this, _c, _id, _a); _id -= 5; } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) { if (_id < 5) *reinterpret_cast<int*>(_a[0]) = -1; _id -= 5; }
|
这段代码里 5 就是我们当前类 Widget 声明的元方法的计数,三个信号和两个槽函数。
◆如果调用方式 _c 为 QMetaObject::InvokeMetaMethod,说明要进行元方法调用:
- 如果相对序号 _id < 5 ,就是序号 0 到序号 4,说明是本级类的元方法,那么调用私有静态函数 qt_static_metacall 来处理。然后把 _id 减去本级类的元方法计数 5 。 注:因为我们的 Widget 类没有派生类,所以 _id 会变成负数返回,如果 Widget 类还有派生类,那么派生类会继续处理新的相对序号 _id 。
◆如果调用方式 _c 为 QMetaObject::RegisterMethodArgumentMetaType,是注册元方法参数类型,这是 Qt5 引入的新特性, 为元方法加入了参数类型的匹配,比如 connect 函数需要检查信号和槽的参数类型是否兼容,所以需要注册元方法的参数类型:
- 如果相对序号 _id < 5 ,说明是本级类的元方法,将 _a[0] 指针保存的变量数值设置为 -1,表示处理过了(全局数组 qt_meta_data_Widget 已经记录了参数类型)。然后将 _id 计数减去本级类的元方法计数 5。
qt_metacall 函数里面关于元方法调用的代码就上面这些,本级类的信号、槽、invokable 元方法主要靠私有静态函数 qt_static_metacall 处理,这个私有静态函数等会再讲。
继续看虚函数 qt_metacall 后面的代码,后面代码都是关于属性处理的,对于属性部分代码,我们拆成三块来讲解,首先是读属性操作:
#ifndef QT_NO_PROPERTIES else if (_c == QMetaObject::ReadProperty) { void *_v = _a[0]; switch (_id) { case 0: *reinterpret_cast< QString*>(_v) = nickName(); break; case 1: *reinterpret_cast< int*>(_v) = count(); break; case 2: *reinterpret_cast< double*>(_v) = m_value; break; default: break; } _id -= 3; |
如果没有定义不能使用属性的宏 QT_NO_PROPERTIES,那么才进行后面的属性操作,正常都是有属性的,可以不管这个宏。
_c 为 QMetaObject::ReadProperty,就是进行读属性的操作,本级类 Widget 总共有 3 个,现在 _id 是本级类属性的相对序号,_a 是属性操作的参数指针数组。
_a[0] 是一个指针,对于读属性,它指向返回值的变量,为了写代码方便,把 _a[0] 指针赋给了 _v。
根据属性相对序号 _id :
- 0号属性是 nickName,使用读函数 nickName() 得到 QString 变量,然后把返回值填到 _v 指向的变量。
- 1号属性是 count,使用读函数 count() 得到 int 变量,也把返回值填充到 _v 指向的变量。
- 2号属性是 value,注意我们在 widget.h 声明 value 属性时,指定了私有成员 m_value,但省略了读写函数,所以 moc 工具自动添加了读 value 属性的代码,就是把私有成员 m_value 数值填充到 _v 指向的变量。
本级类做完读属性处理后,把 _id 减去本级类的属性计数 3 。
接下来是属性操作的第二块代码,写属性操作:
} else if (_c == QMetaObject::WriteProperty) { void *_v = _a[0]; switch (_id) { case 0: setNickName(*reinterpret_cast< QString*>(_v)); break; case 1: setCount(*reinterpret_cast< int*>(_v)); break; case 2: if (m_value != *reinterpret_cast< double*>(_v)) { m_value = *reinterpret_cast< double*>(_v); Q_EMIT valueChanged(m_value); } break; default: break; } _id -= 3;
|
为 QMetaObject::WriteProperty 时,就是写属性操作,这时候 _a[0] 指针是指向输入参数,用于设置属性值。同样将 _a[0] 赋给 _v ,方便后面写代码。
根据属性相对序号 _id:
- 0号属性 nickName,设置属性的函数为 setNickName,把输入参数指针 _v 转为 QString*,然后取出 QString 变量作为参数。
- 1号属性 count,设置属性的函数为 setCount,把输入参数指针 _v 转为 int*,然后取出整型变量作为参数。
- 2号属性 value,指定了对应的私有成员 m_value,但没有声明读写函数,所以 moc 工具自动为该属性生成写属性代码,首先将私有成员设置成 _v 指向的变量数值,然后触发 valueChanged 信号。因为 value 属性声明了信号,所以 moc 工具就会在修改成员时发出相应的信号。在指定成员变量的情况下能省略属性的读写函数,这是 Qt 提供的方便特性,但是 moc 工具生成的代码是固定的,它不会对数值做有效性检查。一般建议手动编写属性的读写函数。
处理完本级类的属性之后,_id 减去本级类的属性计数 3。
属性的读写两块代码讲完之后,剩下的是属性声明其他部分对应的代码:
} else if (_c == QMetaObject::ResetProperty) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyDesignable) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyScriptable) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyStored) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyEditable) { _id -= 3; } else if (_c == QMetaObject::QueryPropertyUser) { _id -= 3; } else if (_c == QMetaObject::RegisterPropertyMetaType) { if (_id < 3) *reinterpret_cast<int*>(_a[0]) = -1; _id -= 3; } #endif // QT_NO_PROPERTIES return _id; }
|
ResetProperty 对应属性的重置函数,之前都没有写重置函数,其他的如 QueryPropertyDesignable、QueryPropertyScriptable、QueryPropertyStored、 QueryPropertyEditable、QueryPropertyUser,我们在属性声明里都没有加这些行,所以这里的代码全是空的,仅仅是把 _id 减去本级类的属性计数 3,代表本级类处理了。
RegisterPropertyMetaType 是注册属性的类型,与注册函数参数类型是相似的,把 _a[0] 指向的变量数值设置为 -1,然后减去本级类的属性计数 3 。关于属性的操作代码就这些,最后一行是返回新的 _id 。
之前讲过 _id 在经过基类函数处理后,变成当前类的元方法或属性的相对计数,qt_metacall 继续返回新的 _id 就是为了如果当前类 Widget 还有自己的派生类,那么派生类就可以按照新的相对 _id 继续执行下去。
从 QObject 基类到我们这层 Widget 类,都有类似的虚函数 qt_metacall 进行元方法调用和属性操作,从而实现这种一层层的基于 _id 序号的处理方式。基于序号的处理方式是非常精确的,每层基类处理都不能出错,整个元对象系统才会正常工作。
Qt 类库的特点就是设计非常精巧,它本身类库的内部源码、内部实现原理比较复杂,然而利用它已经做好的类库和信号、槽、属性等特性是非常简单的,所以使用 Qt 库开发程序是轻松愉快的事。
讲完三个虚函数 metaObject()、qt_metacast()、qt_metacall() ,还剩下一个私有静态函数 qt_static_metacall(),这个函数用于处理当前类的元方法调用以及信号函数相对序号查询,qt_metacall() 会调用这个私有静态函数。下面来看看这个私有静态函数的代码:
void Widget::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) { if (_c == QMetaObject::InvokeMetaMethod) { Widget *_t = static_cast<Widget *>(_o); switch (_id) { case 0: _t->nickNameChanged((*reinterpret_cast< const QString(*)>(_a[1]))); break; case 1: _t->countChanged((*reinterpret_cast< int(*)>(_a[1]))); break; case 2: _t->valueChanged((*reinterpret_cast< double(*)>(_a[1]))); break; case 3: _t->setNickName((*reinterpret_cast< const QString(*)>(_a[1]))); break; case 4: _t->setCount((*reinterpret_cast< int(*)>(_a[1]))); break; default: ; } } else if (_c == QMetaObject::IndexOfMethod) { int *result = reinterpret_cast<int *>(_a[0]); void **func = reinterpret_cast<void **>(_a[1]); { typedef void (Widget::*_t)(const QString & ); if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Widget::nickNameChanged)) { *result = 0; } } { typedef void (Widget::*_t)(int ); if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Widget::countChanged)) { *result = 1; } } { typedef void (Widget::*_t)(double ); if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Widget::valueChanged)) { *result = 2; } } } } |
我们看 qt_static_metacall() 函数里面 if - else if 的结构,可以知道它是两种用途,第一种就是上面 qt_metacall() 调用元方法函数时,会通过 qt_static_metacall() 来实现;另一种用途是以 QMetaObject::IndexOfMethod 方式调用,注意qt_static_metacall() 负责查询本级类的信号函数的相对序号,而 Qt 文档中另一个函数是查询元方法绝对序号的:
int QMetaObject::indexOfMethod(const char * method) const
|
读者要看仔细了,indexOfMethod() ,首字母小写,这是一个普通函数。IndexOfMethod 是首字母都大写,是一个枚举常量。虽然名称比 较像,但是它们根本没关系!
公开函数 QMetaObject::indexOfMethod() 和私有静态函数 Widget::qt_static_metacall() ,这两个函数其实没什么关系,工作原理和用途都不一样:
- 公开函数 QMetaObject::indexOfMethod()是基于字符串的比较,与函数指针没关系,用于所有元方法的绝对序号查询。
- Widget::qt_static_metacall() 里面 QMetaObject::IndexOfMethod 相关代码,是在新式语法 connect 函数调用里,判断 connect 函数参数源头的函数指针是不是真的信号函数指针,基于函数指针匹配,得到信号函数的相对序号。
下面我们先看 qt_static_metacall() 第一种用途的代码,就是调用元方法:
void Widget::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) { if (_c == QMetaObject::InvokeMetaMethod) { Widget *_t = static_cast<Widget *>(_o); switch (_id) { case 0: _t->nickNameChanged((*reinterpret_cast< const QString(*)>(_a[1]))); break; case 1: _t->countChanged((*reinterpret_cast< int(*)>(_a[1]))); break; case 2: _t->valueChanged((*reinterpret_cast< double(*)>(_a[1]))); break; case 3: _t->setNickName((*reinterpret_cast< const QString(*)>(_a[1]))); break; case 4: _t->setCount((*reinterpret_cast< int(*)>(_a[1]))); break; default: ; }
|
函数头有四个参数:
第一个是对象指针 _o,因为静态函数没有对象的 this 指针,需要手动传递;
第二个是元调用的方式,QMetaObject::InvokeMetaMethod 是元方法调用,QMetaObject::IndexOfMethod 是元方法(信号)查询;
第三个是元方法的相对序号,调用元方法需要这个参数,而查询信号不用这个参数;
第四个是参数指针数组,调用元方法时,_a 是参数指针数组,而查询信号相对序号时,_a[0]记录相对序号数值。
对于调用元方法的用途,qt_metacall()是这样调用私有静态函数的:
qt_static_metacall(this, _c, _id, _a);
|
手动传递了 this 指针,_c 是调用方式,_id 是元方法的相对序号,_a 是参数指针数组。
qt_static_metacall() 先把参数里的 _o 指针(原来是 this 指针)转换为原本的 Widget * 类型,现在对象指针叫 _t ,
然后根据相对序号 _id :
- 0 号元方法是 nickNameChanged,参数指针保存在 _a[1] 里面,转成 QString *,再取出字符串变量作为信号函数参数。
- 1 号元方法是 countChanged,参数指针保存在 _a[1] 里面,转成 int *,再取出整型变量作为信号函数参数。
- 2 号元方法是 valueChanged,参数指针保存在 _a[1] 里面,转成 double *,再取出双精度浮点数变量作为信号函数参数。
- 3 号元方法是 setNickName,参数指针保存在 _a[1] 里面,转成 QString *,再取出字符串变量作为槽函数参数。
- 4 号元方法是 setCount,参数指针保存在 _a[1] 里面,转成 int *,再取出整型变量作为槽函数参数。
对于元方法调用,使用的是相对序号 _id 来找到对应函数执行。需要注意的是,我们之前信号函数不是用 emit 触发的吗?
在 qt_static_metacall() 函数里调用槽函数是正常的,因为 emit 信号函数之后,会一步步走到 qt_static_metacall(),然后应该调用槽函数。
为什么 qt_static_metacall() 还要调用信号函数?
我们在 4.3.3 小节,有信号关联到信号的示例,因为信号不仅可以关联调用槽函数,还可以关联调用另一个兼容的信号函数。
不仅可以从信号函数触发,一步步走到槽函数,也可以从信号函数触发,一步步走到另一个信号函数,所以 qt_static_metacall() 函数里既调用信号函数,也调用槽函数,这是 Qt 信号和槽函数的灵活性。
讲完元方法调用的代码,接下来是关于元方法( 信号 )序号查询的代码,我们把元 方法查询的代码分成四小块来看:
} else if (_c == QMetaObject::IndexOfMethod) { int *result = reinterpret_cast<int *>(_a[0]); void **func = reinterpret_cast<void **>(_a[1]);
|
元方法查询其实只用到两个参数,一个是 _c ,判断是不是 QMetaObject::IndexOfMethod,是这个查询就进入元方法序号查询的代码。另一个参数是 _a 指针数组,_a[0] 转换成 int * 类型 result ,指向一个整型变量,保存查询到的相对序号用于反馈(是输出);_a[1] 是输入参数指针,转成 void ** 类型的 func ,func 是指向函数指针的指针,是被查询的函数指针变量(是输入)。
接下来三个小块都是大括号封装的代码,这里大括号没有定义函数、类或结构体,就是单纯的一对大括号,仅仅用于限定类型声明的作用域,类型声明超出大括号范围就失 效,然后就可以进行下一次声明。第一段大括号封装的代码:
{ typedef void (Widget::*_t)(const QString & ); if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Widget::nickNameChanged)) { *result = 0; } }
|
这是赤裸裸的一对大括号,里面定义了一个函数指针类型 _t ,简单说 _t 就是与 &Widget::nickNameChanged 函数指针相同类型。接下来把 func 指向的函数指针转换成 _t 类型,把 &Widget::nickNameChanged 也转成 _t 类型,二者函数指针一样,说明要查询的函数指针与 nickNameChanged 信号一样,返回 nickNameChanged 信号函数的序号 0 。
_t 函数指针类型声明,一旦超出大括号范围就是失效,然后我们下面就可以重新定义 _t 了。下面看第二段大括号里内容:
{ typedef void (Widget::*_t)(int ); if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Widget::countChanged)) { *result = 1; } }
|
第二段是重新定义 _t 为 &Widget::countChanged 同类型函数指针,然后将 func 指向的函数指针与该信号函数指针做匹配,如 果一样就返回 countChanged 信号函数的序号 1 。
_t 类型超出大括号范围就失效,然后在第三段重新定义:
{ typedef void (Widget::*_t)(double ); if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Widget::valueChanged)) { *result = 2; } }
|
第三段的 _t 是 &Widget::valueChanged 同类型函数指针,然后把 func 指向的函数指针与该信号函数指针做匹配,如果一样就返回 valueChanged 信号函数的序号 2。
qt_static_metacall() 里面关于元方法相对序号查询的代码,实际仅与信号函数有关,没有槽函数什么事。这与上面代码的用途有关,在新式语法的 connect 函数里,比如:
connect(ui->pushButton, &QPushButton::clicked, this, &Widget::FoodIsComing);
|
这句函数调用里面,怎么知道 clicked 一定是信号,而不是其他成员函数呢?所以新式语法里面必须先检查 &QPushButton::clicked 是不是信号函数指针,然后再做关联。
新式语法的 connect 函数调用,源头的函数指针必须是信号函数指针,因此需要通过源头的 qt_static_metacall() 来查询源头的函数指针是不是信号函数指针,根据反馈的 _a[0] 指向的整型变量数值来确认。
新式语法对于接收端的函数指针其实没什么要求,可以是信号、槽、普通成员函数,甚至是 Lambda 函数,所以接收端函数不需要判断是不是槽。之前没说新式语法接收端的函数指针可以是普通成员函数、Lambda 函数,是不想把问题搞复杂。
到这里,前四小节的内容就讲完了,moc_widget.cpp 里的代码也全部讲完了。moc_showchanges.cpp 里的代码与 moc_widget.cpp 类似,但简单许多,所以不讲 moc_showchanges.cpp 里的代码了。
经过前面四个小节学习,我们知道信号是函数,里面调用了 QMetaObject::activate() 函数;
而对于接收端的槽函数或信号,是通过私有静态函数 qt_static_metacall() 调用元方法。
源头是有的,接收端也是有的,中间少了个桥,这个桥就是 Qt 元对象系统的技术内幕,我们需要阅读 Qt 核心源码才能知道。 后面三个小节就是把从源头到接收端的桥解析一下。
5. MOS原理之五:connect
使用信号和槽机制,就必须将源头的信号和接收端的槽函数关联起来,connect 函数有旧式语法和新式的函数指针语法,在这一小节我们主要介绍旧式语法的关联函数。关联函数的源代码位于文件:
C:\Qt\Qt5.4.0\5.4\Src\qtbase\src\corelib\kernel\qobject.cpp
connect 函数有多个重载,我们讲最常见的这个:
QMetaObject::Connection connect(const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection)
|
源头信号是字符串表示的,接收端方法也是字符串表示的,这个静态函数调用举例:
QObject :: connect (& w , SIGNAL ( countChanged ( int )), & s , SLOT ( RecvCount ( int )));
|
这里面有两个宏,可以在头文件 qobjectdefs.h 找到它的声明(与 Q_OBJECT 宏代码在一样的头文件):
# define SLOT(a) "1"#a # define SIGNAL(a) "2"#a
|
#a 是指引用参数的字符串形式,对于包裹槽函数的宏,前面加 "1",对于包裹信号的宏,前面加 "2",因此举例的 connect 函数调用其实等同于下面这句:
QObject::connect(&w, "2countChanged(int)", &s, "1RecvCount(int)");
|
"1" 和 "2" ,称为方法类型代号,是用来区分信号和槽函数名称的,必须确保发送端一定是信号函数的名称字符串。接收端既可以是信号,也可以是槽函数,要求相对宽松一些。
对于 Qt 5.4.0 的源码,该 connect 函数源代码是在 qobject.cpp 文件 2613 行,函数代码比较复杂,不好直接贴出来,安装 Qt 时选择安装了源代码就可以看到 qobject.cpp 文件,下面把该函数的大致过程用文字描述一下,这样方便理解:
(1)判断输入参数 sender、receiver、signal、method,如果有一个为空,那么返回空连接。
(2)检查 sender 和 signal,把 signal 打头的字符解析为方法类型代号,比如 "2" 转成数值 2,如果是信号类型就继续,否则返 回空连接。
(3)跳过 signal 打头的数字字符,根据源头元对象 smeta、信号名、信号参数个数、信号参数类型等计算源头信号的相对序号(函数为 QMetaObjectPrivate::indexOfSignalRelative,返回序号保存到 signal_index)。
(4)如果信号相对序号是负数,说明没找到,那么把 signal 信号名做一下规范化(QMetaObject::normalizedSignature), 去除多余空格等,然后
再用 QMetaObjectPrivate::indexOfSignalRelative 查一次信号的相对序号 signal_index。
(5)如果上面两次查询结果都是负数,没找到序号,打印警告,返回空连接;如果相对序号正确则继续。
(6)对相对序号 signal_index 做处理,如果是克隆信号就改为使用原始信号序号,否则就用前面的相对序号。
(7)把相对序号 signal_index 加上所有基类信号计数,变成新的绝对序号,后面 signal_index 就是信号的绝对序号。
(8)提取 method 打头的数字字符转成方法类型代号,比如 "1" 转成 1,"2" 转成 2,只要是槽代号或信号代号,那就继续,否则返回空连 接。
(9)跳过 method 打头的数字字符,根据接收函数的类型:
如果 method 是槽函数类型,使用 QMetaObjectPrivate::indexOfSlotRelative 函数计算槽函数相对序号 method_index_relative;
如果 method 是信号类型,那么使用 QMetaObjectPrivate::indexOfSignalRelative 函数计算信号的相对序号 method_index_relative。
(10)判断相对序号 method_index_relative 是否为负数,如果为负数,那么把接收函数名称做 规范化(QMetaObject::normalizedSignature),去除多余空格等,然后对规范化的接收函数名称:
如果是槽函数类型,那么使用 QMetaObjectPrivate::indexOfSlotRelative 函数计算槽函数相对序号 method_index_relative;
如果是信号类型,那么使用 QMetaObjectPrivate::indexOfSignalRelative 函数计算信号的相对序号 method_index_relative。
(11)如果上面计算的相对序号 method_index_relative 还是负数,说明没找到接收端的信号或槽函数,返回空连接;如果相对序号是正确的,那么继续。
(12)使用 QMetaObjectPrivate::checkConnectArgs 函数检查信号函数参数个数、类型与接收函数的参数个数、类型是否 能兼容,如果不兼容返回空连接,如果兼容就继续往下走。
(13)如果连接类型 type == Qt::QueuedConnection,那么使用 queuedConnectionTypes 函数计算入队关联需要的额外类型指针 types。
(14)如果没定义不能调试的宏 QT_NO_DEBUG,那么按下面三步再次检查源头信号和接受端函数:
①QMetaObjectPrivate::signal 函数根据源头元对象smeta和信号相对序号 signal_index,得到信号元方法 smethod;
②计算 method_index_relative + rmeta->methodOffset() ,也就是接收端的元方法绝对序号,然后通过 rmeta->method 函数得到接收端元方法 rmethod;
③检查源头元对象 smeta、元方法 smethod 、接收端元对象 rmeta、元方法 rmethod 是不是具有 QMetaMethod::Compatibility 特性。
(15)前面全都是参数判断和查询序号,最后才是关键的一步,进行实际的连接操作:
QMetaObject :: Connection handle = QMetaObject :: Connection ( QMetaObjectPrivate :: connect ( sender , signal_index , smeta , receiver , method_index_relative , rmeta , type , types )); return handle ;
真正的连接操作由 QMetaObjectPrivate::connect 函数完成,然后返回新的连接 handle 。 这个 connect 函数源代码有一百多行代码,前面的大部分代码都是在做参数检查、计算源头信号和接收端函数的序号等等。检查做完了,最后才是最关键的 QMetaObjectPrivate::connect() 函数调用。
QMetaObjectPrivate::connect() 函数源代码位于 qobject.cpp 文件 3223 行,这个函数相对简单一些,可以把代码 贴出来:
QObjectPrivate::Connection *QMetaObjectPrivate::connect(const QObject *sender, int signal_index, const QMetaObject *smeta, const QObject *receiver, int method_index, const QMetaObject *rmeta, int type, int *types) { QObject *s = const_cast<QObject *>(sender); QObject *r = const_cast<QObject *>(receiver); int method_offset = rmeta ? rmeta->methodOffset() : 0; Q_ASSERT(!rmeta || QMetaObjectPrivate::get(rmeta)->revision >= 6); QObjectPrivate::StaticMetaCallFunction callFunction = rmeta ? rmeta->d.static_metacall : 0; QOrderedMutexLocker locker(signalSlotLock(sender), signalSlotLock(receiver)); if (type & Qt::UniqueConnection) { QObjectConnectionListVector *connectionLists = QObjectPrivate::get(s)->connectionLists; if (connectionLists && connectionLists->count() > signal_index) { const QObjectPrivate::Connection *c2 = (*connectionLists)[signal_index].first; int method_index_absolute = method_index + method_offset; while (c2) { if (!c2->isSlotObject && c2->receiver == receiver && c2->method() == method_index_absolute) return 0; c2 = c2->nextConnectionList; } } type &= Qt::UniqueConnection - 1; } QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection); c->sender = s; c->signal_index = signal_index; c->receiver = r; c->method_relative = method_index; c->method_offset = method_offset; c->connectionType = type; c->isSlotObject = false; c->argumentTypes.store(types); c->nextConnectionList = 0; c->callFunction = callFunction; QObjectPrivate::get(s)->addConnection(signal_index, c.data()); locker.unlock(); QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index); if (smethod.isValid()) s->connectNotify(smethod); return c.take(); }
|
return c . take (); } QMetaObjectPrivate::connect() 函数有 8 个参数:
sender 是源头;signal_index 源头信号绝对序号;smeta 是源头的元对象;
receiver 是接收方;method_index 是接收函数的相对序号;rmeta 是接收端的元对象;
type 是连接(关联)类型;types 是多线程程序的入队关联需要的额外类型指针。
QObject::connect 函数有多个重载,其他重载的 QObject::connect 也会调用 QMetaObjectPrivate::connect() 函数,我们下面只按照上面解释的 QObject::connect 函数里调用的 QMetaObjectPrivate::connect() 函数来讲:
QObject *s = const_cast<QObject *>(sender); QObject *r = const_cast<QObject *>(receiver);
|
因为关联源头信号和接收端函数需要修改源头内部数据和接收端内部数据,const_cast 把常量指针转为非常量指针,并且还是指向原来的对象。现在源头指针是 s,接收端是 r 。
int method_offset = rmeta ? rmeta->methodOffset() : 0; Q_ASSERT(!rmeta || QMetaObjectPrivate::get(rmeta)->revision >= 6); QObjectPrivate::StaticMetaCallFunction callFunction = rmeta ? rmeta->d.static_metacall : 0;
|
如果 rmeta 不为空,那么计算接收端的元方法偏移 method_offset,后面会用偏移加上相对序号得到接收端函数的绝对序号。
我们这里的 rmeta 都是有确定值,不为空,所以忽略 Q_ASSERT 判断即可。
callFunction 就是接收端私有数据保存的 d.static_metacall,我们在 4.6.4 讲过一个私有静态函数 qt_static_metacall(),现在 callFunction 就是接收端的私有静态函数 qt_static_metacall()。
QOrderedMutexLocker locker(signalSlotLock(sender), signalSlotLock(receiver));
|
关联操作需要访问、修改源头和接收端的信号和槽信息,因此需要使用互斥锁,独占源头和接收端信号和槽信息的访问权,先锁定访问权,然后再进行操作。这对于多线程操 作尤为重要,不能多个线程同时操作一对源头和接收端,那样会造成信号和槽数据的混乱,如果情况严重会导致程序崩溃,因此启用互斥锁。
一般情况下,关联类型 type 是 Qt::AutoConnection、Qt::DirectConnection、Qt::QueuedConnection 或Qt::BlockingQueuedConnection,这些类型的关联,可以将完全相同的源头、信号、接收端、槽函数关联多次,那样信号触发一次,槽函数会被调用 多次,重复关联是有效的。
Qt::UniqueConnection 是唯一关联的标志,可以与其他四种关联标志叠加(OR 运算),那样就会执行上面一段代码,检查关联的唯一性,如果是重复关联就返回 0 ,如果没重复就进行唯一性关联。上面代码就是穷举比较源头信号关联列表 c2 里面有没有重复的关联。一般关联类型 type 都是 Qt::AutoConnection,所以可以不用管上面一段唯一性检查的代码。
这段代码开始出定义了一个指针 c,它指向一个新建的 QObjectPrivate::Connection 对象,每一次从源头信号到接收端的函数的连接,都会有这样一个连接对象。然后往 c 指向的连接对象里填充数据:
- 发送源头对象;
- 源头信号绝对序号;
- 接收端对象;
- 接收端方法相对序号;
- 接收端元方法偏移;
- 连接类型;
- 是否为 SlotObject,新式关联语法和 QML程序会用到这个奇怪的 SlotObject。
- 保存多线程入队关联需要的 types 指针。
- nextConnectionList 是下一个链表节点,暂时为 0,以后由 QObjectPrivate::addConnection 函数填充。
- callFunction 就是接收端的私有静态函数 qt_static_metacall(),这个接收端私有静态函数是可以根据相对序号调用元方法的。
填充好数据之后,QObjectPrivate::addConnection 函数根据信号绝对序号 signal_index 和连接数据 c.data() 添加新的连接。
感兴趣的读者可以跟踪 QObjectPrivate::addConnection 函数源码,这里不贴代码了,一张数据结构图说明:
添加好新的连接之后,后面代码如下:
locker.unlock(); QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index); if (smethod.isValid()) s->connectNotify(smethod); return c.take(); }
|
因为连接添加完了,修改信号和槽数据的工作结束,进行解锁。
然后获取源头信号的元方法 smethod ,在连接成功建立之后需要发个提醒表示连接成功。如果连接过程耗时很长,其他线程需要等这个连接成功,那么 connectNotify 发的通知就可能有用。一般情况下可以不用管这个通知。
最后 c.take() 是返回这个连接对象,然后 c 指针自己重置为空。
关于旧式语法的 connect 函数就介绍这么多, connect 函数执行过程中,关键的数据就是上图里的数据结构,因为信号的个数在程序编译时就确定了,个数是固定的,所以用向量来保存,而单个信号使用的连接列表,长度不固定,运行时 长度是变的,所以用链表结构管理。
旧式语法是用字符串名查函数的序号,而新版函数指针语法的 connect 函数是模板函数,在文件:
C:\Qt\Qt5.4.0\5.4\Src\qtbase\src\corelib\kernel\qobject.h
可以找到它的代码,从 212 行开始。新式语法代码不讲了,感兴趣的读者可以去翻翻,使用的数据结构是类似的。
因为新式语法专门为接收端构建了 SlotObject,里面可以塞一些奇奇怪怪的函数,所以新式语法比旧式语法灵活得多。
6. MOS原理之六:自动关联
之前 4.5 节留了一个在 ui_*.h 文件里实现自动关联的函数:
void QMetaObject::connectSlotsByName(QObject * object)
|
在源文件
C:\Qt\Qt5.4.0\5.4\Src\qtbase\src\corelib\kernel\qobject.cpp
第 3433 行可以找到实现自动关联的代码。自动关联的过程就是根据字符串匹配查找发送源头、信号,然后关联到 object 对象自己的槽函数。发送源头通常是 object 对象自己的内部成员,比如大窗口里面的一堆子控件,子控件的信号关联到大窗口自己的槽函数。大窗口槽函数必须严格按规则命名:
<object name> 是子控件名字,<signal name> 就是子控件的信号名。
下面我们来看看 QMetaObject::connectSlotsByName 函数的源代码:
void QMetaObject::connectSlotsByName(QObject *o) { if (!o) return; const QMetaObject *mo = o->metaObject(); Q_ASSERT(mo); const QObjectList list = // list of all objects to look for matching signals including... o->findChildren<QObject *>(QString()) // all children of 'o'... << o; // and the object 'o' itself // for each method/slot of o ... for (int i = 0; i < mo->methodCount(); ++i) { const QByteArray slotSignature = mo->method(i).methodSignature(); const char *slot = slotSignature.constData(); Q_ASSERT(slot); // ...that starts with "on_", ... if (slot[0] != 'o' || slot[1] != 'n' || slot[2] != '_') continue; // ...we check each object in our list, ... bool foundIt = false; for(int j = 0; j < list.count(); ++j) { const QObject *co = list.at(j); const QByteArray coName = co->objectName().toLatin1(); // ...discarding those whose objectName is not fitting the pattern "on_<objectName>_...", ... if (coName.isEmpty() || qstrncmp(slot + 3, coName.constData(), coName.size()) || slot[coName.size()+3] != '_') continue; const char *signal = slot + coName.size() + 4; // the 'signal' part of the slot name // ...for the presence of a matching signal "on_<objectName>_<signal>". const QMetaObject *smeta; int sigIndex = co->d_func()->signalIndex(signal, &smeta); if (sigIndex < 0) { // if no exactly fitting signal (name + complete parameter type list) could be found // look for just any signal with the correct name and at least the slot's parameter list. // Note: if more than one of thoses signals exist, the one that gets connected is // chosen 'at random' (order of declaration in source file) QList<QByteArray> compatibleSignals; const QMetaObject *smo = co->metaObject(); int sigLen = qstrlen(signal) - 1; // ignore the trailing ')' for (int k = QMetaObjectPrivate::absoluteSignalCount(smo)-1; k >= 0; --k) { const QMetaMethod method = QMetaObjectPrivate::signal(smo, k); if (!qstrncmp(method.methodSignature().constData(), signal, sigLen)) { smeta = method.enclosingMetaObject(); sigIndex = k; compatibleSignals.prepend(method.methodSignature()); } } if (compatibleSignals.size() > 1) qWarning() << "QMetaObject::connectSlotsByName: Connecting slot" << slot << "with the first of the following compatible signals:" << compatibleSignals; } if (sigIndex < 0) continue; // we connect it... if (Connection(QMetaObjectPrivate::connect(co, sigIndex, smeta, o, i))) { foundIt = true; // ...and stop looking for further objects with the same name. // Note: the Designer will make sure each object name is unique in the above // 'list' but other code may create two child objects with the same name. In // this case one is chosen 'at random'. break; } } if (foundIt) { // we found our slot, now skip all overloads while (mo->method(i + 1).attributes() & QMetaMethod::Cloned) ++i; } else if (!(mo->method(i).attributes() & QMetaMethod::Cloned)) { // check if the slot has the following signature: "on_..._...(..." int iParen = slotSignature.indexOf('('); int iLastUnderscore = slotSignature.lastIndexOf('_', iParen-1); if (iLastUnderscore > 3) qWarning("QMetaObject::connectSlotsByName: No matching signal for %s", slot); } } }
|
QMetaObject::connectSlotsByName 函数内部的英文注释解释比较清楚,我们讲一下大致过程: (1)判断输入参数 o 是否为空,是空的就返回,否则继续。
(2)获取 o 的元对象 mo,保证 mo 也不为空。
(3)获取 o 包含的对象列表 list,QObject::findChildren 函数参数如果为空串就是获取所有子对象,
然后把 o 对象自己也加到 list 里面,这样 list 包含所有子对象和 o 自己。
(4)接下来的所有代码是两层 for 循环:
首先外层 for 循环逐个查找 mo 里包含的所有元方法,取出元方法名字 slot,看看头三个字符是不是 "on_",如果不是这三个字符打头就跳过,如果 是以 "on_" 打头就进入内层 for 循环:
①内层 for 循环对列表 list 里的所有对象依次处理,取出每个对象名称 coName,与槽函数名 slot 中间的 <object name> 比较,跳过所有不匹配的对象。
②如果找到了匹配的发送端对象名 coName,根据槽函数名 slot 里面的 <signal name> 查找发送端信号的绝对序号 sigIndex。
③如果 sigIndex < 0 ,说明上面精确匹配没找到,那么进行粗匹配,查找信号名正确、参数能与槽函数大致匹配上的信号(信号参数可以比槽函数多)。
④再判断是否 sigIndex < 0,还是负数那就真的没有匹配的信号,那么进入对象列表的下一个对象查找。
⑤如果找到了匹配的信号序号 sigIndex ,那么调用 QMetaObjectPrivate::connect 实现关联,如果关联成功,那么就不用查找信号了。当前的槽函数已经有源头对象和信号了,跳出内层 for 循环。
回到外层 for 循环时:
如果找到了匹配的源头对象和信号(foundIt 为真),如果下一个元方法(槽函数)是克隆的,就不做信号源查找,跳过克隆的元方法。
如果找不到,而当前元方法又不是克隆的元方法,那么打印警告信息,没找到。
外层 for 循环结束,这个函数代码就结束了。
关于克隆元方法,以信号 QObject::destroyed 为例,它有默认参数:
void QObject::destroyed(QObject * obj = 0)
|
对于信号和槽关联时,这个信号可以写成两种形式:"destroyed(QObject *)" 和 "destroyed()",这两种字符串是不一样的,一个带参数类型,另一个不带,因此需要引入克隆元方法,表述省略默认参数的信号调用。一般匹配函数名称时 要考虑克隆函数,而做关联时只需要本体函数的序号即可。
自动关联用的 QMetaObjectPrivate::connect 函数与上面小节手动关联函数内部调用的 QMetaObjectPrivate::connect 是一个函数,只是自动关联的参数少一些,二者的原理是类似的。
关联函数通常将源头信号和接收端槽函数关联一次就够了,关联之后会将新的连接加入到对应源头信号的连接列表里面。旧式语法的 QObject::connect 函数和自动关联的 QMetaObject::connectSlotsByName 函数,其关联过程是用到字符串匹配的,而且还比较多,所以关联函数本身效率不是很高,好在关联函数通常是一次性的,程序启动时关联函数执行完之后,基本就不再调用 关联函数,不调用自然没啥影响。
一旦连接列表成功建立,从信号触发到槽函数被调用,这个过程效率如何呢?我们来看看最后的小节,从信号到槽的过程。
7. MOS原理之七:从信号到槽
根据 moc_*.cpp 文件内信号函数到隐藏的四个函数介绍,我们知道源头是在信号函数里调用 QMetaObject::activate(),这是信号源头的工作。在接收端,私有静态函数 qt_static_metacall() 会调用元方法,这样接收端的槽函数就会被执行。
源头和接收端缺少一个桥,上面介绍的 connect 函数主要功能是新建连接,添加到对应信号的连接列表里,它的作用就是搭建从源头到接收端的桥。桥搭建好了之后,我们知道古代有赵州桥千年不倒,现代也有未竣工就自己坍塌的 豆腐渣桥,Qt 信号到槽的桥,运行起来怎么样,是赵州桥还是豆腐渣桥,我们走了才知道。所以最后的小节介绍从信号到槽函数的执行通路,看看 Qt 的桥怎么走。
关键的源代码还是在文件
C:\Qt\Qt5.4.0\5.4\Src\qtbase\src\corelib\kernel\qobject.cpp
里面,先看看信号函数里的代码,以 countChanged 信号为例:
// SIGNAL 1 void Widget::countChanged(int _t1) { void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 1, _a); }
|
QMetaObject::activate() 函数有四个参数,第一个是源头对象 this 指针,第二个是源头的静态元对象,第三个是信号函数相对序号 1,第四个是信号函数参数指针数组。
QMetaObject::activate() 函数有多个重载函数,现在调用的这下面这个函数:
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index, void **argv) { activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv); }
|
这个 QMetaObject::activate() 内部就一句,根据源头的元对象计算信号的绝对偏移 QMetaObjectPrivate::signalOffset(m),然后还是以四个参数,调用另一个重载的
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
|
这第二个 QMetaObject::activate() 代码位于 qobject.cpp 里面从 3588 行到 3754 ,代码超过一百行的函数都是比较复杂的,不方便直接贴代码讲解,所以本人倾向于直接写该函数的大概执行流程,感兴趣的读者可以去看它的源码。这个函数里有部分多线程 Qt::QueuedConnection 和 Qt::BlockingQueuedConnection 关联的处理代码,本节例子都没用上,所以基本略过多线程部分代码。
(1)计算信号的绝对序号 signal_index = signalOffset + local_signal_index。
(2)如果没有东西关联到 signal_index 序号的信号,那么返回。
qt_signal_spy_callback_set 是用于 Qt Test 单元测试模块的回调函数合集,成员 signal_begin_callback 记录信号调用过程的开始,
成员 signal_end_callback 记录信号调用过程的结束。凡是 qt_signal_spy_callback_set 相关代码都可以忽略,与正常的功能代码无关。
(3)如果阻塞式关联标志 sender->d_func()->blockSig 为真,那么不调用槽函数,直接返回,与本例子无关,也忽略。
(4)sender->d_func()->declarativeData 是 QML 程序信号触发的相关代码,与本例子无关,忽略。
(5)qt_signal_spy_callback_set.signal_begin_callback 函数指针不为空,执行该回调函数,与本例无关,忽略。
(6)获取当前线程编号 currentThreadId,多线程程序可用这个判断源头和接收端是否在同一线程。
(7)接下来有一对大括号封装的代码,一直到函数结尾的 qt_signal_spy_callback_set.signal_end_callback 前面,全是单元测试需要记录的执行流程,单元测试会记录这中间过程花费时间。下面看大括号里面内容。
(8)用互斥锁 locker 锁定源头的信号和槽数据,访问信号和槽数据一般都要用互斥锁锁定,以免其他线程的干扰。
(9)声明了结构体 ConnectionListsRef,这个结构体主要是构造函数里对源端连接总表 connectionLists 的正在使用次数 inUse 记录加 1,析构函数把 inUse 减 1,其他就是拿 ConnectionListsRef 当作源端连接总表 connectionLists 的引用就行了。
(10)定义了函数里的临时变量 ConnectionListsRef connectionLists = sender->d_func()->connectionLists;
拿临时变量 connectionLists 当作源端的连接总表来用就行了。
(11)如果连接总表是空的,啥都不干返回。如果连接总表不空,继续后面代码。
(12)定义连接列表 const QObjectPrivate::ConnectionList *list;
这个 list 有两种赋值方式:
第一种,信号绝对序号 signal_index 在连接总表的计数范围之内,将单个信号 signal_index 对应的连接链表赋值给 list;
第二种,signal_index 超出连接总表的计数范围,那么把 &connectionLists->allsignals 赋给 list。
注意 allsignals 是一个非常逗逼的称呼,我搜遍了核心项目 corelib.pro 项目代码,发现这是个空链表,里面全是 NULL。
所以如果 signal_index 超出连接总表的计数范围,后面代码根据空链表执行了一遍,这并没什么卵用。
(13)外层 do-while 循环作用,分两类:
第一类:signal_index 超出范围,循环根据空链表跑一遍,结束,除了单元测试用于对比验证,应该没有用。
第二类,正常的 signal_index ,得到的对应信号的连接链表 list,进入内层 do-while 循环,执行链表里面的所有接收端槽函数;
然后外层循环强行把 &connectionLists->allsignals 赋给 list,再根据空链表跑一遍。
也就是说,只有第二类情况才会把外层 do-while 循环跑两轮。跑两轮的用途就是减去第一类的空循环时间,计算出差值,就是信号调用执行的时间。
这个外层的 do-while 循环是为了单元测试增加的,对信号和槽机制的功能没什么卵用。
下面我们直接根据 signal_index 是正常信号的绝对序号的情况,把外层 do-while 循环里包裹的代码走一遍。
① QObjectPrivate::Connection *c = list->first;
获取链表打头的第一个连接 c,如果 c 是空的,进入下轮外层 do-while 循环。否则继续下面代码。
② QObjectPrivate::Connection *last = list->last;
获取当前链表末尾的节点。因为多线程程序中其他线程可能在我们这次信号到槽的调用过程中,同时添加新的连接,那样会对现在的循环有干扰。所以要把现在的链表末 尾节点提取出来保存,我们不调用其他线程这时候新加的槽函数关联。
③ 进入内层 do-while 循环,内层 do-while 循环是遍历从 list->first 到 last 的所有连接节点,与该信号关联的所有接收端槽函数(元方法)都会被调用:
●如果当前连接 c->receiver 接收端为空,继续下轮,取出链表下一个连接来处理。如果有接收端继续下面代码。
●receiver = c->receiver,用 receiver 保存接收端指针。
●bool 变量 receiverInSameThread,接收端与发送源在同一线程就为真,否则为假。
●如果是入队关联 Qt::QueuedConnection,调用 queued_activate,然后 continue 进入下轮循环;
如果是阻塞关联 Qt::BlockingQueuedConnection,使用 semaphore 处理,具体代码这里忽略,与本例无关,
阻塞关联代码处理后也是 continue 进入下轮循环。
●定义切换对象 QConnectionSenderSwitcher sw;
如果源头和接收端在同一线程,那么调用 sw.switchSender() 函数,因为接收端会记录当前是谁给自己发的信号,
sw.switchSender() 函数主要功能就是设置接收端 receiver 内部数据,记录源头指针 sender。
●callFunction = c->callFunction,保存当前连接里记录的 callFunction ,这个函数指针就是我们之前说的接收端私有静态函数 qt_static_metacall()。旧式关联语法和自动关联都用这个私有静态函数调用元方法,而新式语法是用 SlotObj 。下面会区分。
●method_relative = c->method_relative; 保存元方法相对序号。
●下面是关键的三类 if - else if - else 判断:
if (c->isSlotObject) :
第一类,新式语法调用接收端函数,取出槽对象 c->slotObj 指针保存为 obj,
然后使用槽对象 obj->call() 函数调用接收端的函数。
else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) :
第二类,旧式语法接收端元方法调用,callFunction 是存在的,
判断连接里保存的元方法偏移小于等于当前接收端元对象的元方法偏移,保证这段里面的代码不是在接收端析构过程中被调用。
也就是说元方法相对序号 method_relative 是正确的,那么执行
callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);
其实就是直接调用接收端私有函数 qt_static_metacall() ,这个私有静态函数根据相对序号执行元方法。
else:
第三类,也是旧式语法的,连接里保存的 c->method_offset 大于 接收端当前的元方法偏移,说明接收端基类元方法变少了。
意味着接收端在进行析构,元方法才会变少,这时候不能使用相对序号直接调用接收端私有静态函数,按照绝对序号:
method = method_relative + c->method_offset; 计算元方法以前的绝对序号 method ,
根据绝对序号调用 QMetaObject::metacall() 函数,
然后在 QMetaObject::metacall() 函数内部会调用 接收端的虚函数 qt_metacall()。
●connectionLists->orphaned 如果为真,说明源头对象在进行析构,连接总表变成了孤儿,就跳出内层循环。
如果不是孤儿列表,就处理下一个连接。
讲完内层循环,接收端的函数就被调用了,从信号到槽的过程也就走通了。 最后额外解释一下 locker.unlock() 和 locker.relock(),因为信号有可能关联到信号,会有连环触发,所以调用接收端元方法之前要解锁,调用 locker.unlock() ,这样信号连环触发才不会被锁死。等待接收端元方法执行完毕,回到这个 QMetaObject::activate(),然后再进行重新锁定 locker.relock(),继续下轮循环的处理。
实际中从信号到槽函数的过程,比之前想象的要更为简单,程序正常运行时,从重载的复杂 QMetaObject::activate() 里面直接调用接收端私有静态函数 qt_static_metacall() 。其实正常情况下不需要调用虚函数 qt_metacall(),而是直接调用的私有静态函数 qt_static_metacall()。
我们以 countChanged 信号,按照从信号到槽的过程画个图:
程序正常执行过程中,从信号触发到槽函数被调用,这个调用链是比较短的,算头算尾才五个,所以从信号到槽函数的执行过程是比较快的,而且调用过程中是基于序号的, 根本没有用复杂的字符串匹配,所以不用担心运行效率的事,可以放心用信号和槽机制。
只有特殊情况,比如接收端在进行析构操作,上面的调用链会增加 QMetaObject::metacall() 和 接收端虚函数 qt_metacall() 两个节点,qt_metacall() 会调用基类的虚函数,重新计算元方法相对序号,然后调用私有静态函数 qt_static_metacall()。析构函数通常意味着程序都快走到结束了,所以也不用担心虚函数 qt_metacall() 的执行效率。
上面主要介绍旧式语法的执行过程,新式语法是通过槽对象 SlotObj 内部保存的函数指针来调用接收端的函数,执行效率是差不多的,而且由于使用了槽对象 SlotObj ,里面可以保存各种奇奇怪怪的函数指针,用法更为灵活。讲完信号和槽机制相关源代码,本章内容也就完整了,后面章节开始讲解真正的图形界面编程。
|