Qt 信號(hào)和槽底層原理源碼解析筆記

1. 底層數(shù)據(jù)結(jié)構(gòu)——建立連接時(shí)建立的什么宦赠。

讀者可先大致瀏覽一下qobject_p.cpp中添加連接的實(shí)現(xiàn)锻拘,回頭再細(xì)看:

void QObjectPrivate::addConnection(int signal, Connection *c)
{
    /*
    * 中心數(shù)據(jù)結(jié)構(gòu)是二維廣義表
    * 不妨看作一個(gè)二維數(shù)組:
    *  A B C D E...
    * 0
    * 1
    * 2
    * 3
    * 4
    * ...
    * 縱向序號(hào)0123..為signal序號(hào),每一個(gè)signal對(duì)應(yīng)一個(gè)connectionList(無s)單鏈表下愈。
    * 所有conectionList 存放在類的 connectionLists這個(gè)向量中纽绍。
    * 橫向序號(hào)為接收者reciver,相應(yīng)每一列為該reciver對(duì)應(yīng)接收signal的connection節(jié)點(diǎn)(有的話才分配,順序不一定按0123)
    * 每列組成相應(yīng)縱向單鏈表senders(含義為該reciver的senders)势似。
    * 每個(gè)對(duì)象自己保存著自己的senders單鏈表
    * 該二維表為全局?jǐn)?shù)據(jù)結(jié)構(gòu)拌夏,包含以signal索引的行鏈表connectionLists和每個(gè)對(duì)象自己保存的相應(yīng)senders鏈表
    * 
    */
    Q_ASSERT(c->sender == q_ptr);
    if (!connectionLists)
        connectionLists = new QObjectConnectionListVector();
    if (signal >= connectionLists->count())
        connectionLists->resize(signal + 1);

    ConnectionList &connectionList = (*connectionLists)[signal];
    if (connectionList.last) {
        connectionList.last->nextConnectionList = c;
    } else {
        connectionList.first = c;
    }
    connectionList.last = c;

    cleanConnectionLists();
    /*
    * 插入節(jié)點(diǎn)的技巧:
    * prev為二級(jí)指針僧著,指向本對(duì)象的senders列表頭的地址addr(addr中保存的是表頭節(jié)點(diǎn)的地址)
    * senders表本身為一個(gè)單鏈表。
    * 但鏈表中每一個(gè)節(jié)點(diǎn)的prev都指向表頭地址addr障簿,即訪問某一個(gè)節(jié)點(diǎn)盹愚,也可以繼續(xù)訪問整個(gè)senders表。
    * 更新senders表時(shí)卷谈,直接將c插到原表頭杯拐,原表頭變?yōu)閏的next。addr內(nèi)容變?yōu)閏的地址世蔗,但addr本身地址不變端逼。
    */
    c->prev = &(QObjectPrivate::get(c->receiver)->senders);
    c->next = *c->prev;
    *c->prev = c;
    if (c->next)
        c->next->prev = &c->next;

    if (signal < 0) {
        connectedSignals[0] = connectedSignals[1] = ~0;
    } else if (signal < (int)sizeof(connectedSignals) * 8) {
        connectedSignals[signal >> 5] |= (1 << (signal & 0x1f));
    }
}

先看看傳入?yún)?shù):
傳入?yún)?shù)signal就是所謂的信號(hào),是一個(gè)整數(shù)污淋。
Connection 類的定義顶滩,在qobject_p.h中。
它是在QObjectPrivate類中定義的一個(gè)結(jié)構(gòu)體:

struct Connection
    {
        QObject *sender;
        QObject *receiver;
        union {
            StaticMetaCallFunction callFunction;
            QtPrivate::QSlotObjectBase *slotObj;
        };
        // The next pointer for the singly-linked ConnectionList
        Connection *nextConnectionList;
        //senders linked list
        Connection *next;
        Connection **prev;
        QAtomicPointer<const int> argumentTypes;
        QAtomicInt ref_;
        ushort method_offset;
        ushort method_relative;
        uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
        ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
        ushort isSlotObject : 1;
        ushort ownArgumentTypes : 1;
        Connection() : nextConnectionList(nullptr), ref_(2), ownArgumentTypes(true) {
            //ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection
        }
        ~Connection();
        int method() const { Q_ASSERT(!isSlotObject); return method_offset + method_relative; }
        void ref() { ref_.ref(); }
        void deref() {
            if (!ref_.deref()) {
                Q_ASSERT(!receiver);
                delete this;
            }
        }
    };

