Qt元对象系统(Meta-Object System)

        作为信号与槽机制的另一个得力干将,元对象系统不仅能支持信号与槽机制,还支持实时类型信息(RTTI)、动态属性系统等功能。元对象系统使我们可以在不知道对象类型的情况下与对象交互,表面意思很好理解,但是咱也不知道它有啥用啊?对吧?但是别急宝子们,且听我娓娓道来。

        首先,像我这样博爱的人,是一定会照顾到干货神教的同学们的,所有干货我都会加粗,其次,就是跳读者们(包括我)最爱的目录:

目录

玫瑰、神器和烛光晚餐

【QObject 是我爹】

自恋也疯狂(MOC)

我的24K钛合金红线去哪了?

丘比特?

丘比特套娃

女神通讯率


玫瑰、神器和烛光晚餐

特性描述
信号和槽支持对象之间的通信,并且自动跳过信号与槽对不上的连接
反射在运行时查询对象的信息,属性动画和脚本绑定就是通过这个实现的
动态属性在运行时添加新的属性,并储存在一个哈希表中

        要想知道它有啥用,我们就要彻底的、全面的、清晰的了解它,但凡它身着寸缕都是我们对它的不尊重!

        想要和一个人深入交流,首先要了解的就是TA的性格,同样的,想要和一个系统聊人生哲学,我们首先要知道它的脾性,想要使用元对象系统的功能那自然也是有条件的:

  • 类继承自QObject
  • 声明Q_OBJECT宏
  • 元对象编译器(MOC)

以上三个条件缺一不可,少一个它都不和你见面,就像玫瑰、神器和烛光晚餐一样重要。

【QObject 是我爹】

澄清一下,QObject 不是我爹,它是我们程序类的爹,请看如下代码:

class QtConnect : public QMainWindow
{
	Q_OBJECT

public:
	QtConnect(QWidget *parent = 0);
	~QtConnect() = default;

private:
	Ui::QtConnect* ui;
};

虽然这里看起来继承的是QMainWindow,但是QMainWindow是继承自QWidget,而QWidget则是继承的QObject,也就是说 QtConnect 是 QObject 它曾孙,不信你们看:

当然,我们也可以写一个类直接继承QObject,杜绝中间商赚差价,直接做 QObject 的儿子:


#include <QObject>

class StartPage : public QObject {
	Q_OBJECT

public:
	StartPage();
	~StartPage();

private:

};

这样的话我们就创建了一个既不是窗口,也不是控件的类,悄咪咪就继承了 QObject 的家产!

        而 Q_OBJECT 的用法和作用就很简单了,就像往脑门上贴一张纸,纸上写的【QObject 是我爹】,然后元对象系统就会为你服务,帮你干脏活累活。唯一要注意的就是放在类定义中就好,乱放的后果是很严重的!轻则元对象系统不鸟你,严重的话你的程序就要在二次元重生了。

        言归正传(来点干货,主打一个博爱),这里引用Qt官方文档中的一段话:

The Q_OBJECT macro is used to enable meta-object features, such as dynamic properties, signals, and slots.

You can add the Q_OBJECT macro to any section of a class definition that declares its own signals and slots or that uses other services provided by Qt's meta-object system.

意思是:Q_OBJECT 宏用于启用元对象功能,例如动态属性、信号和插槽。您可以将Q_OBJECT 宏添加到类定义的任何部分,该部分声明了自己的信号和插槽,或者使用 Qt 的元对象系统提供的其他服务。

        这里我们补充一点,它还有一个功能就是确保类与元对象编译器(MOC)同时工作,MOC会生成与类相关的元对象代码,信号和槽的连接等功能。

        再来看看 Q_OBJECT 宏不着寸缕的样子:

#define Q_OBJECT \
public: \
   QT_WARNING_PUSH \
   Q_OBJECT_NO_OVERRIDE_WARNING \
   static const QMetaObject staticMetaObject; \
   virtual const QMetaObject *metaObject() const; \
   virtual void *qt_metacast(const char *); \
   virtual int qt_metacall(QMetaObject::Call, int, void **); \
   QT_TR_FUNCTIONS \
