QT核心-元对象的其他妙用

原创
2022/04/02 11:34
阅读数 285

首先,这个功能不是我原创的,它是发表于QT内部的一个大佬,原文链接:Dynamic Signals and Slots。他的这篇文章指出,在常规的开发中,信号和槽都是在编译期指定的,没办法在运行时增删。注意:这里的运行时增删不是指connect连接或disconnect取消连接信号槽,而是增加一个函数,把这个函数当作信号或者槽来使用。
原文的代码看着我实在头疼,感觉跟写着玩儿似的,而且到现在为止我也没有用到动态增删信号槽的功能,所以这里不解析原文的代码了,而是介绍一下我看了原文代码之后对它所使用功能的理解和感悟所实现的其他功能。

正文

注意:这里指的是用SIGNAL和SLOT宏包裹的字符串形式连接的信号槽,而不是直接取函数地址的形式。前者依赖的元对象,后者依赖的C++模板元。

信号槽实现原理

我们首先要搞清楚发送一个信号后是如何调用到槽函数的,我前面的文章也指出了,信号槽作为一个高级功能它依赖了三个核心功能: 生存线程、事件循环和元对象(取函数地址的形式依赖的C++模板元)。生存线程和事件循环是用到当connect函数的第五个参数为Qt::QueuedConnection和Qt::BlockingQueuedConnection的时候的(为Qt::AutoConnection的时候也可能用到,参考我前面的文章),而元对象就是实现通过字符串调用到槽函数的功能。
我们可以创建一个Test.h文件,定义一个Test类继承至QObject并添加Q_OBJECT宏,然后添加一个信号和一个槽函数并编译:

#ifndef TEST_H
#define TEST_H

#include <QObject>

class Test : public QObject
{
    Q_OBJECT
public:
    explicit Test(QObject *parent = nullptr);

signals:
    void testSignal();

public slots:
    void testSlot() {}
};

#endif // TEST_H

我们首先点开Q_OBJECT宏,可以找到里面的一个函数virtual int qt_metacall(QMetaObject::Call, int, void **);

#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, "")

这个函数就是通过元对象操作本类(Test)的实例对象的入口,比如QObject::setProperty/QObject::property的写入-读取数据,或者连接信号槽之后发送信号调用到槽函数(当然还有一些QMeta*的类能做更多的操作)。这个函数我们不会去实现,它由QT的moc编译器检测到Test.h中使用了Q_OBJECT宏之后自动帮我们实现的。比如上面的文件编译后我们就能在build文件夹里面找到一个名为moc_Test.cpp文件,里面就有这个函数的实现。而且这个函数还是一个虚函数,moc_Test.cpp里面的实现其实就是重写了父类的同名函数。打开这个文件就能找到这个函数的实现:

int Test::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) {
        if (_id < 2)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 2;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 2)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 2;
    }
    return _id;
}
  1. 这个函数的第一行是调用父类的同名函数,这个步骤是必须的,因为这个函数里面判断id是从0开始,而传入这个函数的id加上了所有父类的元函数数量,所有的元函数数量可以通过QMetaObject::methodCount函数获取,而当前类的起始id可以通过QMetaObject::methodOffset函数获取。这一步就是减去父类的元函数,可以类比于这个类里面的_id -= 2;
  2. 接下来调用槽函数时_c就等于QMetaObject::InvokeMetaMethod,所以接下来就是进入到qt_static_metacall函数。其实通过元对象调用被Q_INVOKABLE标记的函数和读写属性都是进入这个静态函数。
