Qt 信號和槽源碼分析

作者:曹羣
原文:https://mp.weixin.qq.com/s/Mp...
歡迎關注學而思網校技術團隊公衆號:

clipboard.png

引言:

Qt 是一個1991年由Qt Company開發的跨平臺C++圖形用戶界面應用程序開發框架。它既可以開發GUI程序,也可用於開發非GUI程序,比如控制檯工具和服務器。Qt是面向對象的框架,使用特殊的代碼生成擴展(稱爲元對象編譯器(Meta Object Compiler, moc))以及一些宏,Qt很容易擴展,並且允許真正地組件編程。Qt是跨平臺開發框架,支持Windows、Linux、MacOS等不同平臺;Qt有大量的開發文檔和豐富的API,給開發者帶來了很大的方便;Qt的使用者也越來越多,有很多優秀的產品都基於Qt開發,如:WPS Offic 、Opera瀏覽器、Qt Creator等。Qt的核心機制就是信號和槽,接下來我們通過源代碼分析一下實現原理。

基本概念:

信號:當對象改變其狀態時,信號就由該對象發射 (emit) 出去,而且對象只負責發送信號,它不知道另一端是誰在接收這個信號。
槽:用於接收信號,而且槽只是普通的對象成員函數。一個槽並不知道是否有任何信號與自己相連接。
信號與槽的連接:所有從 QObject 或其子類 ( 例如 QWidget ) 派生的類都能夠包含信號和槽。是通過靜態方法:QObject::connect(sender, SIGNAL(signal), receiver, SLOT(slot)); 來進行管理的,其中 sender 與 receiver 是指向對象的指針,SIGNAL() 與 SLOT() 是轉換信號與槽的宏。

實現原理:

1、首先我們搭建好環境,如在Windows系統上:安裝Qt5.7(包括源碼) + VS2013 及 對應的插件,我們主要是通過VS來進行編譯調試的。

2、我們寫一個簡單實例,然後進行構建,再把Qt安裝目錄中的QtCored的pdb拷貝到我們的可執行文件目錄下面,如下圖所示:

下面是我們要分析的Demo代碼:
// MainWindow.h

// MainWindow.cpp

我們可以創建一個Qt工程,名稱爲Demo,編寫上面的代碼,進行構建,在VS下可以把Qt工程導成VS工程,編譯生成,運行結果如下:

點擊中間的按鈕,我們可以看到控制檯打印如下信息:

第一步:基本結構:

我們分析代碼,可以看到在頭文件Test和MainWindow類中,都有Q_OBJECT這樣的宏,然後我們可以看到上面的可執行文件夾下多出來一個moc_MainWindow.cpp文件,那麼我們可以嘗試把這兩個宏去掉,再進行構建,發現加上了信號和槽的就無法編譯過去,我們去掉這些信號和槽後,就不會生成moc開頭的這個文件了,當然我們就無法實現信號和槽機了,那麼這個宏到底是什麼,有了它編譯器又會做什麼?讓我們看看這個宏:

原來這個宏就是一些靜態方法和虛方法,但是如果我們加入到類中,不進行實現,那一定會報錯的,爲什麼還可以正常運行呢?原來Qt幫我們做了很多事情,在編譯器編譯Qt代碼之前,Qt先將Qt自身擴展的語法進行翻譯,這個操作是通過moc(Meta-Object Compiler)又稱“元對象編譯器”完成的。首先moc會分析源代碼,把包含Q_OBJECT的頭文件生成爲一個C++源文件,這個文件的名字會是源文件名前面加上moc_,之後和原文件一起通過編譯器處理,那我們想到,這個moc開頭的cpp中一定實現了上面宏裏面的方法,以及數據的賦值;接下來我們看看moc_MainWindow.cpp這個文件:

我們從上面的代碼中可以看到,是對Q_OBJECT中的靜態數據進行了賦值,並且實現了那些方法,這些都是Qt的moc編譯器幫我們生成的,對代碼進行了分析,對信號和槽生成了符號,以及特定的數據結構,下面這個主要是記錄了類、信號、槽的引用計數、大小、偏移,後面會用到。

通過把QT_MOC_LITERAL這個宏進行替換後,得到如下數據 :

接下來我們看看下面qt_meta_data_MainWindow這個數組結構:content有兩列,第一列是總數,第二列是在這個數組中描述開始的索引,如1, 14, // methods,說明有一個methods,我們可以看到slots就是從索引14開始的。

從最上面的源代碼中我們可以看到再關聯信號和槽的時候,用到了SIGNAL和SLOT這兩個宏,那麼這兩個宏到底有什麼作用呢?我們分析一下:

分析:
從上面我們可以看到其實這兩個就是一個字符串拼接的宏,會在信號(signal)前面拼接"2",如”2clean()“;會在槽(slots)前面拼接"1",如”1onClean()“; 其中,qFlagLocation這個方法主要是把method存儲在QThreadData裏面FlaggedDebugSignatures中的const char* locations[Count];表中,用於定位代碼對應的行信息。

預編譯後如下:

通過上面的一些基本宏、數據結構的介紹,我們知道Qt給我們做了很多工作,幫我們生成了moc代碼,給我們提供了一些宏,讓我們開發簡潔方便,那麼Qt又是如何把信號和槽進行關聯的呢,就是兩個不同的實例,又是如何進行通過信號槽機制進行通信的呢?接下來我們看看信號和槽關聯的實現原理:

第二步、信號和槽的關聯:

1、檢先對信號和槽的字符串進行檢查,QSIGNAL_CODE 是 1 ;SIGNAL_CODE 是 2。

2、獲取元數據(sender和receiver同理)。

這個方法就是我們上面moc_MainWindow.cpp中。

我們根據調試可以看到QObject::d_ptr->metaObject是空的,所以這樣smeta就是上面這個staticMetaObject變量了。

// 首先我們得了解一下這個QMetaObject 和 QMetaObjectPrivate 的定義:

在Qt中爲了實現二進制兼容性,一般會定義一個私有類,QMetaObjectPrivate就是QMetaObject的私有類,QMetaObject負責一些接口實現,QMetaObjectPrivate具體進行實現,這兩個類一般是通過P指針和D指針進行組合式的訪問,有一個宏:

我們看上面的staticMetaObject是一個QMetaObject類型的變量,其中QMetaObject進行了賦值:

1)&QWidget::staticMetaObject(父對象的MetaObject)-> superdata
2)qt_meta_stringdata_Test.data -> stringdata
3)qt_meta_stringdata_Test() -> data
4)qt_static_metacall(回調函數)->static_metacall

其中QMetaObject 是對外的結構,裏面的connect方法最終調用的還是QMetaObjectPrivate裏面的connect進行實現的。QMetaObject裏的d成員填充了上面的staticMetaObject數據,而QMetaObjectPrivate裏面的成員填充qt_meta_stringdata_Test數組中的數據,我們可以看到填充前14個數據,這也是moc生成methodData時以14爲基數的原因了,轉換方法如下:

3、對信號參數、名稱進行獲取和保存,如下,把信號的參數保存起來,返回方法名稱。

4、計算索引(包括基類)。

具體實現如下:

其中int handle = priv(m->d.data)->methodData + 5i; 我們可以分析,其實就是14+5i ,那爲什麼是5呢?因爲:
// signals: name, argc, parameters, tag, flags
1, 0, 24, 2, 0x06 / Public /,
// slots: name, argc, parameters, tag, flags
3, 0, 25, 2, 0x08 / Private /,
我們可以看到每一個signals或者slots都有5個整形表示。

5、對掩碼進行檢查。

// MethodFlags是一個枚舉類型,我們可以看到MethodSignal = 0x04, MethodSlot = 0x08;

// slots: name, argc, parameters, tag, flags
3, 0, 25, 2, 0x08 / Private /,

6、判斷鏈接類型,默認是Qt::AutoConnection。

enum ConnectionType {

AutoConnection,

DirectConnection,

QueuedConnection,

BlockingQueuedConnection,

UniqueConnection = 0x80

};

我們介紹一些連接類型:
1、AutoConnection:自動連接:默認的方式,信號發出的線程和糟的對象在一個線程的時候相當於:DirectConnection, 如果是在不同線程,則相當於QueuedConnection。
2、DirectConnection:直接連接:相當於直接調用槽函數,但是當信號發出的線程和槽的對象不再一個線程的時候,則槽函數是在發出的信號中執行的。
3、QueuedConnection :隊列連接:內部通過postEvent實現的。不是實時調用的,槽函數永遠在槽函數對象所在的線程中執行。如果信號參數是引用類型,則會另外複製一份的。線程安全的。
4、BlockingQueuedConnection:阻塞連接:此連接方式只能用於信號發出的線程 和 槽函數的對象不再一個線程中才能用,通過信號量+postEvent實現的,不是實時調用的,槽函數永遠在槽 函數對象所在的線程中執行,但是發出信號後,當前線程會阻塞,等待槽函數執行完畢後才繼續執行。
5、UniqueConnection :防止重複連接。如果當前信號和槽已經連接過了,就不再連接了。

