天天看點

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)槽函數

繼續閱讀