可以看到寸爆,與鏈表相關(guān)的有三個(gè)指針礁鲁,其中prev是個(gè)二級(jí)指針。為什么這么用赁豆,在addConnection函數(shù)我寫的注釋里可以看到一些解釋仅醇。其他的結(jié)構(gòu)體參數(shù)在這里不作深究,有些也可以見名知意魔种。

采用二維表的目的有二:

  • 能根據(jù)發(fā)出的信號(hào)找到所有接受者
  • 能將某一個(gè)接受者接收的所有信號(hào)統(tǒng)一管理

采用鏈表的目的析二,自然就是方便動(dòng)態(tài)管理了。

既然知道了建立的是什么數(shù)據(jù)結(jié)構(gòu)节预,那么下一步就是理清是怎么建立的叶摄。
另外,我們都知道Qt中基本上所有的類都繼承于QObject這個(gè)類安拟,這個(gè)類包含些什么蛤吓?為什么連接的數(shù)據(jù)結(jié)構(gòu)是存在QObjectPrivate這個(gè)類中?QObjectPrivate和QObject是什么關(guān)系糠赦?為什么要這樣定義会傲?這是我們最后要討論的問題。

2. 從信號(hào)和槽的定義到整數(shù)的轉(zhuǎn)換

(1) 神奇的關(guān)鍵字

再看信號(hào)和槽的基本定義和使用方式拙泽。
一個(gè)簡(jiǎn)單例子:
定義一個(gè) Counter類

class Counter : public QObject
{
    Q_OBJECT
    int m_value;
public:
    int value() const { return m_value; }
public slots: //使用public slots聲明槽函數(shù)
    void setValue(int value);
signals: //使用signal聲明信號(hào)唆铐,也是函數(shù)
    void valueChanged(int newValue);
};

信號(hào)的發(fā)射方式。在某處發(fā)射信號(hào)的寫法:

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        //使用emit關(guān)鍵字發(fā)射信號(hào)奔滑,附帶參數(shù)艾岂。
        emit valueChanged(value); 
    }
}

有了信號(hào)和槽,還需要將它們建立連接關(guān)系:

Counter a, b;
  QObject::connect(&a, SIGNAL(valueChanged(int)),
                   &b, SLOT(setValue(int)));

//可省略QObject::默認(rèn)的是同一個(gè)函數(shù)
// 信號(hào)和對(duì)應(yīng)的槽朋其,參數(shù)要一致
connect(&a, &a->valueChanged,&b, b->setValue);

首先來看這幾個(gè)Qt中特有的關(guān)鍵字的含義:slots,signals,emit
no-keywords.h中:

#define signals Q_SIGNALS
#define slots Q_SLOTS
#define emit Q_EMIT

具體地:

# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)   //主要有個(gè)public
//進(jìn)一步可見:
# define QT_ANNOTATE_ACCESS_SPECIFIER(x)

#define Q_EMIT  //(空)

簡(jiǎn)單來說王浴,emit就只是個(gè)寫程序用到的邏輯關(guān)鍵字脆炎,起提示作用。
而signals除了public關(guān)鍵字聲明為公有氓辣,其他部分就只在預(yù)處理過程中起作用秒裕。
slots就是只有預(yù)處理作用。

