Qt信號槽-原理分析(轉(zhuǎn))

一、問題

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

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

  1. moc預(yù)編譯在干嘛
  2. signals和slots關(guān)鍵字產(chǎn)生的理由
  3. 信號槽連接方式有什么區(qū)別
  4. 信號和槽函數(shù)有什么區(qū)別
  5. connect到底干了什么
  6. 信號觸發(fā)原理

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

二、Moc

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

image

Q_OBJECT是一個非常重要的宏,他是Qt實現(xiàn)元編譯系統(tǒng)的一個關(guān)鍵宏,這個宏展開后,里邊包含了很多Qt幫助我們寫的代碼,包括了變量定義、函數(shù)聲明等等,下邊是一個測試例子,是我用moc命令生成的一個moc文件。

分析下面這個幾個變量和函數(shù),將有助于我們更好的理解元編譯系統(tǒng)
1、變量

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

2、Q_OBJECT展開后的函數(shù)聲明
以下5個函數(shù)都是使用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個函數(shù),我們首先需要引入一個Qt元對象,也就是QMetaObject,這個類里邊存儲了父類的源對象、我們當前類描述、函數(shù)描述和qt_static_metacall函數(shù)地址。

a、qt_static_metacall
很重要,根據(jù)函數(shù)索引進行調(diào)用槽函數(shù),這塊需要注意一個很大的細節(jié)問題,這個回調(diào)中,信號和槽都是可以被回調(diào)的,自動生成代碼如下

 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是一個信號聲明,但是卻也可以被回調(diào),這也間接的說明了一個問題,信號是可以當槽函數(shù)一樣使用的。

b、staticMetaObject
構(gòu)造一個QMetaObject對象,傳入當前moc文件的動態(tài)信息

c、metaObject
返回當前QMetaObject,一般而言,虛函數(shù) metaObject() 僅返回類的 staticMetaObject對象。

d、qt_metacast
是否可以進行類型轉(zhuǎn)換,被QObject::inherits直接調(diào)用,用于判斷是否是繼承自某個類。判斷時,需要傳入父類的字符串名稱。

e、qt_metacall
調(diào)用函數(shù)回調(diào),內(nèi)部還是調(diào)用了qt_static_metacall函數(shù),該函數(shù)被異步處理信號時調(diào)用,或者Qt規(guī)定的有一定格式的槽函數(shù)(on_xxx_clicked())觸發(fā),異步調(diào)用代碼如下所示

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、自定義信號
下面這個函數(shù)是我們自己定義的一個信號,moc命令幫我們生成了一個信號函數(shù)實現(xiàn),由此可見,信號其實也是一個函數(shù),只是我們只管寫信號聲明,而信號實現(xiàn)Qt會幫助我們自動生成;槽函數(shù)我們不僅僅需要寫函數(shù)聲明,函數(shù)實現(xiàn)也必須自己寫。

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

這里Qt怎么會知道我們定義了信號呢?這個也是文章開頭我們提出的第2個問題。答案就是signals,當Qt發(fā)現(xiàn)這個標志后,默認我們是在定義信號,它則幫助我們生產(chǎn)了信號的實現(xiàn)體,slots標志是同樣的道理,Qt元系統(tǒng)用來解析槽函數(shù)時用的。
我們在C++文件中添加了編譯器不認識的關(guān)鍵字,這個時候編譯為什么會沒有報錯呢?
因為我們使用了define宏定義,定義了這個關(guān)鍵字

# define signals

三、connect

上面我們分析了moc系統(tǒng)幫助我們生成的moc文件,他是實現(xiàn)信號槽的基礎(chǔ),也是關(guān)鍵所在,這一小節(jié)我們來了解下我們平時使用最多的connect函數(shù),看看他到底干了些什么。

當我們執(zhí)行connect時,實際上他可能像這樣的執(zhí)行流程


image

從這張圖上我們可以看到,connect干的事情并不多,好像就是構(gòu)造了一個Connection對象,然后存儲在了發(fā)送者的內(nèi)存中,具體存儲了哪些內(nèi)容,可以看下面代碼,這是我從Qt源碼中沾出來的部分代碼。

QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection);
c->sender = s;   //發(fā)送者
c->signal_index = signal_index;//信號索引
c->receiver = r;//接收者
c->method_relative = method_index;//槽函數(shù)索引
c->method_offset = method_offset;//槽函數(shù)偏移 主要是區(qū)別于多個信號
c->connectionType = type;//連接類型
c->isSlotObject = false;//是否是槽對象 默認是true
c->argumentTypes.store(types);//參數(shù)類型
c->nextConnectionList = 0;//指向下個連接對象
c->callFunction = callFunction;//靜態(tài)回調(diào)函數(shù),也就是qt_static_metacall

QObjectPrivate::get(s)->addConnection(signal_index, c.data());

上述代碼中我只把關(guān)鍵代碼貼出來了,Qt的源碼實現(xiàn)有很多異常判斷我們這里不需要考慮

發(fā)送者內(nèi)存中存儲結(jié)構(gòu)

class QObjectConnectionListVector : public QVector<QObjectPrivate::ConnectionList>

信號槽連接后在內(nèi)存中已QObjectConnectionListVector對象存儲,這是一個數(shù)組,Qt巧妙的借用了數(shù)組快速訪問指定元素的方式,把信號所在的索引作為下標來索引他連接的Connection對象,眾所周知一個信號可以被多個槽連接,那么我們的的數(shù)組自然而然也就存儲了一個鏈表,用于方便的插入和移除,也就是CommectionList對象。

四、信號觸發(fā)

一切準備就緒,接下來我們看看信號觸發(fā)后,是怎么關(guān)聯(lián)到槽函數(shù)的

Qt為我們提供了5種類型的連接方式,如下

  • Qt::AutoConnection 自動連接,根據(jù)sender和receiver是否在一個線程里來決定使用哪種連接方式,同一個線程使用直連,否則使用隊列連接
  • Qt::DirectConnection 直連
  • Qt::QueuedConnection 隊列連接
  • Qt::BlockingQueuedConnection 阻塞隊列連接,顧名思義,雖然是跨線程的,但是還是希望槽執(zhí)行完之后,才能執(zhí)行信號的下一步代碼
  • Qt::UniqueConnection 唯一連接

一般情況下,我們都使用默認的連接方式,除非一些特殊的需求,我們才會主動指定連接方式。當我們執(zhí)行信號時,函數(shù)的調(diào)用關(guān)系可能會像下面這樣


image
emit testSignal(); 執(zhí)行信號

信號觸發(fā)后,就相當于調(diào)用QMetaObject::activate函數(shù),信號的函數(shù)體是moc幫助我們自動生成的。

下面我們來分析下幾個關(guān)鍵的連接方式,他們都是怎么工作的

1、直連

對于大多數(shù)的開發(fā)工作來說,我們可能都是在同一個線程里進行的,因此直連也是我們使用連接方式最多的一種,直連說白了就是函數(shù)回調(diào)。還記得我們第三小節(jié)講的connect嗎,他構(gòu)造了一個Connection對象,存儲在了發(fā)送者的內(nèi)存中,直連其實就是調(diào)用了我們之前存儲在Connection中的函數(shù)地址。

如下圖所示,是一個直連時,回調(diào)到槽函數(shù)中的一個內(nèi)存堆棧。


image

講connect函數(shù)時,我們分析到,該函數(shù)內(nèi)部其實就是構(gòu)造了一個Connection對象存儲在了發(fā)送者內(nèi)存中,其中有一個變量是isSlotObject,默認是true。當我們使用connect連接信號槽時,該參數(shù)默認就是一個true,但是Qt還提供了了另外一種規(guī)定格式的槽函數(shù),此時isSlotObject就是false啦。

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


image

2、隊列連接

connect連接信號槽時,我們使用Qt::QueuedConnection作為連接類型時,槽函數(shù)的執(zhí)行是通過拋出QMetaCallEvent事件,經(jīng)過Qt的事件循環(huán)達到異步的效果

如下圖所示,是使用隊列連接時,槽函數(shù)的回調(diào)堆棧


image

下面代碼摘自Qt源碼,queued_activate函數(shù)即是處理隊列請求的函數(shù),當我們使用自動連接并且接受者和發(fā)送者不在一個線程時使用隊列連接;或者當我們指定連接方式為隊列時使用隊列連接。

// 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;

五、總結(jié)

講了這么多,Qt信號槽的實現(xiàn)原理其實就是函數(shù)回調(diào),不同的是直連直接回調(diào)、隊列連接使用Qt的事件循環(huán)隔離了一次達到異步,最終還是使用函數(shù)回調(diào)

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

六、推薦閱讀

信號槽5種連接方式: 線程,connect的第五個參數(shù)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容