天天看点

Qt 信号与槽实现原理前言概述signals和slots宏MOC 元对象编译器connect连接实现emit发送实现总结

前言

之前一直停留在使用Qt库的层面,底层的实现也只是了解到一些皮毛而已,现在需要更深的了解它的实现原理,对以后开发会有很大的帮助。

概述

按照我整个深入了解的过程,介绍以下几点主要内容:

  • signals和slots宏
  • MOC 元对象编译器
  • connect连接实现
  • emit发送实现

signals和slots宏

Qt中的signals和slots两个宏的源码:

#     define slots Q_SLOTS
#     define signals Q_SIGNALS

# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)
           

可以看到的是这两个宏是没有意义的,那编译的时候怎么去处理呢?如何辨别哪个是信号,哪个是槽函数呢?

带着这些问题查找资料… …

MOC 元对象编译器

标准C++不支持Qt的元对象系统,所以Qt单独提供了MOC工具来解决和C++的兼容问题。

MOC在预处理的时候,读取C++头文件,如果包含Q_OBJECT宏,则将生成一个C++源文件(moc_headername.cpp)。然后将新生成的源文件和其他文件一起进行编译、链接生成程序。

下面列举我创建的简单的Qt项目来看一下moc生成的源文件:

.h:

class QWidgetTest : public QWidget
{
    Q_OBJECT

public:
    QWidgetTest(QWidget *parent = Q_NULLPTR);

private:
    Ui::QWidgetTestClass ui;

private:
    int a_;

public:
    void func(int a);

signals:
    void sigTest();

    void sigTttt(int a);

public slots:
    void onTest();

    void onButtonClicked();
};
           

.cpp:

QWidgetTest::QWidgetTest(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
	connect(this, SIGNAL(sigTest()), this, SLOT(onTest()));
	connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(onButtonClicked()));
}

void QWidgetTest::func(int a)
{

}

void QWidgetTest::onTest()
{

}

void QWidgetTest::onButtonClicked()
{
	emit sigTest();
}
           

下面将一部分一部分来介绍MOC预处理生成的moc_QWidgetTest.cpp文件。

// 该结构是存储类中的信号、信号参数、槽函数以及类名
struct qt_meta_stringdata_QWidgetTest_t {
    QByteArrayData data[7]; // 有7个信息
    char stringdata0[54];   // 将这些信息按照顺序组成字符串存储
};
/*  
    idx: 信息对应的索引值
    ofs: 在字符串中的偏移量
    len: 偏移长度
*/
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_QWidgetTest_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_QWidgetTest_t qt_meta_stringdata_QWidgetTest = {
    {
QT_MOC_LITERAL(0, 0, 11), // "QWidgetTest"
QT_MOC_LITERAL(1, 12, 7), // "sigTest"
QT_MOC_LITERAL(2, 20, 0), // ""             // 因为sigTest信号不带参数,所以为空
QT_MOC_LITERAL(3, 21, 7), // "sigTttt"
QT_MOC_LITERAL(4, 29, 1), // "a"            // 信号sigTttt的参数
QT_MOC_LITERAL(5, 31, 6), // "onTest"
QT_MOC_LITERAL(6, 38, 15) // "onButtonClicked"

    },
    "QWidgetTest\0sigTest\0\0sigTttt\0a\0onTest\0"
    "onButtonClicked"
};
#undef QT_MOC_LITERAL
           

从源代码可以看出,MOC把类名、信号、信号参数以及槽函数以字符串的形式存储在stringdata中,然后放到qt_meta_stringdata_QWidgetTest_t 结构体中。

// 该数组主要存储元对象信息(信号、槽函数、类信息和属性系统相关信息)
static const uint qt_meta_data_QWidgetTest[] = {

 // content:
       8,       // revision
       0,       // classname        // 类名,值的含义是其在qt_meta_stringdata_QWidgetTest_t结构中的索引值
       0,    0, // classinfo        // 前者0表示有0个classinfo,后者0表示classinfo在qt_meta_data_QWidgetTest中的索引
       // classinfo 需要在头文件中使用Q_CLASSINFO,主要用于以Key-Value的形式定义类的附加信息
       4,   14, // methods          // 4表示有4个method的信息,14表示具体内容在qt_meta_data_QWidgetTest数组中的索引
       0,    0, // properties       // 含义同上
       0,    0, // enums/sets       // 含义同上
       0,    0, // constructors     // 含义同上
       0,       // flags            // flag,具体含义不是很清楚
       2,       // signalCount      // 信号个数

    /* 
    上面methods字段介绍了有4个method信息,具体含义如下
    name:对应qt_meta_stringdata_QWidgetTest_t结构中的索引值
    argc:参数个数
    */
 // signals: name, argc, parameters, tag, flags
       1,    0,   34,    2, 0x06 /* Public */,
       3,    1,   35,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       5,    0,   38,    2, 0x0a /* Public */,
       6,    0,   39,    2, 0x0a /* Public */,

    // 信号参数类型
 // signals: parameters
    QMetaType::Void,
    QMetaType::Void, QMetaType::Int,    4,

    // 槽函数参数类型
 // slots: parameters
    QMetaType::Void,
    QMetaType::Void,

       0        // eod
};
           