void Test::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<Test *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->testSignal(); break;
        case 1: _t->testSlot(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (Test::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Test::testSignal)) {
                *result = 0;
                return;
            }
        }
    }
    Q_UNUSED(_a);
}
  1. 这个函数里就只是一个switch-case语句,每一个元函数都有一个唯一id标识,这个id在moc编译时确定。比如这里调用槽函数的id就等于1(注意这个id不是真的等于1,而是上面说的减去了父类元函数数量之后的值,由QMetaMethod::methodIndex指定)。
  2. 现在我们知道了槽函数是通过id来调用的,但是这个id是怎么得来的呢?我们进入QObject::connect函数的内部去查看他的源码(注意我所使用的版本为5.15.2,可能每个版本不太一样,但大同小异)。
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
                                     const QObject *receiver, const char *method,
                                     Qt::ConnectionType type)
{
    if (sender == nullptr || receiver == nullptr || signal == nullptr || method == nullptr) {
        qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
                 sender ? sender->metaObject()->className() : "(nullptr)",
                 (signal && *signal) ? signal+1 : "(nullptr)",
                 receiver ? receiver->metaObject()->className() : "(nullptr)",
                 (method && *method) ? method+1 : "(nullptr)");
        return QMetaObject::Connection(nullptr);
    }
    QByteArray tmp_signal_name;

    if (!check_signal_macro(sender, signal, "connect", "bind"))
        return QMetaObject::Connection(nullptr);
    const QMetaObject *smeta = sender->metaObject();
    const char *signal_arg = signal;
    ++signal; //skip code
    QArgumentTypeArray signalTypes;
    Q_ASSERT(QMetaObjectPrivate::get(smeta)->revision >= 7);
    QByteArray signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes);
    int signal_index = QMetaObjectPrivate::indexOfSignalRelative(
            &smeta, signalName, signalTypes.size(), signalTypes.constData());
    if (signal_index < 0) {
        // check for normalized signatures
        tmp_signal_name = QMetaObject::normalizedSignature(signal - 1);
        signal = tmp_signal_name.constData() + 1;

        signalTypes.clear();
        signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes);
        smeta = sender->metaObject();
        signal_index = QMetaObjectPrivate::indexOfSignalRelative(
                &smeta, signalName, signalTypes.size(), signalTypes.constData());
    }
    if (signal_index < 0) {
        err_method_notfound(sender, signal_arg, "connect");
        err_info_about_objects("connect", sender, receiver);
        return QMetaObject::Connection(nullptr);
    }
    signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
    signal_index += QMetaObjectPrivate::signalOffset(smeta);

    QByteArray tmp_method_name;
    int membcode = extract_code(method);

    if (!check_method_code(membcode, receiver, method, "connect"))
        return QMetaObject::Connection(nullptr);
    const char *method_arg = method;
    ++method; // skip code

    QArgumentTypeArray methodTypes;
    QByteArray methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes);
    const QMetaObject *rmeta = receiver->metaObject();
    int method_index_relative = -1;
    Q_ASSERT(QMetaObjectPrivate::get(rmeta)->revision >= 7);
    switch (membcode) {
    case QSLOT_CODE:
        method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
                &rmeta, methodName, methodTypes.size(), methodTypes.constData());
        break;
    case QSIGNAL_CODE:
        method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
                &rmeta, methodName, methodTypes.size(), methodTypes.constData());
        break;
    }
    if (method_index_relative < 0) {
        // check for normalized methods
        tmp_method_name = QMetaObject::normalizedSignature(method);
        method = tmp_method_name.constData();

        methodTypes.clear();
        methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes);
        // rmeta may have been modified above
        rmeta = receiver->metaObject();
        switch (membcode) {
        case QSLOT_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
                    &rmeta, methodName, methodTypes.size(), methodTypes.constData());
            break;
        case QSIGNAL_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
                    &rmeta, methodName, methodTypes.size(), methodTypes.constData());
            break;
        }
    }

    if (method_index_relative < 0) {
        err_method_notfound(receiver, method_arg, "connect");
        err_info_about_objects("connect", sender, receiver);
        return QMetaObject::Connection(nullptr);
    }

    if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(),
                                              methodTypes.size(), methodTypes.constData())) {
        qWarning("QObject::connect: Incompatible sender/receiver arguments"
                 "\n        %s::%s --> %s::%s",
                 sender->metaObject()->className(), signal,
                 receiver->metaObject()->className(), method);
        return QMetaObject::Connection(nullptr);
    }

    int *types = nullptr;
    if ((type == Qt::QueuedConnection)
            && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) {
        return QMetaObject::Connection(nullptr);
    }

#ifndef QT_NO_DEBUG
    QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index);
    QMetaMethod rmethod = rmeta->method(method_index_relative + rmeta->methodOffset());
    check_and_warn_compat(smeta, smethod, rmeta, rmethod);
#endif
    QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
        sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));
    return handle;
}
  1. 这个函数的源码很庞大,但大多数都是校验,我们只需要看最后一行QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));,前面会根据我们传入的信号和槽函数的名字来获取signal_indexmethod_index_relative。当然这个函数里面是使用QMetaObjectPrivate来获取的,这个类我们用不了(当然通过特殊办法也可以用,但不建议),我们在外部可以使用QMetaObjectQMetaMethod来获取。然后再使用QMetaObjectPrivate::connect函数将信号的id和槽函数的id连接起来,我们这里就是将id为0的信号和id为1的槽函数连接起来,当id为0的信号发送之后,QT内部就会去查id为1的槽函数,然后再传给Test::qt_metacall函数就可以调用到testSlot函数啦。
  2. 上面说了QMetaObjectPrivate::connect函数我们是没办法使用的,但是QT在QMetaObject里面开放了一个connect函数QMetaObject::Connection QMetaObject::connect(const QObject *sender, int signal_index, const QObject *receiver, int method_index, int type = 0, int *types = nullptr),我们进到这个函数内部可以看到它也是调用了QMetaObjectPrivate::connect
QMetaObject::Connection QMetaObject::connect(const QObject *sender, int signal_index,
                                          const QObject *receiver, int method_index, int type, int *types)
{
    const QMetaObject *smeta = sender->metaObject();
    signal_index = methodIndexToSignalIndex(&smeta, signal_index);
    return Connection(QMetaObjectPrivate::connect(sender, signal_index, smeta,
                                       receiver, method_index,
                                       nullptr, //FIXME, we could speed this connection up by computing the relative index
                                       type, types));
}
  1. 最后再看一个问题,就是signal_index指代的id是从什么地方发出的。再次查看moc_Test.cpp文件,可以找到moc为我们自动实现的信号
// SIGNAL 0
void Test::testSignal()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

所以所谓的发送信号就是调用QMetaObject::activate函数并将信号的id传入进去,比如这个函数传入的就是固定的0。
8. 现在我们就知道了从信号到槽函数的整个调用过程,总结一下:

  • 去掉槽函数所在类里面的Q_OBJECT宏,但注意还是需要继承至QObject
  • 槽函数所在类手动重写virtual int qt_metacall(QMetaObject::Call, int, void **);函数
  • 使用QMetaObject::connect连接两个唯一id
  • 调用QMetaObject::activate函数并传入连接时的信号id
  • 槽函数手动重写的qt_metacall函数里面判断id为连接时的槽id并作相应的处理

下面改造一下Test类:

#ifndef TEST_H
#define TEST_H

#include <QObject>

class Test : public QObject
{
public:
    explicit Test(QObject *parent = nullptr);

    int qt_metacall(QMetaObject::Call _c, int _id, void **_a) override;
};

#endif // TEST_H
#include "Test.h"

#include <QDebug>

Test::Test(QObject *parent)
    : QObject{parent}
{
    QMetaObject::connect(this, metaObject()->methodCount(), this, metaObject()->methodCount() + 1);
    QMetaObject::activate(this, metaObject(), metaObject()->methodCount(), nullptr);
}

int Test::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) {
        if (_id == 1) { // 注意连接槽函数时id有加一
            qDebug() << "槽函数被调用了";
        }
        _id -= 2;
    }
    return _id;
}
#include "MainWindow.h"

#include <QApplication>

#include "Test.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Test t;
    MainWindow w;
    w.show();
    return a.exec();
}

运行上面的代码就能看到输出槽函数被调用了;

剩下的单开一章吧

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部