由此可見钞啸,定義的信號(hào)和槽函數(shù)几蜻, 最終變成了整數(shù),而定義的特殊性体斩,僅在于多一個(gè)預(yù)處理特征(包括#define xx本身)梭稚。那么可以推測(cè),是使用某種預(yù)處理機(jī)制絮吵,進(jìn)行了轉(zhuǎn)化弧烤。那么這是什么機(jī)制?如何運(yùn)作的蹬敲?

(2) Qt 的MOC(the Meta Object Compiler)預(yù)處理器

文檔翻譯時(shí)間:

Qt信號(hào)/槽和屬性系統(tǒng)基于在運(yùn)行時(shí)內(nèi)省對(duì)象的能力暇昂。內(nèi)省意味著能夠列出對(duì)象的方法和屬性,并具有關(guān)于它們的各種信息伴嗡,例如它們的參數(shù)類型急波。
C ++本身不提供內(nèi)省支持,因此Qt附帶了一個(gè)提供它的工具瘪校。該工具是MOC幔崖。它是一個(gè)代碼生成器(而不是像某些人所說的預(yù)處理器)。它解析頭文件并生成一個(gè)額外的C ++文件渣淤,該文件與程序的其余部分一起編譯。生成的C ++文件包含內(nèi)省所需的所有信息吉嫩。

引用說那不是預(yù)處理价认,但是我仍然認(rèn)為,既然需要用到了宏定義自娩,先對(duì)代碼進(jìn)行一遍處理(生成相關(guān)代碼也是一種處理)用踩,仍然可以看作是預(yù)處理,只不過是全部預(yù)處理的一部分忙迁。

再來看每個(gè)基于QObject類都需要聲明的一個(gè)宏Q_OBJECT:

#define Q_OBJECT \
public: \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS /* translations helper */ \
private: \
    Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);

Q_OBJECT定義了一堆函數(shù)和一個(gè)靜態(tài)QMetaObject這些函數(shù)在MOC生成的文件中實(shí)現(xiàn)脐彩。

簡(jiǎn)單說,就是MOC根據(jù)代碼的關(guān)鍵字姊扔,自動(dòng)提取出信號(hào)和槽惠奸,并進(jìn)行處理,生成了相應(yīng)的cpp文件恰梢,相關(guān)要使用的函數(shù)即由Q_OBJECT定義佛南,也生成在相應(yīng)cpp文件中梗掰,隨整個(gè)工程一同進(jìn)行編譯鏈接。

對(duì)于老版本的信號(hào)和槽:

Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)
#else
# define SLOT(a)     "1"#a
# define SIGNAL(a)   "2"#a
#endif

這些宏只是使用預(yù)處理器將參數(shù)轉(zhuǎn)換為字符串嗅回,并在前面添加代碼及穗。在debug模式下,如果信號(hào)連接不起作用绵载,我們還會(huì)使用文件位置注釋字符串以顯示警告消息埂陆。這是以兼容的方式在Qt 4.5中添加的。為了知道哪些字符串具有行信息娃豹,我們使用qFlagLocation焚虱,它將在具有兩個(gè)條目的表中注冊(cè)字符串地址。

也即直接將函數(shù)名翻譯成字符串培愁,在前面加1或2字符著摔,所謂的信號(hào)和槽,也就是相應(yīng)的字符串定续。另外只需要處理參數(shù)傳遞問題谍咆。

新版用法中,傳遞的也是函數(shù)名私股,但從方式上看并沒有轉(zhuǎn)換成字符串摹察,而是直接傳遞函數(shù)指針〕ǎ總之供嚎,就是兩種區(qū)分名稱的方法而以。函數(shù)指針方式能方便編輯器和編譯器檢查語法錯(cuò)誤峭状,使用更安全克滴。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市优床,隨后出現(xiàn)的幾起案子劝赔,更是在濱河造成了極大的恐慌,老刑警劉巖胆敞,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件着帽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡移层,警方通過查閱死者的電腦和手機(jī)仍翰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來观话,“玉大人予借,你說我怎么就攤上這事。” “怎么了蕾羊?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵喧笔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我龟再,道長(zhǎng)书闸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任利凑,我火速辦了婚禮浆劲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘哀澈。我一直安慰自己牌借,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布割按。 她就那樣靜靜地躺著膨报,像睡著了一般。 火紅的嫁衣襯著肌膚如雪适荣。 梳的紋絲不亂的頭發(fā)上现柠,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音弛矛,去河邊找鬼够吩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛丈氓,可吹牛的內(nèi)容都是我干的周循。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼万俗,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼湾笛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起闰歪,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤嚎研,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后课竣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡置媳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年于樟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拇囊。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迂曲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寥袭,到底是詐尸還是另有隱情路捧,我是刑警寧澤关霸,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站杰扫,受9級(jí)特大地震影響队寇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜章姓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一佳遣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凡伊,春花似錦零渐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至银还,卻和暖如春风宁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背见剩。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工杀糯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苍苞。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓固翰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親羹呵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骂际,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容