一祠肥、問題
學(xué)習(xí)Qt有一段時間了,信號槽用的也是666梯皿,可是對信號槽的機制還是一知半解仇箱,總覺著不是那么得勁兒县恕,萬一哪天面試被問到了還說不清楚,那豈不是很尷尬剂桥。最近抽空研究了下Qt的信號和槽進制忠烛,結(jié)果發(fā)現(xiàn)也不是那么難嘛!不管是同步還是異步权逗,說白了都是函數(shù)回調(diào)美尸,只是回調(diào)的地方變了而已
首先,我們先看如下幾個問題斟薇,認真的思考下师坎,從以前的知識儲備中嘗試回答他們,如果說這幾個問題你都很清楚堪滨,那么恭喜你胯陋,你不適合看這篇文章。
- moc預(yù)編譯在干嘛
- signals和slots關(guān)鍵字產(chǎn)生的理由
- 信號槽連接方式有什么區(qū)別
- 信號和槽函數(shù)有什么區(qū)別
- connect到底干了什么
- 信號觸發(fā)原理
下面我們就分模塊來講述下Qt的信號槽袱箱,首先分析下Moc他到底干了什么惶岭,如果沒有他信號槽還能行嗎?接著我們在來分析下最常用的connect函數(shù)犯眠,最后在看下信號執(zhí)行后是怎么觸發(fā)槽函數(shù)的?
二症革、Moc
qt中的moc 全稱是 Meta-Object Compiler筐咧,也就是“元對象編譯器”,當(dāng)我們編譯C++
文件時噪矛,如果類聲明中包含了宏Q_OBJECT量蕊,則會生成另外一個C++源文件,也就是我們經(jīng)惩Оぃ看到的moc_xxx.cpp文件残炮,執(zhí)行流程可能會像這樣。
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鳍烁,這個類里邊存儲了父類的源對象叨襟、我們當(dāng)前類描述、函數(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)搀玖,這也間接的說明了一個問題,信號是可以當(dāng)槽函數(shù)一樣使用的钓账。
b、staticMetaObject
構(gòu)造一個QMetaObject對象絮宁,傳入當(dāng)前moc文件的動態(tài)信息
c梆暮、metaObject
返回當(dāng)前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,當(dāng)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ù)赋焕,看看他到底干了些什么参歹。
當(dāng)我們執(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 唯一連接
一般情況下急前,我們都使用默認的連接方式,除非一些特殊的需求浸踩,我們才會主動指定連接方式叔汁。當(dāng)我們執(zhí)行信號時,函數(shù)的調(diào)用關(guān)系可能會像下面這樣
emit testSignal(); 執(zhí)行信號
信號觸發(fā)后检碗,就相當(dāng)于調(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。當(dāng)我們使用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ù)术陶,當(dāng)我們使用自動連接并且接受者和發(fā)送者不在一個線程時使用隊列連接;或者當(dāng)我們指定連接方式為隊列時使用隊列連接煤痕。
// 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)
- moc預(yù)編譯幫助我們構(gòu)建了信號槽回調(diào)的開頭(信號函數(shù)體)和結(jié)尾(qt_static_metacall回調(diào)函數(shù))巷帝,中間的回調(diào)過程Qt已經(jīng)在QOjbect函數(shù)中實現(xiàn)
- signals和slots就是為了方便moc解析我們的C++文件忌卤,從中解析出信號和槽
- 信號槽總共有5種連接方式,前四種是互斥的楞泼,可以表示為異步和同步驰徊。第五種唯一連接時配合前4種方式使用的
- 信號和槽本質(zhì)上是一樣的,但是對于使用者來說堕阔,信號只需要聲明棍厂,moc幫你實現(xiàn),槽函數(shù)聲明和實現(xiàn)都需要自己寫
- connect方法就是把發(fā)送者超陆、信號牺弹、接受者和槽存儲起來,供后續(xù)執(zhí)行信號時查找
- 信號觸發(fā)就是一系列函數(shù)回調(diào)
六侥猬、推薦閱讀
信號槽5種連接方式: 線程例驹,connect的第五個參數(shù)
moc文件解析:Qt高級——Qt信號槽機制源碼解析