最後到了信號和槽關聯核心的地方了:

首先,我們先得了解以下數據結構:

上面的這三個數據結構很重要,QObject是我們最熟悉的基類,QObjectPrivate是它的私有類,進行具體實現,QObjectPrivate繼承自QObjectData,在QObject裏面以組合的形式也進行P指針和D指針的方式進行訪問的。在信號和槽關聯過程中,數據結構Connection是很重要的數據結構,下面的這個結構是ConnectionList的一個Vector:

有了上面的數據結構,我們就可以分析下面的鏈接過程了, 我們看到下面的先是調用的QMetaObjectPrivate的connect, 之後又用QMetaObject::Connection進行了指針包裝:

QObjectPrivate::get(s) 方法其實就是獲取了一個QObjec裏面的QObjectPrivate實例,之後調用addConnection方法添加到鏈表中:

結構如下:

分析:
1、每個QObject對象都有一個QObjectConnectionListVector結構,這是一個Vector容器,它裏面的基本單元都是ConnectionList類型的數據,ConnectionList的個數與該QObject對象的signal個數相同。每個ConnectionList對應一個信號,它記錄了連接到這個信號上的所有連接。前面已經看到ConnectionList的定義中有兩個重要成員:first和last,他們都是Connection 類型的指針,分別指向連接到這個信號上的第一個和最後一個連接。所有連接到這個信號上的連接以單向鏈表的方式組織了起來,Connection結構體中的nextConnectionList成員就是用來指向這個鏈表中的下一個連接的。
2、同時,每個QObject對象還有一個senders成員,senders是一個Connection類型的指針,senders本身也是一個鏈表的頭結點,這個鏈表中的所有結點都是連接到這個QObject對象上的某個槽的連接。不過這個鏈表跟上一段提到的鏈表可不是同一個,雖然他們可能有一些共同結點。
3、每一個Connection對象都同時處於兩個鏈表當中。其中一個是以Connection的nextConnectionList成員組織起來的單向鏈表,這個單項鍊表中每個結點的共同點是,他們都依賴於同一個QObject對象的同一個信號,這個鏈表的頭結點就是這個信號對應的ConnectionList結構中的first;另一個鏈表是以Connection的next和prev成員組織起來的雙向鏈表,這個雙向鏈表中每個結點的共同點是,他們的槽都在同一個QObject對象上,這個鏈表的頭結點就是這個Qobject對象的sender。這兩個鏈表會有交叉(共同結點),但他們有不同的鏈接指針,所以不是同一個鏈表。

4、在Connect的時候,就是先new一個Connection對象出來,設置好這個連接的信息後,將它分別添加到上面提到的兩個鏈表中;disconnect的時候,就從從這兩個鏈表中將它移除,然後delete掉。而當一個QObject對象被銷燬的時候,它的sender指針指向的那個雙向鏈表中的所有連接都會被逐個移除!

第三步、發送信號到接受信號:

1、我們點擊上面的button後,然後調用到onDestory槽裏面, 這是我們寫的信號觸發的地方:

2、接下來就進入了moc_MainWindow.cpp裏面的代碼,調用了QMetaObject的靜態方法activate:

// 然後進入真正的QMetaObject::activate

我們的例子是Autoconntion模式,所以就會執行下面的代碼進行回調:

我們終於看到了,函數進行了回調到moc_MainWindow.cpp裏面,然後調用對應的槽onClean ;

最終調用到這裏後,打印輸出:"MainWindow::onClean"

最後就是調用完後,會回到onDestory這裏:

注意:如果我們在onClean中進行了對m_testWidget對象的釋放操作(delete m_testWidget),再到onDestory()中 emit clean(); 後面進行訪問成員,那麼一定崩潰,所以要注意。

參考文獻:

1、https://woboq.com/blog/how-qt...

2、Qt5.7源碼

3、自己用C++實現的信號和槽demo:http://note.youdao.com/notesh...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章