Qt信號槽原理


1、說明

使用Qt已經好幾年了,一直以為自己懂Qt,熟悉Qt,使用起來很是熟練,無論什么項目,都喜歡用Qt編寫。但真正去看Qt的源碼,去理解Qt的思想也就近兩年的事。

本次就着重介紹一下Qt的核心功能--信號槽機制,相信接觸過Qt的人都能很熟悉地使用,甚至,大部分人還能輕松地說出信息槽的幾種用法。但是信號槽的核心可不是簡單說說就能說清楚的。

那么,本次,就從Qt的源碼中講解一下信號槽的機制。

其實,直到寫這篇文章,我也沒有完全看明白相關的源碼,只是明白了其中的大部分以及使用機制,其中還有很多細節的,留待以后整理。

如果錯誤還請大家指正。

2、環境以及知識點

Qt版本:Qt 5.5.1

系統:windows 10

在閱讀本文前,希望你能:

  1. 熟練使用C++,了解make的編譯方法和過程;
  2. 熟練使用Qt的信號槽功能,對信號槽的寫法以及4和5的區別了如指掌;
  3. QMetaObject元數據系統;
  4. 懂一些設計模式,能理解觀察者模式;

3、信號槽源碼分析

以下將按照SIGNAL/SLOT宏定義連接信號槽的方式做講解

接下來將會從按照以下的步驟來進行分析:

  1. Qt元數據系統;
  2. moc預編譯;
  3. Q_OBJECT宏;
  4. signals和slots關鍵字以及emit;
  5. SIGNAL()和SLOT()宏;
  6. connect 方法;
  7. 觸發信號;

3.1、Qt的元數據系統

沒看過Qt源碼的同學可能會對QMetaObject有些陌生,我們打開Qt手冊,查看此類的說明,介紹如下:

The QMetaObject class contains meta-information about Qt objects.

The Qt Meta-Object System in Qt is responsible for the signals and slots inter-object communication mechanism, runtime type information, and the Qt property system. A single QMetaObject instance is created for each QObject subclass that is used in an application, and this instance stores all the meta-information for the QObject subclass. This object is available as QObject::metaObject().

這里是說,QMetaObject包含了Qt的元對象信息。元對象機制類似Java的反射機制。通過繼承QObject,並在定義類是添加一定Qt內置宏,能在運行時動態獲取Qt的信號槽、類型信息以及相關屬性。

一個簡答的例子

void MainWindow::onClickButton()
{
    qDebug()<<"on click button";
    const QMetaObject* metaObject = this->metaObject();
    qDebug()<<metaObject->className();
    qDebug()<<metaObject->superClass()->className();

    int methodIndex = metaObject->indexOfMethod("testFunction()");
    qDebug()<<methodIndex;
    qDebug()<<metaObject->method(methodIndex).name();
    metaObject->method(methodIndex).invoke(this);

    QMetaObject::invokeMethod(this, "testFunction");
}

如上,一個簡單的例子,通過QMetaObject,我們得到了該對象的類名、父類名、方法並調用了該方法

怎么樣,熟悉Java的小伙伴已經發現了,這不就是Java的反射嗎,誰說C++沒有反射呢

那么,Qt是如何實現”反射“的呢?答案是使用moc預編譯

3.2、moc編譯

moc全稱Meta-Object Compiler,即元對象編譯器。我們可以在Qt的安裝目錄的bin文件下看到moc工具,moc.exe。Qt的構建的時候,會調用該工具生成moc文件,我們在編譯目錄下看到的moc_xxx.cpp文件就是該工具生成的。

Qt的MinGW版本使用的是qmake進行項目管理,它和cmake功能類似,但沒有后者強大。使用qmake生成Makefile后,我們打開Makefile文件,我們可以狠清楚地看到有一個調用moc.exe工具的地方,代碼太多,就不列出來了。

此外,我們還發現,並不是所有的代碼都會生成moc_xxx.cpp文件的,只有使用了 Q_OBJECT 宏的類文件,才會生成。沒有錯,moc工具就是根據 Q_OBJECT 宏來生成moc_xxx.cpp文件的,而實現“反射”的元數據系統的也是依靠Q_OBJECT的。

到此,我們其實已經能夠大概理清qmake項目的構建步驟了。步驟和常用的cmake項目類似,區別就是,qmake生成的Makefile文件種,會寫有調用moc工具的指令,以達到moc_xxx.cpp文件的生成。

我們可以使用moc工具手動生成moc_xxx.cpp,使用指令 moc.exe mainwindow.h,即會在控制台打印moc文件信息,也可以使用 -o 參數來將生成的內容寫入文件,其余參數可以使用 moc.exe -h 來查看

