Qt信號槽-原理分析


一、問題

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

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

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

下面我們就分模塊來講述下Qt的信號槽,首先分析下Moc他到底干了什么,如果沒有他信號槽還能行嗎?接着我們在來分析下最常用的connect函數,最后在看下信號執行后是怎么觸發槽函數的?

二、Moc

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

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的事件循環隔離了一次達到異步,最終還是使用函數回調

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

六、推薦閱讀

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

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


如果您覺得文章不錯,不妨給個 打賞,寫作不易,感謝各位的支持。您的支持是我最大的動力,謝謝!!!




很重要--轉載聲明

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。



免責聲明!

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



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