1. 程式人生 > >從零開始實現訊號槽機制:二

從零開始實現訊號槽機制:二

好了,是時候寫段Qt程式碼看看了,這是一段典型的使用Qt訊號槽的程式碼,因為我們這段程式碼直接寫在main.cpp裡面,所以在最後記得加上#include "main.moc":

#include <iostream>
#include <QApplication>
using namespace std;

class Button : public QObject
{
    Q_OBJECT
public:
    void nowClick(bool b) { emit click(b); }
signals:
    void click(bool);
};

class Tv : public QObject
{
    Q_OBJECT
public:
    Tv(int b, int t) : bootTime(b), offTime(t){}
protected slots:
    void onStateChanged(bool b)
    {
        if ( b == true )
            cout << "Tv is being turned on. bootTime is " << bootTime << endl;
        else
            cout << "Tv is being turned off. offTime is " << offTime << endl;
    }
private:
    int bootTime;
    int offTime;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Button btn;
    Tv tv(10, 20);
    QObject::connect(&btn, SIGNAL(click(bool)), &tv, SLOT(onStateChanged(bool)));
    btn.nowClick(true);

    return a.exec();
}

#include "main.moc"

我們知道Qt原始碼在make之前需要先進行一道qmake,而qmake會呼叫moc.exe這個“元物件編譯器”來對包含Q_OBJECT巨集的檔案進行一個預處理,要弄清Qt的訊號槽如何運作,首先我們得知道signals,slots這種不符合C++規範的東西到底被處理成了什麼鬼,ok,我們在qobjectdefs.h裡面找到它們了:
#define slots
#define signals public // Qt5 中由 protected 改為 public 以支援更多特性
#define emit
#define SLOT(a) "1"#a
#define SIGNAL(a) "2"#a

當然這些定義在某些情況下,比如定義了QT_NO_EMIT時會不同,不過這超出了本文的討論範圍,大家有興趣可以去讀下原始碼。好的,現在我們知道了,“slots”和“emit”根本就是兩個空巨集,而signals僅僅是一個public,這樣看來,Qt中的訊號是個真正的函式無疑,而不是像sigslot中以functor的方式實現。connect函式則是藉助SIGNAL和SLOT兩個巨集為訊號和槽函式添加了一個數字並將其轉化成字串,也就是說,上面的main()中的connect實際等價於:
QObject::connect(&btn, "2click(bool)", &tv, "1onStateChanged(bool)");

這種連結方式是可以通過編譯並得到正確結果的,但我們現在還不能刪掉signals,和slots關鍵字——它們不是定義為空嗎,為什麼不能刪呢?很簡單,signals定義的函式我們根本沒有實現,它只有一個宣告。既然能通過編譯,說明Qt在把signals改為空之前必定還做了些其他的事情。翻看main.moc,我們果然發現了這個訊號函式的實現:
// SIGNAL 0
void Button::click(bool _t1)
{
    void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

Qt不管該訊號的引數型別與引數個數,將其統一轉成了void*並存在陣列中,傳遞給QMetaObject::activate()進行呼叫。到這裡你也許會疑惑了,我們在前面的設計中被引數問題弄到頭大,槽函式收到這堆二進位制資料以後如何知道該用哪種格式來解釋它們呢?

好的,active()這個函式後面再說,我們先弄清楚QMetaObject是個神馬,它是理解Qt訊號槽機制的關鍵所在。貼段官方介紹吧:

QMetaObject類包含Qt物件的元資訊。Qt的元物件系統負責訊號和槽的通訊機制,執行時型別資訊,和Qt的屬性系統。每個QObject的子類將被建立一個QMeteObject例項,並被應用於應用程式中,這個例項儲存了該QObject子類所有的元資訊,通過QObject::metaobject()可以獲取它。

也就是說,基於QMetaObject,我們可以獲取QObject子類物件的類名、父類名、元方法(訊號、槽和其他宣告為INVOKABLE的方法)、列舉、屬性、建構函式等諸多資訊。而QMetaObject中的資料則是來自於moc對原始檔所進行的詞法分析。看看我們main.moc中Qt為我們的Button類生成的整型陣列:

static const uint qt_meta_data_Button[] = {

 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       1,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount

 // signals: name, argc, parameters, tag, flags
       1,    1,   19,    2, 0x06 /* Public */,

 // signals: parameters
    QMetaType::Void, QMetaType::Bool,    2,

       0        // eod
};

// content欄目中的13個整型數表示的資訊已由註釋給出,對於有兩列的資料,第一列表示該類專案的個數,第二列表示這一類專案的描述資訊開始於這個陣列中的哪個位置(索引值)。可以看到Button類包含一個方法資訊(nowClick()非INVOKABLE方法不被記錄),就是我們的訊號了,並且該方法的描述資訊開始於第14個int資料。

// signals註釋下那個“1”即為qt_meta_data_Button[14],註釋寫得更清楚,表明這裡開始記錄的是(訊號方法)資訊,每個方法的描述資訊由5個int型資料組成。分別代表方法名、該方法所需引數的個數、關於引數的描述(表示與引數相關的描述資訊開始於本陣列中的哪個位置,也是個索引)、以及tag和flags。最後,該陣列存放了方法的返回型別、每個引數的型別、以及引數的名稱。也就是說,任何一個可以拿到Button類的父類指標(QObjcet*)的物件都可以清楚地瞭解其signal的所有資訊。

除了這個整形陣列,moc還為我們生成了metaObject(),qt_metacall()等函式,前者用來獲取元物件,後者十分關鍵,我們先將連線建立起來再來看它。

是時候建立連線了

我們現在已經知道,基於元物件系統,Qt可以通過名稱很快地找到對應的方法的索引,然後,我們還需要一個用來管理連線的類,由於Qt中的QObject類即可以作為接收者也可以作為傳送者,因此這個Connection需要同時包含傳送物件與接收物件的指標,以及對應訊號與槽函式的索引。Qt在QObjectPrivate中定義了這個Connection,位於qobject_p.h中:

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(0), 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 { return method_offset + method_relative; }
        void ref() { ref_.ref(); }
        void deref() {
            if (!ref_.deref()) {
                Q_ASSERT(!receiver);
                delete this;
            }
        }
    };

