Qt的信號和槽是如何工作的


 

用Qt做過開發的朋友,不知道是否曾為下面這些問題疑惑過:
我們知道Qt是基於C++的,Qt寫的代碼最終還是要由C++編譯器來編譯,但是我們的Qt代碼中有很多C++里沒有的關鍵字,比如slots\signals\Q_OBJECT等,為什么C++編譯器會放過他們呢?
Qt的槽函數跟普通的成員函數有何區別?一個信號發出后,與之關聯的槽函數是什么時候由誰來調用的?
Qt的信號定義跟函數定義有相同的形式,那信號跟普通函數有何區別?一個信號被emit時我們的程序到底在干什么?
信號和槽的連接是怎么建立的?又是如何斷開的?
本篇博文里對這些問題作了詳盡的解釋,初學Qt或者用了很長時間Qt卻很少思考過這些問題的朋友,可以在下面的段落中找到答案。

下文出自- original:http://woboq.com/blog/how-qt-signals-slots-work.html

譯:NewThinker_Jiwey NewThinker_wei @CSDN

 
 
【譯】Qt的信號-槽機制是如何工作的
 
(譯注:如果閱讀過程中有感覺疑惑的地方,可以參考一下本文的原文-譯文對照版: http://blog.csdn.net/newthinker_wei/article/details/22701695
 

很多人都知道Qt的信號和槽機制,但它究竟是如何工作的呢?在本篇博文中,我們會探索QObject 和 QMetaObject 內部的一些具體實現,揭開信號-槽的神秘面紗。

在本篇博文中,我會貼出部分Qt5的源碼,有時候會根據需要改變一下格式並做一些簡化。

 

首先,我們看一個官方例程,借此回顧一下信號和槽的使用方法。

頭文件是這樣的:

***********************************************

class Counter :publicQObject
{
    Q_OBJECT
    int m_value;
public:
    int value()const {returnm_value; }
public slots:
    void setValue(intvalue);
signals:
    void valueChanged(intnewValue);
};

***********************************************



 

在源文件的某個地方,可以找到setValue()函數的實現:

***********************************************

voidCounter::setValue(intvalue)

{

   if (value !=m_value){

       m_value =value; emitvalueChanged(value);

    }

}

***********************************************

 

 

 

然后開發者可以像下面這樣使用Counter的對象:

***********************************************

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

  a.setValue(12); // a.value() == 12, b.value() == 12

***********************************************

這是從1992年Qt最初階段開始就沿用下來而幾乎沒有變過的原始語法。

雖然基本的API並沒有怎么變過,但它的實現方式卻變了幾次。很多新特性被添加了進來,底層的實現也發生了很多變化。不過這里面並沒有什么神奇的難以理解的東西,本篇博文會向你展示這究竟是如何工作的,


MOC,元對象編譯器

 

Qt的信號/槽和屬性系統都基於其能在運行時刻對對象進行實時考察的功能。實時考察意味着即使在運行過程中也可以列出一個對象有哪些方法(成員函數)和屬性,以及關於它們的各種信息(比如參數類型)。如果沒有實時考察這個功能,QtScript 和 QML 就基本不可能實現了。

C++本身不提供對實時考察的支持,所以Qt就推出了一個工具來提供這個支持。這個工具就是MOC。注意,它是一個代碼生成器,而不是很多人說的“預編譯器”。

MOC會解析頭文件並為每一個含有Q_OBJECT宏的頭文件生成一個對應的C++文件(這個文件會跟工程中的其他代碼一塊參與編譯)。這個生成的C++文件包含了實時考察功能所需的全部信息(文件一般被命名為moc_HeaderName。cpp)。

因為這個額外的代碼生成器,Qt有時對語言會有很嚴格的要求。 這里我就讓這篇Qt文檔來解釋這個嚴格的要求。代碼生成器沒有什么錯誤,MOC起到了很大的作用。

幾個神奇的宏

你能看出這幾個關鍵字並不是標准C++的關鍵字嗎?signals, slots, Q_OBJECT, emit, SIGNAL, SLOT. 這些都是Qt對C++的擴展。這幾個關鍵字其實都是很簡單的宏定義而已,在qobjectdefs.h 頭文件中可以找到他們的定義。

 

[cpp]  view plain  copy
 
  1. #define signals public  
  2. #define slots /* nothing */  

 

 

 

 

 

 

 

 

 

 

沒錯,信號和槽都是很簡單的功能:編譯器會將他們與其他任何宏一樣處理。不過這些宏還有一個特殊的作用:MOC會發現他們。

Signals在Qt4之前都是protected類型的,他們在Qt5中變為了public,這樣就可以使用一些新的語法了。

[cpp]  view plain  copy
 
  1. #define Q_OBJECT \  
  2. public: \  
  3.     static const QMetaObject staticMetaObject; \  
  4.     virtual const QMetaObject *metaObject() const; \  
  5.     virtual void *qt_metacast(const char *); \  
  6.     virtual int qt_metacall(QMetaObject::Call, int, void **); \  
  7.     QT_TR_FUNCTIONS /* translations helper */ \  
  8. private: \  
  9.     Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);  
  10.   
  11.   
  12. Q_OBJECT defines a bunch of functions and a static QMetaObject Those functions are implemented in the file generated by MOC.  
  13. #define emit /* nothing */  

 

emit是個空的宏定義,而且MOC也不會對它進行解析。也就是,emit完全是可有可無的,他沒有任何意義(除了對開發者有提示作用)。

[cpp]  view plain  copy
 
  1. Q_CORE_EXPORT const char *qFlagLocation(const char *method);  
  2. #ifndef QT_NO_DEBUG  
  3. # define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)  
  4. # define SLOT(a)     qFlagLocation("1"#a QLOCATION)  
  5. # define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)  
  6. #else  
  7. # define SLOT(a)     "1"#a  
  8. # define SIGNAL(a)   "2"#a  
  9. #endif  


(譯注:對於#define的一些高級用法,參見我整理的一片文章:http://blog.csdn.net/newthinker_wei/article/details/8893407

上面這些宏,會利用預編譯器將一些參數轉化成字符串,並且在前面添加上編碼。

在調試模式中,如果singnal的連接出現問題,我們提示警告信息的時候還會注明對應的文件位置。這是在Qt4.5之后以兼容方式添加進來的功能。為了知道代碼對應的行信息,我們可以用qFlagLocation ,它會將對應代碼的地址信息注冊到一個有兩個入口的表里。

MOC生成的代碼

我們現在就來看看Qt5的moc生成的部分代碼。

The QMetaObject

***********************************************

const QMetaObject Counter::staticMetaObject = {
    { &QObject::staticMetaObject,qt_meta_stringdata_Counter.data,
      qt_meta_data_Counterqt_static_metacall,0,0}
};


const QMetaObject *Counter::metaObject()const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

*********************************************** 

我們在這里可以看到Counter::metaObject()和 Counter::staticMetaObject 的實現。他們都是在Q_OBJECT宏中被聲明的。

QObject::d_ptr->metaObject 只被用於動態元對象(QML對象),隨意通常虛函數metaObject()只是返回類的staticMetaObject 。

staticMetaObject被創建為只讀數據。qobjectdefs.h 文件中QMetaObject的定義如下:

 

[cpp]  view plain  copy
 
  1. struct QMetaObject  
  2. {  
  3.     /* ... Skiped all the public functions ... */  
  4.   
  5.     enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };  
  6.   
  7.     struct { // private data  
  8.         const QMetaObject *superdata;  
  9.         const QByteArrayData *stringdata;  
  10.         const uint *data;  
  11.         typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);  
  12.         StaticMetacallFunction static_metacall;  
  13.         const QMetaObject **relatedMetaObjects;  
  14.         void *extradata; //reserved for future use  
  15.     } d;  
  16. };  

 

 

 

 

 

 

 

 

 

 