private: \
   Q_OBJECT_NO_ATTRIBUTES_WARNING \
   Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *,QMetaObject::Call, int, void **); \
   QT_WARNING_POP \
   
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject,"")

可以看到【QObject 是我爹】这样一句话中竟然隐含了这么多的信息,一个静态 QMetaObject 变量,三个虚函数,一个QPrivateSignal结构体,一个静态 qt_static_metacall 函数,还有其余六个宏定义。这些定义我们很快就会用到,六个宏我们在其他文章中再讲,先来看看MOC是如何舔,哦不,如何配合工作的。

自恋也疯狂(MOC)

        MOC呢是一个比较自恋的家伙,所有他生成的元对象代码都会被打上moc的标签。耳听为虚,眼见为实,虽然我不会骗大家,但是以在下的文笔,难以写出它自恋的程度,不告诉大家的话,在下寝食难安啊!

        这样,我们先写一个 QtConnect 类,继承自 QObject ,文件名是:mainwindow.h/cpp,头文件内容如下(请同学们使用史上最强开源工具:CtrlCV):

#ifndef MAINWINDOW_H_LW
#define MAINWINDOW_H_LW

#include <QMainWindow>
#include "ui_mainwindow.h"

namespace Ui {
	class QtConnect;
}

class QtConnect : public QMainWindow
{
	Q_OBJECT

public:
	QtConnect(QWidget *parent = 0);
	~QtConnect() = default;

signals:
	void signal1();
	void signal2();
	void signal3(int);

private:

private slots:
	void slots1();
	void slots2();
	void slots3(int);

private:
	Ui::QtConnect* ui;
};

#endif // MAINWINDOW_H_LW

CPP内容如下:

#include "mainwindow.h"
#include <qdebug.h>

QtConnect::QtConnect(QWidget* parent) :
	QMainWindow(parent), ui(new Ui::QtConnect)
{
	ui->setupUi(this);

	connect(this, &QtConnect::signal1, this, &QtConnect::slots1);
	connect(this, &QtConnect::signal2, this, &QtConnect::slots1);

	connect(this, &QtConnect::signal1, this, &QtConnect::slots2);
	connect(this, &QtConnect::signal2, this, &QtConnect::slots2);

	connect(this, &QtConnect::signal3, this, &QtConnect::slots3);

	emit signal1();
	emit signal2();
	emit signal3(128);
}


void QtConnect::slots1() {
	qInfo() << "slots1 once!";
}

void QtConnect::slots2() {
	qInfo() << "slots2 once!";
}

void QtConnect::slots3(int val) {
	qInfo() << "slots3 once: " << val;
}

我们点击生成,然后进到build目录中,直接搜索moc,就能看到类似如下的画面:

moc_mainwindow.cpp,就是MOC为我们mainwindow这个类文件所生成的元对象代码文件,它是在真正编译代码之前完成的,可以看做一种预处理过程,正如Bjarne Stroustrup在《The C++ Programming Language》中所说:“预处理是C++编译过程中的一个重要步骤,它为编译器提供了所需的所有信息。”

我的24K钛合金红线去哪了?

        C++之父说的对,但是这话不应该是我说出来,太文学了,我不是这块料。

        前文说到,在信号和槽机制的背后,有一老六红娘悄悄为它们牵线,那是怎么牵的呢?我们打开moc_mainwindow.cpp可以在最底下看到如下代码:

// SIGNAL 0
void QtConnect::signal1()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

// SIGNAL 1
void QtConnect::signal2()
{
    QMetaObject::activate(this, &staticMetaObject, 1, nullptr);
}

// SIGNAL 2
void QtConnect::signal3(int _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 2, _a);
}