每個Sender需要維護一個QVector,其長度是其signal的個數,QVector內每個單元存放著一個Connection連結串列的頭結點,儲存該訊號的每個連結;而Receiver則相對簡單一些,它直接維護一個Connection連結串列,表示所有連結到它身上的連結。當然,一個QObjcet可能即是Sender又是Receiver,因此一個QObject可能同時在維護這兩個資料結構,並且,如果是該QObjcet內部的訊號槽呼叫,兩個Connection物件將會重疊,貼張國際友人的影象像下面這樣:


我們通常用來建立連線的connect()函式宣告如下(Qt5支援了更多過載型別):

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
                                     const QObject *receiver, const char *method,
                                     Qt::ConnectionType type)

可以看到,Qt在建立訊號槽時不需要函式指標,兩個字元型的函式名即可。連結建立後,當訊號發出,實際呼叫了QMetaObject::activate(),該函式位於qobject.cpp:
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
    int signal_index = signalOffset + local_signal_index;

    /* 第一件要做的事就是快速檢查64位的掩碼. 如果是0,
     * 則可以肯定這個訊號沒有被槽函式連結,可以直接返回,
     * 這意味著發射一個沒有連結到任何槽的訊號是及其快速的 */
    if (!sender->d_func()->isSignalConnected(signal_index))
        return; 

    /* ... 跳過一些Debug和QML鉤子、正常檢測程式碼 ... */

    /* 鎖定互斥物件, 保證對容器的所有操作都是執行緒安全的 */
    QMutexLocker locker(signalSlotLock(sender));

    /* 獲取該signal的ConnectionList.  簡化了一些檢測性程式碼 */
    QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;
    const QObjectPrivate::ConnectionList *list =
        &connectionLists->at(signal_index);

    QObjectPrivate::Connection *c = list->first;
    if (!c) continue;
    // 檢查這段期間有沒有新新增而在發射過程中沒有發射的訊號
    QObjectPrivate::Connection *last = list->last;

    /* 對每個槽函式進行迭代 */
    do {
        if (!c->receiver)
            continue;

        QObject * const receiver = c->receiver;
        const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;

        // 決定該連結是立即傳送還是放入事件佇列
        if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
            || (c->connectionType == Qt::QueuedConnection)) {
            /* 拷貝引數然後放入事件 */
            queued_activate(sender, signal_index, c, argv);
            continue;
        } else if (c->connectionType == Qt::BlockingQueuedConnection) {
            /* ... 跳過 ... */
            continue;
        }

        /* Helper struct that sets the sender() (and reset it backs when it
         * goes out of scope */
        QConnectionSenderSwitcher sw;
        if (receiverInSameThread)
            sw.switchSender(receiver, sender, signal_index);

        const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
        const int method_relative = c->method_relative;
        if (c->isSlotObject) {
            /* ... 跳過....  Qt5-style 連結至函式指標 */
        } else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
            /* 如果我們有一個 callFunction (由moc生成指向 qt_static_metacall的指標)
             * 我們可以直接呼叫它. 我們同樣需要檢查儲存的 metodOffset是否仍然有效
             *  (可能在此之前被析構) */
            locker.unlock(); // 呼叫該函式時我們不能保持鎖定
            callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
            locker.relock();
        } else {
            /* 動態物件的反饋 */
            const int method = method_relative + c->method_offset;
            locker.unlock();
            metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
            locker.relock();
        }

        // 檢查該物件是否未被槽刪除
        if (connectionLists->orphaned) break;
    } while (c != last && (c = c->nextConnectionList) != 0);
}

一路追蹤這裡的callFunction()和metacall(),結果發現它們都呼叫了QObject::qt_metacall(),而在qobjectdefs.h檔案中我們看到:

virtual int qt_metacall(QMetaObject::Call, int, void **);

恩,這是個虛擬函式,也就是說,最後的呼叫都回到了moc為我們建立的那個qt_metacall()函式。

因為我們的Tv類寫得很簡單,所以生成的qt_metacall()也很簡短,qt_metacall()則呼叫了qt_static_metacall()來觸發我們宣告的槽函式onStateChanged():

void Tv::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Tv *_t = static_cast<Tv *>(_o);
        switch (_id) {
        case 0: _t->onStateChanged((*reinterpret_cast< bool(*)>(_a[1]))); break;
        default: ;
        }
    }
}

int Tv::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 1)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 1;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 1)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 1;
    }
    return _id;
}


到這裡應該差不多了,總結一下。我們在上篇博文中實現的sigslot機制已經能夠比較好地實現兩個元件之間的解耦,但是缺點是設計庫時需要針對不同引數數量的訊號與連結需要重複編碼,槽函式必須繼承一個共同的基類等。

而Qt的訊號槽機制建立在其龐大的元物件體系之上,由於其訊號與槽函式的引數型別可以隨時隨地查到,因此在傳參時可以僅僅傳遞一個void*型別的指標,然後通過虛擬函式機制呼叫為被調類寫好的qt_matecall(),就很容易對引數反向解析從而呼叫相應的槽函數了。基本上是以一定的效能損失換來了更高的靈活性,也算是各有千秋吧。Boost.signal現在還沒有用過,到時候接觸下再做個比較相信會更加清晰。(^_^)