代碼中用的d是為了表明那些數據都本應為私有的。然而他們並沒有成為私有的是為了保持它為POD和允許靜態初始化。(譯注:在C++中,我們把傳統的C風格的struct叫做POD(Plain Old Data),字面意思古老的普通的結構體)。

這里會用父類對象的元對象(此處就是指QObject::staticMetaObject )來初始化QMetaObject的superdata,而它的stringdata 和 data 這兩個成員則會用之后要講到的數據初始化。static_metacall是一個被初始化為 Counter::qt_static_metacall的函數指針。

實時考察功能用到的數據表

首先,我們來分析 QMetaObject的這個整型數組:

***********************************************

static const uint qt_meta_data_Counter[] = {

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

// signals: name, argc, parameters, tag, flags

       1,    1,   24,    20x05,

// slots: name, argc, parameters, tag, flags
       4,    1,   27,    20x0a,

// signals: parameters
    QMetaType::Void,QMetaType::Int,   3,

// slots: parameters
    QMetaType::Void,QMetaType::Int,   5,

       0        // eod
};

***********************************************

開頭的13個整型數組成了結構體的頭信息。對於有兩列的那些數據,第一列表示某一類項目的個數,第二列表示這一類項目的描述信息開始於這個數組中的哪個位置(索引值)。

這里,我們的Counter類有兩個方法,並且關於方法的描述信息開始於第14個int數據。

每個方法的描述信息由5個int型數據組成。第一個整型數代表方法名,它的值是該方法名(譯注:方法名就是個字符串)在字符串表中的索引位置(之后會介紹字符串表)。第二個整數表示該方法所需參數的個數,后面緊跟的第三個數就是關於參數的描述(譯注:它表示與參數相關的描述信息開始於本數組中的哪個位置,也是個索引)。我們現在先忽略掉tag和flags。對於每個函數,Moc還會保存它的返回類型、每個參數的類型、以及參數的名稱。

 

字符串表

***********************************************

struct qt_meta_stringdata_Counter_t {
    QByteArrayData data[6];
    char stringdata[47];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
offsetof(qt_meta_stringdata_Counter_t, stringdata) + ofs \
- idx * sizeof(QByteArrayData) \
)

static const qt_meta_stringdata_Counter_tqt_meta_stringdata_Counter = {
    {
QT_MOC_LITERAL(0,0,7),
QT_MOC_LITERAL(1,8,12),
QT_MOC_LITERAL(2,21,0),
QT_MOC_LITERAL(3,22,8),
QT_MOC_LITERAL(4,31,8),
QT_MOC_LITERAL(5,40,5)
    },
    "Counter\0valueChanged\0\0newValue\0setValue\0"
    "value\0"
};
#undef QT_MOC_LITERAL

***********************************************

這主要就是一個QByteArray的靜態數組。QT_MOC_LITERAL 這個宏可以創建一個靜態的QByteArray ,其數據就是參考的在它下面的對應索引處的字符串。

信號

 
MOC還實現了信號signals(譯注:根據前面的介紹我們已經知道signals其實就是publib,因此所有被定義的信號其實都必須有具體的函數定義,這樣才能C++編譯器編譯,而我們在開發中並不曾寫過信號的定義,這是因為這些都由MOC來完成)。所有的信號都是很簡單的函數而已,他們只是為參數創建一個指針數組並傳遞給QMetaObject::activate函數。指針數組的第一個元素是屬於返回值的。在我們的例子中將它設置為了0,這是因為我們的返回類型是void。
傳遞給activate 函數的第3個參數是信號的索引(在這里,該索引為0)。
***********************************************
// SIGNAL 0
void
  Counter:: valueChanged( int _t1)
{
     void * _a[] = {  0, const_cast< void*>( reinterpret_cast< const void*>(& _t1)) };
    QMetaObject::activate(this, &staticMetaObject,0,_a);
}
***********************************************

 

 

 
調用槽
通過一個槽的索引來調用一個槽函數也是行得通的,這要借助qt_static_metacall這個函數:
 
 
***********************************************
void  Counter:: qt_static_metacall( QObject * _oQMetaObject:: Call _c, int _id, void ** _a)
{
     if ( _c == QMetaObject::InvokeMetaMethod) {
        Counter *_t =static_cast<Counter *>(_o);
        switch (_id) {
        case 0_t->valueChanged((*reinterpret_cast<int(*)>(_a[1])));break;
        case 1_t->setValue((*reinterpret_cast<int(*)>(_a[1])));break;
        default: ;
        }
 
***********************************************
 
函數中的指針數組與在上面介紹signal時的那個函數中的指針數組格式相同。只不過這里沒有用到_a[0],因為這里所有的函數都是返回void。
 
關於索引需要注意的地方
在每個 QMetaObject中,對象的槽、信號和其他一些被涉及到的方法都被分配了一個索引,這些索引值從0開始。他們是按照“先信號、再槽、最后其他方法”的順序排列。這樣得到的索引被稱為相對索引( relative index),相對索引與該類的父類(就是基類)和祖宗類中的方法數量無關。
但是通常,我們不是想知道相對索引,而是想知道在包含了從父類和祖宗類中繼承來的所有方法后的絕對索引。為了得到這個索引,我們只需要在相關索引( relative index)上加上一個偏移量就可以得到絕對索引 absolute index了。這個絕對索引就是在Qt的API中使用的, 像 QMetaObject::indexOf{Signal,Slot,Method}這樣的函數返回的就是絕對索引。
另外,在信號槽的連接機制中還要用到一個關於信號的向量索引。這樣的索引表中如果把槽也包含進來的話槽會造成向量的浪費,而一般槽的數量又要比信號多。所以從Qt4.6開始,Qt內部又多出了一個專門的信號索引signal index ,它是一個只包含了信號的索引表。
在用Qt開發的時候,我們只需要關心絕對索引就行。不過在瀏覽Qt源碼的時候,要留意這三種索引的不同。
連接是如何進行的

在進行信號和槽的連接時Qt做的一件事就是找出要連接的信號和槽的索引。Qt會在meta object的字符串表中查找對應的索引。

然后會創建一個QObjectPrivate::Connection對象並添加到內部的鏈表中。

一個 connection 中需要存儲哪些數據呢?我們需要一種能根據信號索引signal index快速訪問到對應的connection的方法。因為可能會同時有不止一個槽連接到同一個信號上,所以每一個信號都要有一個槽列表;每個connection必須包含接收對象的指針以及被連接的槽的索引;我們還想讓一個connection能在與之對應的接收者被銷毀時自動取消連接,所以每個接收者對象也需要知道誰與他建立了連接這樣才能在析構時將connection清理掉。

下面是定義在 qobject_p.h 中的QObjectPrivate::Connection :

(譯注:只認識QObject不認識QObjectPrivate?看這里:http://blog.csdn.net/newthinker_wei/article/details/22789885)

 

***********************************************

struct QObjectPrivate::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 intargumentTypes;
    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;
        }
    }
};

***********************************************

每一個對象有一個connection vector:每一個信號有一個 QObjectPrivate::Connection的鏈表,這個vector就是與這些鏈表相關聯的。
每一個對象還有一個反向鏈表,它包含了這個對象被連接到的所有的 connection,這樣可以實現連接的自動清除。而且這個反向鏈表是一個雙重鏈表。

 

使用鏈表是因為它們可以快速地添加和移除對象,它們靠保存在QObjectPrivate::Connection中的next/previous前后節點的指針來實現這些操作。

注意senderListprev指針是一個“指針的指針”。這是因為我們不是真的要指向前一個節點,而是要指向一個指向前節點的指針。這個“指針的指針”只有在銷毀連接時才用到,而且不要用它重復往回迭代。這樣設計可以不用對鏈表的首結點做特殊處理。

 

(譯注:對連接的建立如果還有疑惑,請參考:http://blog.csdn.net/newthinker_wei/article/details/22791617

 

信號的發送

我們已經知道當調用一個信號的時候,最終調用的是MOC生成的代碼中的QMetaObject::activate函數。

這里是qobject.cpp中這個函數的實現代碼,這里貼出來的只是一個注解版本。

***********************************************

void QMetaObject::activate(QObject *senderconst QMetaObject *m,intlocal_signal_index,
                           void **argv)
{
    activate(sender,QMetaObjectPrivate::signalOffset(m),local_signal_index,argv);
    /* We just forward to the next function here. We pass the signal offset of
* the meta object rather than the QMetaObject itself
* It is split into two functions because QML internals will call the later. */

}

void QMetaObject::activate(QObject *senderintsignalOffset,intlocal_signal_index,void **argv)
{
    int signal_index =signalOffset +local_signal_index;

    /* The first thing we do is quickly check a bit-mask of 64 bits. If it is 0,
* we are sure there is nothing connected to this signal, and we can return
* quickly, which means emitting a signal connected to no slot is extremely
* fast. */

    if (!sender->d_func()->isSignalConnected(signal_index))
        return// nothing connected to these signals, and no spy

 

    ......

    ......

 

    /*譯注:獲得當前正在運行的線程的ID*/

    Qt::HANDLE currentThreadId = QThread::currentThreadId();


    /* ... Skipped some debugging and QML hooks, and some sanity check ... */

   /*跳過一些調試代碼和完整性檢測*/
    /* We lock a mutex because all operations in the connectionLists are thread safe */

    /*這里用一個互斥量鎖住,因為在ConnectionList里的所有操作都應是線程安全的*/
    QMutexLocker locker(signalSlotLock(sender));

    /* Get the ConnectionList for this signal. I simplified a bit here. The real code
* also refcount the list and do sanity checks */

   /*得到這個信號的ConnectionList。這里我做了一些簡化。原來的代碼還有完整性檢測等*/
    QObjectConnectionListVector *connectionLists =sender->d_func()->connectionLists;
    const QObjectPrivate::ConnectionList *list =
        &connectionLists->at(signal_index);

    QObjectPrivate::Connection *c = list->first;
    if (!c) continue;
    // We need to check against last here to ensure that signals added
// during the signal emission are not emitted in this emission.

    QObjectPrivate::Connection *last = list->last;

    /* Now iterates, for each slot */
    do {
        if (!c->receiver)
            continue;

        QObject * const receiver = c->receiver;

        /*譯注:比較當前正在運行的線程的ID與receiver所屬的線程的ID是否相同*/
        const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;

        // determine if this connection should be sent immediately or
// put into the event queue

       //譯注:注意下面這一段,從這里可以看出對於跨線程的連接,信號發出

       //后槽函數不會立即在當前線程中執行。其執行要等到槽函數所在的線程被

       //激活后。有時間了再研究一下queued_activate這個函數。
        if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
            || (c->connectionType == Qt::QueuedConnection)) {
            /* Will basically copy the argument and post an event */
            queued_activate(sender,signal_index,c,argv);
            continue;
        } else if (c->connectionType == Qt::BlockingQueuedConnection) {
            /* ... Skipped ... */
            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::StaticMetaCallFunctioncallFunction =c->callFunction;
        const int method_relative = c->method_relative;
        if (c->isSlotObject) {
            /* ... Skipped.... Qt5-style connection to function pointer */
        } else if (callFunction &&c->method_offset <=receiver->metaObject()->methodOffset()) {
            /* If we have a callFunction (a pointer to the qt_static_metacall
* generated by moc) we will call it. We also need to check the
* saved metodOffset is still valid (we could be called from the
* destructor) */

            locker.unlock();// We must not keep the lock while calling use code
            callFunction(receiver,QMetaObject::InvokeMetaMethod,method_relative,argv);
            locker.relock();
        } else {
            /* Fallback for dynamic objects */
            const int
 method =method_relative +c->method_offset;
            locker.unlock();
            metacall(receiver,QMetaObject::InvokeMetaMethod,method,argv);
            locker.relock();
        }

        // Check if the object was not deleted by the slot
        if (connectionLists->orphaned)break;
    } while (c != last && (c = c->nextConnectionList) !=0);
}
***********************************************


免責聲明!

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



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