3.3、Q_OBJECT

我們可以從源代碼中查看 Q_OBJECT 的內容,這里調整一個格式,使用 Q_OBJECT 宏之后,會在類定義的開頭多出以下代碼:

public:
    Q_OBJECT_CHECK
    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_WARNING_POP
    QT_TR_FUNCTIONS
private:
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
    struct QPrivateSignal {};

可以看到,這里多出幾個方法和一些變量

  1. 屬性staticMetaObject,元數據對象,可以從中獲取當前類的元數據;
  2. 方法metaObject(),獲取元數據對象指針,大多數情況下,返回staticMetaObject指針;
  3. 方法qt_metacast(),原數據對象類型轉換,轉換成指定的類型,使用時一般傳入父類的名稱字符串;
  4. 方法qt_metacall(),執行函數的回調,信號觸發;
  5. 方法qt_static_metacall(),回調函數,被qt_metacall()調用,內部執行槽;

這里的幾個方法都沒有實現體,因為實現部分會有 moc 工具生成,在moc_xxx.cpp 文件中可以查看這些方法的實現體

3.4、signals和slots

signals 用於聲明自定義信號,slots 用於聲明槽函數,emit 用於發送信號,我們可以從源碼中查看這三個宏定義

define slots
define signals public
define emit

可以看出,這三個宏幾乎什么都沒有做,signals 就是聲明所謂的信號是public方法,而slots和emit更是為空,標准C++在編譯的時候,根本不受這三個宏的影響,那么它們的用處在哪里呢?在moc工具調用和connect連接的時候。

打開moc_xxx.cpp文件,對比查看信號

signals:
    void clickButton(int value);

    void clickButton2();
// SIGNAL 0
void MainWindow::clickButton(int _t1)
{
    void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void MainWindow::clickButton2()
{
    QMetaObject::activate(this, &staticMetaObject, 1, Q_NULLPTR);
}

其實,信號就是方法,而 emit clickButton() 發送信號,就是調用 clickButton() 方法,換言之,觸發信號,就算不要emit也無妨

3.5、SIGNAL()和SLOT()

查看源碼

# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)

qFlagLocation() 源碼如下:

const char *qFlagLocation(const char *method)
{
    QThreadData *currentThreadData = QThreadData::current(false);
    if (currentThreadData != 0)
        currentThreadData->flaggedSignatures.store(method);
    return method;
}

store() 方法

void store(const char* method)
{ locations[idx++ % Count] = method; }

所以 SIGNAL(clickButton()) 宏展開為 qFlagLocation("2"clickButton(int) QLOCATION)

SLOT() 同理,這里的1和2,最后會添加到信號槽的前面,其實是為了區分信號和槽,源碼中還有一個0在 METHOD()

qFlagLocation 方法的作用是將信號槽轉換成字符串保存起來,store 方法中,locations是個二維數組,而 idx 每次都加一,保證信號和槽的不同的方法存儲在不同的數組中。

我們也可以在代碼中打印出來看下:

qDebug()<<SIGNAL(clickButton(int));	//2clickButton(int)
qDebug()<<SLOT(onClickButton());	//1onClickButton()

3.6、connect方法

最后,就是最關鍵的connect方法,做了一些簡單的注釋

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
                                     const QObject *receiver, const char *method,
                                     Qt::ConnectionType type)
{
    if (sender == 0 || receiver == 0 || signal == 0 || method == 0) {
        qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
                 sender ? sender->metaObject()->className() : "(null)",
                 (signal && *signal) ? signal+1 : "(null)",
                 receiver ? receiver->metaObject()->className() : "(null)",
                 (method && *method) ? method+1 : "(null)");
        return QMetaObject::Connection(0);
    }
    QByteArray tmp_signal_name;

    if (!check_signal_macro(sender, signal, "connect", "bind"))
        return QMetaObject::Connection(0);
    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());
    //小於0表示,表示信號索引有問題
    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(0);
    }
    //根據當前信號的索引找到最原始的信號的索引,因為信號是可以被繼承,這里找的祖先信號
    signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
    signal_index += QMetaObjectPrivate::signalOffset(smeta);//信號的索引再加上信號的偏移量

    QByteArray tmp_method_name;
    //提取槽的編碼,應該是QSLOT_CODE或者QSIGNAL_CODE,用於判斷槽是信號還是方法
    int membcode = extract_code(method);

    //檢查槽編碼,槽可以是槽函數或者信號,初次以為,都無效
    if (!check_method_code(membcode, receiver, method, "connect"))
        return QMetaObject::Connection(0);
    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;
    }
    //槽的索引為-1,表示無效
    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(0);
    }

    //檢查信號和槽的參數
    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(0);
    }

    int *types = 0;
    //隊列連接檢查,參數要是基本類型,或者使用元數據注冊
    if ((type == Qt::QueuedConnection)
            && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) {
        return QMetaObject::Connection(0);
    }

