[Qt]窥探信号槽的实现细节

简要目录

简介

本文转载于涛哥的知乎文章,原文链接窥探信号槽的实现细节,个人感觉讲解的非常好,深入浅出,让我比较好的理解了QT信号槽的实现机制,也解决了我一直以来的困惑。现转载过来便于自己时常复习。

本文关于信号-槽的实现细节,信号-槽是一种对象之间的通信机制(如下图能够比较好的理解),是Qt在标准C++之外,使用元对象编译器(meta-object compiler, MOC)实现的语法。

元对象编译器

元对象编译器简单介绍:Qt不是标准的C++语言,而是C++语言的扩充,从Qt的关键字emit,slots,signals就能够看出来,所以有人会觉得Qt 的程序编译速度慢,这主要是因为在 Qt 将源代码交给标准 C++ 编译器之前,需要事先将这些扩展的语法去除掉。而完成这一操作的工具就是所谓的MOC。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件。如果它发现在一个头文件中包含了宏 Q_OBJECT,则会生成另一个 C++ 源文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。这个新的文件名字将会是原文件名前面加上 moc_ 构成。这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译处理。另外,还可以看出一点,moc的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT 宏就不存在了。

Qt的元对象系统是用来处理对象间通信的信号和槽,它运行信息类型和动态属性。

Qt的元对象系统包括以下3个部分的内容:

  1. QObject类;
  2. 类声明私有段中的Q_OBJECT宏;
  3. 元对象编译器

下面通过一个简单的案例,学习一些信号-槽的实现细节
[Qt]窥探信号槽的实现细节

猫和老鼠的故事

[Qt]窥探信号槽的实现细节
Tom有个技能叫”喵”,就是发出猫叫,而正在偷吃东西的Jerry,听见猫叫声就会逃跑。

我们用信号-槽的方式写出来。

Tom类实现

//Tom.h,汤姆类实现,包含信号猫叫(miao)
#pragma once

#include <QObject>
#include <QDebug>
class Tom : public QObject
{
    Q_OBJECT
public:
    Tom(QObject *parent = nullptr) : QObject(parent)
    {
    }
    void miaow()
    {
        qDebug() <<  u8"喵!" ;
        emit miao();
    }
signals:
    void miao();
};

Jerry类实现

//Jerry.h,杰瑞类实现,包含槽函数runAway,对Tom对象miao信号做出反应
#pragma once

#include <QObject>
#include <QDebug>
class Jerry : public QObject
{
    Q_OBJECT
public:
    Jerry(QObject *parent = nullptr) : QObject(parent)
    {
    }
public slots:
    void runAway()
    {
        qDebug() << u8"那只猫又来了,快溜!" ;
    }
};

以上面的代码为例,要使用信号-槽功能,先决条件是继承QObject类,并在类声明中增加Q_OBJECT宏

之后在”signals:” 字段之后声明一些函数,这些函数就是信号。

在”public slots:” 之后声明的函数,就是槽函数。

main函数:

//main.cpp
#include <QCoreApplication>
#include "Tom.h"
#include "Jerry.h"
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Tom tom;
    Jerry jerry;

    QObject::connect(&tom, &Tom::miao, &jerry, &Jerry::runAway);
    tom.miaow();


    return a.exec();
}

该main函数声明了Tom和Jerry类的实例(对象),将信号函数以及槽函数进行连接,实现了两个对象(tom, jerry对象)之间的通信,接着emit发送tom对象的miao信号函数,从而使得槽函数runAway触发。

运行结果

运行结果:
[Qt]窥探信号槽的实现细节

信号-槽的声明和实现

信号和槽的本质都是函数。我们知道C++中的函数要有声明(declare),也要有实现(implement), 而信号只要声明,不需要写实现。这是因为moc会为我们自动生成。另外触发信号时,不写emit关键字,直接调用信号函数,也是没有问题的。这是因为emit是一个空的宏

#define emit

Q_OBJECT宏

我们来看一下Q_OBJECT宏,展开如下:

(不同的Qt版本有些差异,这里用的是5.12.4,以此为例)

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

可以看到,关键的地方,是声明了一个只读的静态成员变量staticMetaObject(QMetaObject类型),以及3个public的成员函数。

    static const QMetaObject staticMetaObject; 

    virtual const QMetaObject *metaObject() const; 

    virtual void *qt_metacast(const char *); 

    virtual int qt_metacall(QMetaObject::Call, int, void **);

还有一个静态函数成员

static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **)