MOC帮我们把信号函数给实现了!不得不说,它虽然自恋,但是它确实厉害啊!这些函数都有一个共同点,都调用了QMetaObject::activate,QMetaObject::activate的内部实现机制又是如何呢?请再听我娓娓道来,我本想直接放源码,但是在下深知没有人喜欢又长又枯燥的源码(包括我),所以,想看源码的各位神,请移步:QMetaObject::activate(源码C++)其他同学请跟随我的脚步,我们按顺序阅读理解 QMetaObject::activate 源码。

丘比特?

首先在 QMetaObject::activate 函数中有这样一句代码:

if (!sender->d_func()->isSignalConnected(signal_index))
    return; 

if (sender->d_func()->blockSig)
    return;

第一个 if 的作用是检查当前信号是否有与之连接的槽函数,诶~匹配合适对象嘛!像不像丘比特?

第二个if的作用是检查这个信号是不是阻塞的,这个我们是可以通过 blockSignals(bool) 这个函数设置的,也就是说,信号想找对象,但是咱不让它找!憋死它!

我们继续:

QMutexLocker locker(signalSlotLock(sender));

QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;

QMutexLocker locker(signalSlotLock(sender));就是给这个信号上锁,这里涉及到一个线程安全的问题,QMutexLocker 目的是保护一次只有一个线程访问一个对象、数据结构或一段代码。

第二句代码是获取当前信号的ConnectionList链表容器,大白话就是槽函数容器的容器,诶~俄罗斯套娃嘛!

继续往下看:

 const QObjectPrivate::ConnectionList *list;
    if (signal_index < connectionLists->count())
        list = &connectionLists->at(signal_index);
    else
        list = &connectionLists->allsignals;

到这里才是真正的获取到了槽函数列表。

接下来是一个巨大的 do while 循环,而且是 do while 套娃 do while,这一部分内容是调用槽函数的关键(牵红线),一会儿我们着重讲一下,先来看省略第二层 do while 之后的代码:

    do {
        QObjectPrivate::Connection *c = list->first;
        if (!c) continue;
        // We need to check against last here to ensure that signals added
        // during the signal emission are not emitted in this emission.
        QObjectPrivate::Connection *last = list->last;

        do
        {
            //此处省略上千代码。。。
        } while (c != last && (c = c->nextConnectionList) != 0);

        if (connectionLists->orphaned)
            break;
    } while (list != &connectionLists->allsignals &&
             //start over for all signals;
             ((list = &connectionLists->allsignals), true));

    --connectionLists->inUse;
    Q_ASSERT(connectionLists->inUse >= 0);
    if (connectionLists->orphaned)
    {
        if (!connectionLists->inUse)
            delete connectionLists;
    }
    else if (connectionLists->dirty)
    {
        sender->d_func()->cleanConnectionLists();
    }

    locker.unlock();

    if (qt_signal_spy_callback_set.signal_end_callback != 0)
        qt_signal_spy_callback_set.signal_end_callback(sender, signal_absolute_index);

可以到看,其实第一个 do while 的内容并不多,代码主要集中在第二个 do while 循环中,在 do while 之外就是一些连接链表、解锁之类的操作,到这里,一个 QMetaObject::activate 函数就阅读完了,是不是很快!这不得给自己鼓个掌?今天就是天王老子来了,这掌也得鼓!

        OKOK,适当鼓掌有益,过度鼓掌伤身啊!咱们还没看完呢!别开香槟啊!

        先看第一个 do while 循环,这里声明了两个 QObjectPrivate::Connection 变量,它把列表的第一个连接赋值给了c,最后一个连接赋值给了last。这里有一句注释是这样写的:

// We need to check against last here to ensure that signals added during the signal emission are not emitted in this emission.

意思是:我们需要在这里检查最后一个,以确保在信号发射期间添加的信号不会在这次发射中发射。那么问题就来了,这里添加的信号是什么?连接指的是啥呢? QObjectPrivate::Connection 是个什么玩意儿?这仨其实是一个东西,就是 Connection  结构体,我们扒光它看看:

struct Connection
{
    QObject *sender;//发送者
    QObject *receiver;//接受者
    StaticMetaCallFunction callFunction;//调用的槽函数
    // The next pointer for the singly-linked ConnectionList
    Connection *nextConnectionList;
    //senders linked list
    Connection *next;
    Connection **prev;
    QBasicAtomicPointer<int> argumentTypes;
    ushort method_offset;
    ushort method_relative;
    ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
    ~Connection();
    int method() const { return method_offset + method_relative; }
};

这么一看,是不是就好理解了!这个 Connection 在第二层循环中有大用,我们一会儿再讲。第一层 do while 中有一句判断:

list != &connectionLists->allsignals && ((list = &connectionLists->allsignals), true)

它的作用的时间是否已经处理了所有的连接链表,如果没有,则继续处理其他列表,其中的 true 让 do while 始终保持执行。

丘比特套娃

很显然,我们还没有看到丘比特的真身。但是!凭我多年单身的经验,掐指一算,就知道距离首胜只有一步之遥了!我们来看第二个 do while 循环,它的第一句代码:

if (!c->receiver) continue;

QObject * const receiver = c->receiver;
const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId;

如果接受者为空则跳过,否则获取它的接受者和其线程ID。简直不要太简单好吗?这是在侮辱兄弟们的智商!继续!

// determine if this connection should be sent immediately or
// put into the event queue
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                    || (c->connectionType == Qt::QueuedConnection))
{
    queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);
                continue;
    #ifndef QT_NO_THREAD
}

这里有一句注释:

// determine if this connection should be sent immediately or put into the event queue

 意思是:确定是否应立即发送此连接或将其放入事件队列。Qt的信号和槽机制是支持跨线程通讯的,如果在同一个线程内,就像回调一样调用,如果在不同线程,那么为了线程安全考虑就会把此链接放入对方线程的事件列队中,等待对方读取。

是否跨线程是由 connect 函数中第五个参数决定的,其默认是 Qt::AutoConnection。

Qt::AutoConnection 的意义是:如果在同一线程内则自动使用 Qt::DirectConnection 否则使用 Qt::QueuedConnection。但是为了方便代码的维护和复用,如果要跨线程操作,尽量把第五个参数改为:Qt::QueuedConnection。

在这里,我们看到的这个 if 其实就是跨线程操作。

继续:

//阻塞队列连接类型
else if (c->connectionType == Qt::BlockingQueuedConnection)
{
	locker.unlock();
	if (receiverInSameThread)
	{
		qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
			"Sender is %s(%p), receiver is %s(%p)",
			sender->metaObject()->className(), sender,
			receiver->metaObject()->className(), receiver);
	}
	QSemaphore semaphore;
	QCoreApplication::postEvent(receiver, new QMetaCallEvent(c->method_offset, c->method_relative,
		c->callFunction,
		sender, signal_absolute_index,
		0, 0,
		argv ? argv : empty_argv,
		&semaphore));
	semaphore.acquire();
	locker.relock();
	continue;
#endif
}

我们看到,这里判断的是 Qt::BlockingQueuedConnection它和 Qt::QueuedConnection 的作用是一致的,唯一的区别是发送完信号后,发送者所在线程会阻塞,直到槽函数运行完。通常在多线程间需要同步的场合会用到这个。

需要注意的是,接收者和发送者绝对不能在一个线程,否则会死锁。

那么,处理完跨线程的事件,接下来该处理线程内的了吧?德国骨科也是很重要的!咱继续:

QObjectPrivate::Sender currentSender;
QObjectPrivate::Sender *previousSender = 0;
if (receiverInSameThread)
{
	currentSender.sender = sender;
	currentSender.signal = signal_absolute_index;
	currentSender.ref = 1;
	previousSender = QObjectPrivate::setCurrentSender(receiver, ¤tSender);
}
//获取连接的回调函数指针
const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
const int method_relative = c->method_relative;

果不其然!英雄所见略同啊!Qt也很重视德国骨科!这里都是一些赋值操作,不过多解释。

拷贝着拷贝着就发现只有最后一段代码了呀宝子们!