#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;
}

方法代碼很多很雜,但無非就是檢查信號槽的格式,獲取參數列表, 最后保存起來

QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection);
c->sender = s;  //發送者對象
c->signal_index = signal_index; //信號索引
c->receiver = r;    //接受者對象
c->method_relative = method_index;  //槽索引
c->method_offset = method_offset;   //槽偏移
c->connectionType = type;           //連接方式
c->isSlotObject = false;
c->argumentTypes.store(types);
c->nextConnectionList = 0;
c->callFunction = callFunction;//靜態回調函數

//在發送者元數據內加上連接信息
//信號發送者的對象內存中保存了連接的信息,包括槽的對象,槽地址,連接方式等
QObjectPrivate::get(s)->addConnection(signal_index, c.data());

3.7、觸發信號

這時候再回過頭來看3.4中的信號觸發,我們知道,emit信號就是調用moc文件中的方法,方法的核心就是 QMetaObject::activate()

直接看該方法中調用槽函數的一段

//因為一個信號可能連接多個槽,這里循環遍歷鏈表進行調用
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 {
		if (!c->receiver)
			continue;

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

		// 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_index, c, argv ? argv : empty_argv, locker);
			continue;
#ifndef QT_NO_THREAD
			//阻塞式隊列連接
		} 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;//資源計數器,avail為0
			QMetaCallEvent *ev = c->isSlotObject ?
				new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) :
				new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore);
			//根據連接信息構造一個事件,並添加到接受者的 事件隊列中
			QCoreApplication::postEvent(receiver, ev);
			//信號發送者的線程阻塞,acquire資源數為1,>avail(0),這里阻塞
			//當槽執行玩之后釋放,這里的avail才會增加,阻塞結束
			semaphore.acquire();
			locker.relock();
			continue;
#endif
		}

		QConnectionSenderSwitcher sw;

		if (receiverInSameThread) {
			sw.switchSender(receiver, sender, signal_index);
		}
		const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
		const int method_relative = c->method_relative;
		if (c->isSlotObject) {
			c->slotObj->ref();
			QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj);
			locker.unlock();
			obj->call(receiver, argv ? argv : empty_argv);

			// Make sure the slot object gets destroyed before the mutex is locked again, as the
			// destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool,
			// and that would deadlock if the pool happens to return the same mutex.
			obj.reset();

			locker.relock();
		} else 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();
			const int methodIndex = c->method();
			if (qt_signal_spy_callback_set.slot_begin_callback != 0)
				qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv);

			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, methodIndex);
			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(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();
		}

		if (connectionLists->orphaned)
			break;
	} while (c != last && (c = c->nextConnectionList) != 0);

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

由上面代碼,我們大概可以理解信號槽的幾種連接方式:

  1. 默認連接並且信號槽的對象不在同一個線程中,則效果和隊列連接類似;
  2. 阻塞時隊列連接,信號和槽對象不同處於同一個線程中;
  3. Qt使用QSemaphore來實現阻塞式的槽函數調用;

4、小結

本次的源碼因為種種原因,看的不是很詳細,但是理解Qt的信號槽機制綽綽有余了

  1. Qt自帶的元數據系統利用C++的宏等特性實現反射機制;
  2. 利用元數據系統,在連接信號槽是將槽的信息(接收對象、槽方法、參數列表、連接方式等)保存在信號的元數據中;
  3. 信號也是方法,方法體有moc工具生成,方法內獲取該信號連接的所有槽信息,並依序執行;

直到這里,信號槽的邏輯已經顯而易見了,它就是一個變種的觀察者模式,槽的信息保存在信號對象中也就是設置回調函數,觸發信號也就是執行回調函數,只是Qt庫將其中的各種操作細節封裝起來了,所以,使用起來,不去關注設計模式的細節,也就容易很多了。不得不說,無論是從設計思路,還是開發技巧上看,Qt的開發者真的很牛叉。

5、第三方信號槽庫

信號槽機制是Qt首創,但不是其獨有,其他各類C++流行框架也都是互相借鑒,C++標准庫的預備役的boost中也有信號槽機制的實現。如果平時開發中需要用到信號槽機制,但是又不想引入這些龐大的類庫,可以使用輕量級別的信號槽庫:http://sigslot.sourceforge.net,該庫不詳細介紹,有興趣的小伙伴自己學習把。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM