Qt訊號槽-原理分析
一、問題
學習Qt有一段時間了,訊號槽用的也是666,可是對訊號槽的機制還是一知半解,總覺著不是那麼得勁兒,萬一哪天面試被問到了還說不清楚,那豈不是很尷尬。最近抽空研究了下Qt的訊號和槽進位制,結果發現也不是那麼難嘛!不管是同步還是非同步,說白了都是函式回撥,只是回撥的地方變了而已
首先,我們先看如下幾個問題,認真的思考下,從以前的知識儲備中嘗試回答他們,如果說這幾個問題你都很清楚,那麼恭喜你,你不適合看這篇文章。
- moc預編譯在幹嘛
- signals和slots關鍵字產生的理由
- 訊號槽連線方式有什麼區別
- 訊號和槽函式有什麼區別
- connect到底幹了什麼
- 訊號觸發原理
下面我們就分模組來講述下Qt的訊號槽,首先分析下Moc他到底幹了什麼,如果沒有他訊號槽還能行嗎?接著我們在來分析下最常用的connect函式,最後在看下訊號執行後是怎麼觸發槽函式的?
二、Moc
qt中的moc全稱是 Meta-Object Compiler,也就是“元物件編譯器”,當我們編譯C++
Q_OBJECT是一個非常重要的巨集,他是Qt實現元編譯系統的一個關鍵巨集,這個巨集展開後,裡邊包含了很多Qt幫助我們寫的程式碼,包括了變數定義、函式宣告等等,下邊是一個測試例子,是我用moc命令生成的一個moc檔案。
分析下面這個幾個變數和函式,將有助於我們更好的理解元編譯系統
1、變數
- static const qt_meta_stringdata_completerTst_t qt_meta_stringdata_completerTst:儲存函式列表
- static const uint qt_meta_data_completerTst:類檔案描述
2、Q_OBJECT展開後的函式宣告
以下5個函式都是使用Q_OBJECT巨集自動生成的
- void xxx::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
- const QMetaObject xxx::staticMetaObject
- const QMetaObject *xxx::metaObject()
- void *xxx::qt_metacast(const char *_clname)
- int xxx::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
為了更好的理解這5個函式,我們首先需要引入一個Qt元物件,也就是QMetaObject,這個類裡邊儲存了父類的源物件、我們當前類描述、函式描述和qt_static_metacall函式地址。
a、qt_static_metacall
很重要,根據函式索引進行呼叫槽函式,這塊需要注意一個很大的細節問題,這個回撥中,訊號和槽都是可以被回撥的,自動生成程式碼如下
if (_c == QMetaObject::InvokeMetaMethod) {
completerTst *_t = static_cast<completerTst *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->lanuch(); break;
case 1: _t->test(); break;
default: ;
}
}
lanch是一個訊號宣告,但是卻也可以被回撥,這也間接的說明了一個問題,訊號是可以當槽函式一樣使用的。
b、staticMetaObject
構造一個QMetaObject物件,傳入當前moc檔案的動態資訊
c、metaObject
返回當前QMetaObject,一般而言,虛擬函式 metaObject() 僅返回類的 staticMetaObject物件。
d、qt_metacast
是否可以進行型別轉換,被QObject::inherits直接呼叫,用於判斷是否是繼承自某個類。判斷時,需要傳入父類的字串名稱。
e、qt_metacall
呼叫函式回撥,內部還是呼叫了qt_static_metacall函式,該函式被非同步處理訊號時呼叫,或者Qt規定的有一定格式的槽函式(on_xxx_clicked())觸發,非同步呼叫程式碼如下所示
void QMetaCallEvent::placeMetaCall(QObject *object)
{
if (slotObj_) {
slotObj_->call(object, args_);
} else if (callFunction_ && method_offset_ <= object->metaObject()->methodOffset()) {
callFunction_(object, QMetaObject::InvokeMetaMethod, method_relative_, args_);
} else {
QMetaObject::metacall(object, QMetaObject::InvokeMetaMethod, method_offset_ + method_relative_, args_);
}
}
3、自定義訊號
下面這個函式是我們自己定義的一個訊號,moc命令幫我們生成了一個訊號函式實現,由此可見,訊號其實也是一個函式,只是我們只管寫訊號宣告,而訊號實現Qt會幫助我們自動生成;槽函式我們不僅僅需要寫函式宣告,函式實現也必須自己寫。
- void xxx::lanuch():自定義訊號
這裡Qt怎麼會知道我們定義了訊號呢?這個也是文章開頭我們提出的第2個問題。答案就是signals,當Qt發現這個標誌後,預設我們是在定義訊號,它則幫助我們生產了訊號的實現體,slots標誌是同樣的道理,Qt元系統用來解析槽函式時用的。
我們在C++檔案中添加了編譯器不認識的關鍵字,這個時候編譯為什麼會沒有報錯呢?
因為我們使用了define巨集定義,定義了這個關鍵字
# define signals
三、connect
上面我們分析了moc系統幫助我們生成的moc檔案,他是實現訊號槽的基礎,也是關鍵所在,這一小節我們來了解下我們平時使用最多的connect函式,看看他到底幹了些什麼。
當我們執行connect時,實際上他可能像這樣的執行流程
從這張圖上我們可以看到,connect乾的事情並不多,好像就是構造了一個Connection物件,然後儲存在了傳送者的記憶體中,具體儲存了哪些內容,可以看下面程式碼,這是我從Qt原始碼中沾出來的部分程式碼。
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;//是否是槽物件 預設是true
c->argumentTypes.store(types);//引數型別
c->nextConnectionList = 0;//指向下個連線物件
c->callFunction = callFunction;//靜態回撥函式,也就是qt_static_metacall
QObjectPrivate::get(s)->addConnection(signal_index, c.data());
上述程式碼中我只把關鍵程式碼貼出來了,Qt的原始碼實現有很多異常判斷我們這裡不需要考慮
傳送者記憶體中儲存結構
class QObjectConnectionListVector : public QVector<QObjectPrivate::ConnectionList>
訊號槽連線後在記憶體中已QObjectConnectionListVector物件儲存,這是一個數組,Qt巧妙的借用了陣列快速訪問指定元素的方式,把訊號所在的索引作為下標來索引他連線的Connection物件,眾所周知一個訊號可以被多個槽連線,那麼我們的的陣列自然而然也就儲存了一個連結串列,用於方便的插入和移除,也就是CommectionList物件。
四、訊號觸發
一切準備就緒,接下來我們看看訊號觸發後,是怎麼關聯到槽函式的
Qt為我們提供了5種類型的連線方式,如下
- Qt::AutoConnection 自動連線,根據sender和receiver是否在一個執行緒裡來決定使用哪種連線方式,同一個執行緒使用直連,否則使用佇列連線
- Qt::DirectConnection 直連
- Qt::QueuedConnection 佇列連線
- Qt::BlockingQueuedConnection 阻塞佇列連線,顧名思義,雖然是跨執行緒的,但是還是希望槽執行完之後,才能執行訊號的下一步程式碼
- Qt::UniqueConnection 唯一連線
一般情況下,我們都使用預設的連線方式,除非一些特殊的需求,我們才會主動指定連線方式。當我們執行訊號時,函式的呼叫關係可能會像下面這樣
emit testSignal(); 執行訊號
訊號觸發後,就相當於呼叫QMetaObject::activate函式,訊號的函式體是moc幫助我們自動生成的。
下面我們來分析下幾個關鍵的連線方式,他們都是怎麼工作的
1、直連
對於大多數的開發工作來說,我們可能都是在同一個執行緒裡進行的,因此直連也是我們使用連線方式最多的一種,直連說白了就是函式回撥。還記得我們第三小節講的connect嗎,他構造了一個Connection物件,儲存在了傳送者的記憶體中,直連其實就是呼叫了我們之前儲存在Connection中的函式地址。
如下圖所示,是一個直連時,回撥到槽函式中的一個記憶體堆疊。
講connect函式時,我們分析到,該函式內部其實就是構造了一個Connection物件儲存在了傳送者記憶體中,其中有一個變數是isSlotObject,預設是true。當我們使用connect連線訊號槽時,該引數預設就是一個true,但是Qt還提供了了另外一種規定格式的槽函式,此時isSlotObject就是false啦。
如下圖所示,這是一個使用Qt規定格式的槽函式。格式:on_objectname_clicked();。
2、佇列連線
connect連線訊號槽時,我們使用Qt::QueuedConnection作為連線型別時,槽函式的執行是通過丟擲QMetaCallEvent事件,經過Qt的事件迴圈達到非同步的效果
如下圖所示,是使用佇列連線時,槽函式的回撥堆疊
下面程式碼摘自Qt原始碼,queued_activate函式即是處理佇列請求的函式,當我們使用自動連線並且接受者和傳送者不在一個執行緒時使用佇列連線;或者當我們指定連線方式為佇列時使用佇列連線。
// 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;
五、總結
講了這麼多,Qt訊號槽的實現原理其實就是函式回撥,不同的是直連直接回調、佇列連線使用Qt的事件迴圈隔離了一次達到非同步,最終還是使用函式回撥
- moc預編譯幫助我們構建了訊號槽回撥的開頭(訊號函式體)和結尾(qt_static_metacall回撥函式),中間的回撥過程Qt已經在QOjbect函式中實現
- signals和slots就是為了方便moc解析我們的C++檔案,從中解析出訊號和槽
- 訊號槽總共有5種連線方式,前四種是互斥的,可以表示為非同步和同步。第五種唯一連線時配合前4種方式使用的
- 訊號和槽本質上是一樣的,但是對於使用者來說,訊號只需要宣告,moc幫你實現,槽函式宣告和實現都需要自己寫
- connect方法就是把傳送者、訊號、接受者和槽儲存起來,供後續執行訊號時查詢
- 訊號觸發就是一系列函式回撥
六、推薦閱讀
最簡化訊號槽:QT學習——Qt訊號與槽實現原理
moc檔案解析:Qt高階——Qt訊號槽機制原始碼解析