那么声明的这些成员变量/函数,在哪里实现呢?答案是moc生成的cpp文件。

信号的moc生成

[Qt]窥探信号槽的实现细节
如上图所示目录结构,项目编译完成后,在build文件夹中,自动生成了moc_Jerry.cppmoc_Tom.cpp两个文件

其中moc_Tom.cpp内容如下:

/****************************************************************************
** Meta object code from reading C++ file 'Tom.h'
**
** Created by: The Qt Meta Object Compiler version 67 (Qt 5.12.4)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/

#include "../../TomJerry/Tom.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qmetatype.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'Tom.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 67
#error "This file was generated using the moc from 5.12.4. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif

QT_BEGIN_MOC_NAMESPACE
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
struct qt_meta_stringdata_Tom_t {
    QByteArrayData data[3];
    char stringdata0[10];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_Tom_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_Tom_t qt_meta_stringdata_Tom = {
    {
QT_MOC_LITERAL(0, 0, 3), // "Tom"
QT_MOC_LITERAL(1, 4, 4), // "miao"
QT_MOC_LITERAL(2, 9, 0) // ""

    },
    "Tom\0miao\0"
};
#undef QT_MOC_LITERAL

static const uint qt_meta_data_Tom[] = {

 // content:
       8,       // revision
       0,       // classname
       0,    0, // classinfo
       1,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount

 // signals: name, argc, parameters, tag, flags
       1,    0,   19,    2, 0x06 /* Public */,

 // signals: parameters
    QMetaType::Void,

       0        // eod
};

void Tom::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<Tom *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->miao(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (Tom::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Tom::miao)) {
                *result = 0;
                return;
            }
        }
    }
    Q_UNUSED(_a);
}

QT_INIT_METAOBJECT const QMetaObject Tom::staticMetaObject = { {
    &QObject::staticMetaObject,
    qt_meta_stringdata_Tom.data,
    qt_meta_data_Tom,
    qt_static_metacall,
    nullptr,
    nullptr
} };


const QMetaObject *Tom::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

void *Tom::qt_metacast(const char *_clname)
{
    if (!_clname) return nullptr;
    if (!strcmp(_clname, qt_meta_stringdata_Tom.stringdata0))
        return static_cast<void*>(this);
    return QObject::qt_metacast(_clname);
}

int Tom::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)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 1;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 1)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 1;
    }
    return _id;
}

// SIGNAL 0
void Tom::miao()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
QT_WARNING_POP
QT_END_MOC_NAMESPACE

可以大致看出,生成的cpp文件中,就是变量staticMetaObject以及那几个函数的实现。

staticMetaObject是一个结构体,用来存储Tom这个类的信号、槽等元信息,并把qt_static_metacall静态函数作为函数指针存储起来。

因为是静态成员,所以实例化多少个Tom对象,它们的元信息都是一样的,不能改变。

qt_static_metacall函数提供了两种“元调用的实现”:

  1. 如果是InvokeMetaMethod类型的调用,则直接把参数中的QObject对象,转换成Tom类然后调用其miao函数
  2. 如果是IndexOfMethod类型的调用,即获取元函数的索引号,则计算miao函数的偏移并返回。

而moc_Tom.cpp末尾的:

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

就是信号函数的实现。

信号的触发

miao信号的实现,直接调用了QMetaObject::activate函数。其中0代表miao这个函数的索引号。

QMetaObject::activate函数的实现,在Qt源码的QObject.cpp文件中,略微复杂一些,且不同版本的Qt,实现差异都比较大,这里总结一下大致的实现:

先找出与当前信号连接的所有对象-槽函数,再逐个处理。这里处理的方式,分为三种:

if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
    // 队列处理
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
    // 阻塞处理
    // 如果同线程,打印潜在死锁。
} else {
    //直接调用槽函数或回调函数
}

receiverInSameThread表示当前线程id和接收信号的对象的所在线程id是否相等。

  1. 如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。
  2. 如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。
  3. 如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,本身就是阻塞的,直接调用就好了,如果走阻塞队列,则多了加锁的过程。如果槽中又发了同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)

队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent放进了事件循环,等到下一次事件分发,相应的线程才会去调用槽函数。

槽和moc生成

slot函数我们自己已经实现了,moc不会做额外的处理,所以自动生成的moc_Jerry.cpp文件中,只有Q_OBJECT宏的展开,和前面的moc_Tom.cpp是一致的,不赘述了。

上一篇:配置chrony服务,实现服务器时间自动同步


下一篇:《Qt5.9 c++开发指南》