Qt信號槽-原理分析

1、問題

學習Qt有一段時間了,信號槽用的也是666,但是對信號槽的機制仍是隻知其一;不知其二,總覺着不是那麼得勁兒,萬一哪天面試被問到了還說不清楚,那豈不是很尷尬。最近抽空研究了下Qt的信號和槽進制,結果發現也不是那麼難嘛!不論是同步仍是異步,說白了都是函數回調,只是回調的地方變了而已面試

首先,咱們先看以下幾個問題,認真的思考下,從之前的知識儲備中嘗試回答他們,若是說這幾個問題你都很清楚,那麼恭喜你,你不適合看這篇文章。數組

  1. moc預編譯在幹嗎
  2. signals和slots關鍵字產生的理由
  3. 信號槽鏈接方式有什麼區別
  4. 信號和槽函數有什麼區別
  5. connect到底幹了什麼
  6. 信號觸發原理

下面咱們就分模塊來說述下Qt的信號槽,首先分析下Moc他到底幹了什麼,若是沒有他信號槽還能行嗎?接着咱們在來分析下最經常使用的connect函數,最後在看下信號執行後是怎麼觸發槽函數的?異步

2、Moc

qt中的moc 全稱是 Meta-Object Compiler,也就是「元對象編譯器」,當咱們編譯C++
文件時,若是類聲明中包含了宏Q_OBJECT,則會生成另一個C++源文件,也就是咱們常常看到的moc_xxx.cpp文件,執行流程可能會像這樣。

函數







Q_OBJECT是一個很是重要的宏,他是Qt實現元編譯系統的一個關鍵宏,這個宏展開後,裏邊包含了不少Qt幫助咱們寫的代碼,包括了變量定義、函數聲明等等,下邊是一個測試例子,是我用moc命令生成的一個moc文件。

分析下面這個幾個變量和函數,將有助於咱們更好的理解元編譯系統學習

一、變量

- static const qt_meta_stringdata_completerTst_t qt_meta_stringdata_completerTst:存儲函數列表
- static const uint qt_meta_data_completerTst:類文件描述

二、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函數地址。ui

a、qt_static_metacall

很重要,根據函數索引進行調用槽函數,這塊須要注意一個很大的細節問題,這個回調中,信號和槽都是能夠被回調的,自動生成代碼以下this

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是一個信號聲明,可是卻也能夠被回調,這也間接的說明了一個問題,信號是能夠當槽函數同樣使用的。.net

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_);
    }
}

三、自定義信號

下面這個函數是咱們本身定義的一個信號,moc命令幫咱們生成了一個信號函數實現,因而可知,信號其實也是一個函數,只是咱們只管寫信號聲明,而信號實現Qt會幫助咱們自動生成;槽函數咱們不只僅須要寫函數聲明,函數實現也必須本身寫。

- void xxx::lanuch():自定義信號

這裏Qt怎麼會知道咱們定義了信號呢?這個也是文章開頭咱們提出的第2個問題。答案就是signals,當Qt發現這個標誌後,默認咱們是在定義信號,它則幫助咱們生產了信號的實現體,slots標誌是一樣的道理,Qt元系統用來解析槽函數時用的。

咱們在C++文件中添加了編譯器不認識的關鍵字,這個時候編譯爲何會沒有報錯呢?

由於咱們使用了define宏定義,定義了這個關鍵字

# define signals

3、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對象。

4、信號觸發

一切準備就緒,接下來咱們看看信號觸發後,是怎麼關聯到槽函數的

Qt爲咱們提供了5種類型的鏈接方式,以下

  • Qt::AutoConnection 自動鏈接,根據sender和receiver是否在一個線程裏來決定使用哪一種鏈接方式,同一個線程使用直連,不然使用隊列鏈接
  • Qt::DirectConnection 直連
  • Qt::QueuedConnection 隊列鏈接
  • Qt::BlockingQueuedConnection 阻塞隊列鏈接,顧名思義,雖然是跨線程的,可是仍是但願槽執行完以後,才能執行信號的下一步代碼
  • Qt::UniqueConnection 惟一鏈接

通常狀況下,咱們都使用默認的鏈接方式,除非一些特殊的需求,咱們纔會主動指定鏈接方式。當咱們執行信號時,函數的調用關係可能會像下面這樣






emit testSignal(); 執行信號

信號觸發後,就至關於調用QMetaObject::activate函數,信號的函數體是moc幫助咱們自動生成的。

下面咱們來分析下幾個關鍵的鏈接方式,他們都是怎麼工做的

一、直連

對於大多數的開發工做來講,咱們可能都是在同一個線程裏進行的,所以直連也是咱們使用鏈接方式最多的一種,直連說白了就是函數回調。還記得咱們第三小節講的connect嗎,他構造了一個Connection對象,存儲在了發送者的內存中,直連其實就是調用了咱們以前存儲在Connection中的函數地址。

以下圖所示,是一個直連時,回調到槽函數中的一個內存堆棧。

講connect函數時,咱們分析到,該函數內部其實就是構造了一個Connection對象存儲在了發送者內存中,其中有一個變量是isSlotObject,默認是true。當咱們使用connect鏈接信號槽時,該參數默認就是一個true,可是Qt還提供了了另一種規定格式的槽函數,此時isSlotObject就是false啦。

以下圖所示,這是一個使用Qt規定格式的槽函數。格式:on_objectname_clicked();。

二、隊列鏈接

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;

5、總結

講了這麼多,Qt信號槽的實現原理其實就是函數回調,不一樣的是直連直接回調、隊列鏈接使用Qt的事件循環隔離了一次達到異步,最終仍是使用函數回調

  1. moc預編譯幫助咱們構建了信號槽回調的開頭(信號函數體)和結尾(qt_static_metacall回調函數),中間的回調過程Qt已經在QOjbect函數中實現
  2. signals和slots就是爲了方便moc解析咱們的C++文件,從中解析出信號和槽
  3. 信號槽總共有5種鏈接方式,前四種是互斥的,能夠表示爲異步和同步。第五種惟一鏈接時配合前4種方式使用的
  4. 信號和槽本質上是同樣的,可是對於使用者來講,信號只須要聲明,moc幫你實現,槽函數聲明和實現都須要本身寫
  5. connect方法就是把發送者、信號、接受者和槽存儲起來,供後續執行信號時查找
  6. 信號觸發就是一系列函數回調

6、推薦閱讀

最簡化信號槽:QT學習——Qt信號與槽實現原理

moc文件解析:Qt高級——Qt信號槽機制源碼解析




轉載聲明:本站文章無特別說明,皆爲原創,版權全部,轉載請註明:朝十晚八 or Twowords

相關文章
相關標籤/搜索