Qt 信號和槽源碼分析

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

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() 是轉換信號與槽的宏。

實現原理:

  • 一、首先咱們搭建好環境,如在Windows系統上:安裝Qt5.7(包括源碼) + VS2013 及 對應的插件,咱們主要是經過VS來進行編譯調試的。
  • 二、咱們寫一個簡單實例,而後進行構建,再把Qt安裝目錄中的QtCored的pdb拷貝到咱們的可執行文件目錄下面,以下圖所示:

clipboard.png

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

clipboard.png

// MainWindow.cpp瀏覽器

clipboard.png

咱們能夠建立一個Qt工程,名稱爲Demo,編寫上面的代碼,進行構建,在VS下能夠把Qt工程導成VS工程,編譯生成,運行結果以下:安全

clipboard.png

點擊中間的按鈕,咱們能夠看到控制檯打印以下信息:服務器

clipboard.png

第一步:基本結構:

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

clipboard.png

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

clipboard.png

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

clipboard.png

經過把QT_MOC_LITERAL這個宏進行替換後,獲得以下數據 :工具

clipboard.png

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

clipboard.png

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

clipboard.png

分析:

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

clipboard.png

預編譯後以下:

clipboard.png

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

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

clipboard.png

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

clipboard.png

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

clipboard.png

clipboard.png

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

咱們根據調試能夠看到QObject::d_ptr->metaObject是空的,因此這樣smeta就是上面這個staticMetaObject變量了。

clipboard.png

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

clipboard.png

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

clipboard.png

clipboard.png

咱們看上面的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爲基數的緣由了,轉換方法以下:

clipboard.png

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

clipboard.png

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

clipboard.png

具體實現以下:

clipboard.png

其中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個整形表示。

  • 五、對掩碼進行檢查。

clipboard.png

// MethodFlags是一個枚舉類型,咱們能夠看到MethodSignal = 0x04, MethodSlot = 0x08;

clipboard.png

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

  • 六、判斷連接類型,默認是Qt::AutoConnection。
enum ConnectionType {

  AutoConnection,

  DirectConnection,

  QueuedConnection,

  BlockingQueuedConnection,

  UniqueConnection =  0x80

};

咱們介紹一些鏈接類型:

  • 一、AutoConnection:自動鏈接:默認的方式,信號發出的線程和糟的對象在一個線程的時候至關於:DirectConnection, 若是是在不一樣線程,則至關於QueuedConnection。
  • 二、DirectConnection:直接鏈接:至關於直接調用槽函數,可是當信號發出的線程和槽的對象再也不一個線程的時候,則槽函數是在發出的信號中執行的。
  • 三、QueuedConnection :隊列鏈接:內部經過postEvent實現的。不是實時調用的,槽函數永遠在槽函數對象所在的線程中執行。若是信號參數是引用類型,則會另外複製一份的。線程安全的。
  • 四、BlockingQueuedConnection:阻塞鏈接:此鏈接方式只能用於信號發出的線程 和 槽函數的對象再也不一個線程中才能用,經過信號量+postEvent實現的,不是實時調用的,槽函數永遠在槽 函數對象所在的線程中執行,可是發出信號後,當前線程會阻塞,等待槽函數執行完畢後才繼續執行。
  • 五、UniqueConnection :防止重複鏈接。若是當前信號和槽已經鏈接過了,就再也不鏈接了。

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

首先,咱們先得了解如下數據結構:

clipboard.png

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

clipboard.png

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

clipboard.png

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

clipboard.png

結構以下:

clipboard.png

clipboard.png

分析:

  • 一、每一個QObject對象都有一個QObjectConnectionListVector結構,這是一個Vector容器,它裏面的基本單元都是ConnectionList類型的數據,ConnectionList的個數與該QObject對象的signal個數相同。每一個ConnectionList對應一個信號,它記錄了鏈接到這個信號上的全部鏈接。前面已經看到ConnectionList的定義中有兩個重要成員:first和last,他們都是Connection 類型的指針,分別指向鏈接到這個信號上的第一個和最後一個鏈接。全部鏈接到這個信號上的鏈接以單向鏈表的方式組織了起來,Connection結構體中的nextConnectionList成員就是用來指向這個鏈表中的下一個鏈接的。
  • 二、同時,每一個QObject對象還有一個senders成員,senders是一個Connection類型的指針,senders自己也是一個鏈表的頭結點,這個鏈表中的全部結點都是鏈接到這個QObject對象上的某個槽的鏈接。不過這個鏈表跟上一段提到的鏈表可不是同一個,雖然他們可能有一些共同結點。
  • 三、每個Connection對象都同時處於兩個鏈表當中。其中一個是以Connection的nextConnectionList成員組織起來的單向鏈表,這個單項鍊表中每一個結點的共同點是,他們都依賴於同一個QObject對象的同一個信號,這個鏈表的頭結點就是這個信號對應的ConnectionList結構中的first;另外一個鏈表是以Connection的next和prev成員組織起來的雙向鏈表,這個雙向鏈表中每一個結點的共同點是,他們的槽都在同一個QObject對象上,這個鏈表的頭結點就是這個Qobject對象的sender。這兩個鏈表會有交叉(共同結點),但他們有不一樣的連接指針,因此不是同一個鏈表。
  • 四、在Connect的時候,就是先new一個Connection對象出來,設置好這個鏈接的信息後,將它分別添加到上面提到的兩個鏈表中;disconnect的時候,就從從這兩個鏈表中將它移除,而後delete掉。而當一個QObject對象被銷燬的時候,它的sender指針指向的那個雙向鏈表中的全部鏈接都會被逐個移除!

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

  • 一、咱們點擊上面的button後,而後調用到onDestory槽裏面, 這是咱們寫的信號觸發的地方:

clipboard.png

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

clipboard.png

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

clipboard.png

咱們的例子是Autoconntion模式,因此就會執行下面的代碼進行回調:

clipboard.png

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

clipboard.png

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

clipboard.png

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

clipboard.png

注意:若是咱們在onClean中進行了對m_testWidget對象的釋放操做(delete m_testWidget),再到onDestory()中 emit clean(); 後面進行訪問成員,那麼必定崩潰,因此要注意。

參考文獻:

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

二、Qt5.7源碼

三、本身用C++實現的信號和槽demo:http://note.youdao.com/notesh...

相關文章
相關標籤/搜索