qt_meta_data_QWidgetTest数组主要是存储元对象信息的一些索引参数,我个人觉得这种处理方式不是很好,就像是强行约定了一种规则,可能这样做效率会更高,就不继续深究了… …

// 执行对象对应的信号和槽函数,或者是查找信号的索引
void QWidgetTest::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<QWidgetTest *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->sigTest(); break;
        case 1: _t->sigTttt((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 2: _t->onTest(); break;
        case 3: _t->onButtonClicked(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (QWidgetTest::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&QWidgetTest::sigTest)) {
                *result = 0;
                return;
            }
        }
        {
            using _t = void (QWidgetTest::*)(int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&QWidgetTest::sigTttt)) {
                *result = 1;
                return;
            }
        }
    }
}
// 初始化静态元对象,所有实例化的对象公用一个静态元对象
QT_INIT_METAOBJECT const QMetaObject QWidgetTest::staticMetaObject = { {
    QMetaObject::SuperData::link<QWidget::staticMetaObject>(),
    qt_meta_stringdata_QWidgetTest.data,
    qt_meta_data_QWidgetTest,
    qt_static_metacall,
    nullptr,
    nullptr
} };

// 获取元对象
const QMetaObject *QWidgetTest::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

// 通过类名获取元对象指针
void *QWidgetTest::qt_metacast(const char *_clname)
{
    if (!_clname) return nullptr;
    if (!strcmp(_clname, qt_meta_stringdata_QWidgetTest.stringdata0))
        return static_cast<void*>(this);
    return QWidget::qt_metacast(_clname);
}

// 这里应该是通过元对象来调用对应的方法
int QWidgetTest::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 < 4)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 4;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 4)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 4;
    }
    return _id;
}

// 下面就是信号函数的具体实现,其实就是调用了avtivate
// SIGNAL 0
void QWidgetTest::sigTest()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

// SIGNAL 1
void QWidgetTest::sigTttt(int _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}
           

上面的代码主要是实现一些元对象的方法调用以及信号函数。

刚开始接触信号槽的时候还疑惑为什么信号不用实现,emit只是一个宏,怎么去触发槽函数呢。看到这里才有所了解,信号是由MOC预处理时实现。

connect连接实现

因为篇幅原因,单独写一篇文章《Qt connect的实现原理》。

emit发送实现

上面也介绍了,emit只是一个宏,实际上就是调用了MOC预处理时生成的信号函数。下面具体介绍一下信号函数是如何处理的:

void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
                           void **argv)
{
    int signal_index = local_signal_index + QMetaObjectPrivate::signalOffset(m);

    if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
        doActivate<true>(sender, signal_index, argv);
    else
        doActivate<false>(sender, signal_index, argv);
}

/*!
    \internal
 */
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
    int signal_index = signalOffset + local_signal_index;

    if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
        doActivate<true>(sender, signal_index, argv);
    else
        doActivate<false>(sender, signal_index, argv);
 }

/*!
    \internal
   signal_index comes from indexOfMethod()
*/
void QMetaObject::activate(QObject *sender, int signal_index, void **argv)
{
    const QMetaObject *mo = sender->metaObject();
    while (mo->methodOffset() > signal_index)
        mo = mo->superClass();
    activate(sender, mo, signal_index - mo->methodOffset(), argv);
}
           

可以看到activate函数有三种重载,主要还是调用了doActivate模板函数,因为该函数篇幅有点长,就单独写一篇文章《Qt 信号如何触发槽函数?》。

总结

概况一下信号与槽的实现原理和流程:

  1. MOC预处理:

    1.1. 把类名、信号、槽函数以及对应的参数以字符串的形式存储在结构体中

    1.2. 把上面的信息对应的索引、偏移量和偏移长度存储到结构体中的二维数组中

    1.3. 然后实现一系列方法和信号函数

  2. connect操作将发送者、信号索引、发送者元对象、接收者、槽函数索引、接收者元对象等信息存到Connection结构体中
  3. emit操作时执行预处理生成的信号函数

    3.1. 如果是直连则直接调用槽函数或回调函数

    3.2. 如果是跨线程走队列模式则放入事件队列中

    3.3. 如果是阻塞队列链接则先阻塞然后通过事件系统异步调用(post)槽函数

继续阅读