用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的信號和槽機制,但它究竟是如何工作的呢?在本篇博文中,我們會探索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 a, b;
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 頭文件中可以找到他們的定義。
- #define signals public
- #define slots /* nothing */
沒錯,信號和槽都是很簡單的功能:編譯器會將他們與其他任何宏一樣處理。不過這些宏還有一個特殊的作用:MOC會發現他們。
Signals在Qt4之前都是protected類型的,他們在Qt5中變為了public,這樣就可以使用一些新的語法了。
- #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 defines a bunch of functions and a static QMetaObject Those functions are implemented in the file generated by MOC.
- #define emit /* nothing */
emit是個空的宏定義,而且MOC也不會對它進行解析。也就是,emit完全是可有可無的,他沒有任何意義(除了對開發者有提示作用)。
- 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
(譯注:對於#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_Counter, qt_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的定義如下:
- struct QMetaObject
- {
- /* ... Skiped all the public functions ... */
- enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };
- struct { // private data
- const QMetaObject *superdata;
- const QByteArrayData *stringdata;
- const uint *data;
- typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
- StaticMetacallFunction static_metacall;
- const QMetaObject **relatedMetaObjects;
- void *extradata; //reserved for future use
- } d;
- };
代碼中用的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, 2, 0x05,
// slots: name, argc, parameters, tag, flags
4, 1, 27, 2, 0x0a,
// 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
,其數據就是參考的在它下面的對應索引處的字符串。
信號
void Counter:: valueChanged( int _t1)
{
void * _a[] = { 0, const_cast< void*>( reinterpret_cast< const void*>(& _t1)) };
QMetaObject::activate(this, &staticMetaObject,0,_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: ;
}
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 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;
}
}
};
***********************************************
每一個對象有一個connection vector:每一個信號有一個 QObjectPrivate::Connection的鏈表,這個vector就是與這些鏈表相關聯的。
每一個對象還有一個反向鏈表,它包含了這個對象被連接到的所有的 connection,這樣可以實現連接的自動清除。而且這個反向鏈表是一個雙重鏈表。
使用鏈表是因為它們可以快速地添加和移除對象,它們靠保存在QObjectPrivate::Connection中的next/previous前后節點的指針來實現這些操作。
注意senderList的prev指針是一個“指針的指針”。這是因為我們不是真的要指向前一個節點,而是要指向一個指向前節點的指針。這個“指針的指針”只有在銷毀連接時才用到,而且不要用它重復往回迭代。這樣設計可以不用對鏈表的首結點做特殊處理。
(譯注:對連接的建立如果還有疑惑,請參考:http://blog.csdn.net/newthinker_wei/article/details/22791617)
信號的發送
我們已經知道當調用一個信號的時候,最終調用的是MOC生成的代碼中的QMetaObject::activate函數。
這里是qobject.cpp中這個函數的實現代碼,這里貼出來的只是一個注解版本。
***********************************************
void QMetaObject::activate(QObject *sender, const 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 *sender, intsignalOffset,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);
}
***********************************************