//如果回调有效且连接的方法的偏移小于接收者的元对象的方法的偏移
if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset())
{
	//we compare the vtable to make sure we are not in the destructor of the object.
	locker.unlock();
	if (qt_signal_spy_callback_set.slot_begin_callback != 0)
		qt_signal_spy_callback_set.slot_begin_callback(receiver, c->method(), argv ? argv : empty_argv);
	//根据接收者的方法偏移,接收者等参数调用qt_static_metacall回调函数
	callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);

	if (qt_signal_spy_callback_set.slot_end_callback != 0)
		qt_signal_spy_callback_set.slot_end_callback(receiver, c->method());
	locker.relock();
}
else
{
	const int method = method_relative + c->method_offset;
	locker.unlock();

	if (qt_signal_spy_callback_set.slot_begin_callback != 0)
	{
		qt_signal_spy_callback_set.slot_begin_callback(receiver,
			method,
			argv ? argv : empty_argv);
	}
	//根据接收者、接收者的方法索引等参数调用发送元对象的metacall
	metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);

	if (qt_signal_spy_callback_set.slot_end_callback != 0)
		qt_signal_spy_callback_set.slot_end_callback(receiver, method);

	locker.relock();
}

看上面的注释:“如果回调有效且连接的方法的偏移小于接收者的元对象的方法的偏移”,乍一看是不是一脸懵13???别说,你还真别说,笔者一开始看到也是如此。其实真正的意思很简单,就是决定调用 qt_static_metacall 还是 metacall。

诶~,是不时还是懵?没关系,其实 qt_static_metacall 特别简单,还记的我们之前说的moc_mainwindow.cpp,不记得也没关系,请 Ctrl + F 搜索。

打开 moc_mainwindow.cpp 这个文件,直接搜索 qt_static_metacall,然后我们就会看到:

void QtConnect::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<QtConnect *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->signal1(); break;
        case 1: _t->signal2(); break;
        case 2: _t->signal3((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 3: _t->slots1(); break;
        case 4: _t->slots2(); break;
        case 5: _t->slots3((*reinterpret_cast< int(*)>(_a[1]))); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (QtConnect::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&QtConnect::signal1)) {
                *result = 0;
                return;
            }
        }
        {
            using _t = void (QtConnect::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&QtConnect::signal2)) {
                *result = 1;
                return;
            }
        }
        {
            using _t = void (QtConnect::*)(int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&QtConnect::signal3)) {
                *result = 2;
                return;
            }
        }
    }
}

是不是很简单,其实就是通过不同的id调用不同的函数而已。

 metacall 其实就是对应文件中的 qt_metacall 这个函数,直接搜索就能看到:

int QtConnect::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QMainWindow::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 6)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 6;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 6)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 6;
    }
    return _id;
}

本质上还是调用的 qt_static_metacall,哈哈哈哈哈。

到此为止,我们算是看到丘比特的庐山真面目了。

女神通讯率

        我们之前说元对象系统还支持获取实时类型信息(RTTI),这里需要注意的是:Qt没有采用C++的RTTI机制,而是直接提供了更为强大的元对象,即使编译器不支持RTTI,也能动态获取类型信息。说白了就是可以在在运行时获取对象类型信息,如类名、基类、方法列表等。

通常,我们并不需要用到这个,但在编写元应用(如脚本引擎、对象I/O、dynamics_cast、GUI 构建器等)时,它非常有用。详细可以参考:QMetaObject。

属性系统(Property System)

在 Qt 中,属性系统是一种机制,允许开发者在对象中定义属性,这些属性可以在运行时被查询和修改。属性系统不仅仅是一个数据存储,它还提供了数据的封装、验证和通知机制。

与传统的静态属性不同,Qt 的属性系统支持动态特性,如运行时类型信息和动态属性的添加、查询和修改。可以理解为动态的静态属性。

本文在Qt元对象系统的讲解中,着重了MOC、信号与槽机制,因为文章篇幅的问题,对于获取实时类信息和属性系统并没有过于详细的解释,这两个部分将在后续详细描述,并在本文末尾贴上连接。

OK,江湖再相见。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值