Qt高階——Qt訊號槽機制原始碼解析
一、訊號槽機制的原理
1、訊號槽簡介
訊號槽是觀察者模式的一種實現,特性如下:
A、一個訊號就是一個能夠被觀察的事件,或者至少是事件已經發生的一種通知;
B、一個槽就是一個觀察者,通常就是在被觀察的物件發生改變的時候——也可以說是訊號發出的時候——被呼叫的函式;
C、訊號與槽的連線,形成一種觀察者-被觀察者的關係;
D、當事件或者狀態發生改變的時候,訊號就會被髮出;同時,訊號發出者有義務呼叫所有註冊的對這個事件(訊號)感興趣的函式(槽)。
訊號和槽是多對多的關係。一個訊號可以連線多個槽,而一個槽也可以監聽多個訊號。
訊號槽與語言無關,有多種方法可以實現訊號槽,不同的實現機制會導致訊號槽的差別很大。訊號槽術語最初來自 Trolltech 公司的 Qt 庫,由於其設計理念的先進性,立刻引起電腦科學界的注意,提出了多種不同的實現。目前,訊號槽依然是 Qt 庫的核心之一,其他許多庫也提供了類似的實現,甚至出現了一些專門提供這一機制的工具庫。
訊號槽是Qt物件以及其派生類物件之間的一種高效通訊介面,是Qt的核心特性,也是Qt區別與其他工具包的重要地方。訊號槽完全獨立於標準的C/C++語言,因此要正確的處理好訊號和槽,必須藉助於一個成為MOC(Meta Object Compiler)的Qt工具,MOC工具是一個C++預處理程式,能為高層次的事件處理自動生成所需要的附加程式碼。
2、不同平臺的實現
MFC中的訊息機制沒有采用C++中的虛擬函式機制,原因是訊息太多,虛擬函式開銷太大。在Qt中也沒有采用C++中的虛擬函式機制,而是採用了訊號槽機制,原因與此相同。更深層次的原因上,多型的底層實現機制只有兩種,一種是按照名稱查表,一種是按照位置查表。兩種方式各有利弊,而C++的虛擬函式機制無條件的採用了後者,導致的問題就是在子類很少過載基類實現的時候開銷太大,再加上介面程式設計中子類眾多的情況,基本上C++的虛擬函式機制效率太低,於是各家庫的編寫者就只好自謀生路,當然,這其實是C++語言本身的缺陷。
二、Qt訊號槽例項解析
1、訊號槽使用示例
使用簡單的例項:
#ifndef OBJECT_H #define OBJECT_H #include <QObject> #include <QString> #include <QDebug> class Object : public QObject { Q_OBJECT Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged) Q_PROPERTY(int score READ score WRITE setScore NOTIFY scoreChanged) Q_PROPERTY(Level level READ level WRITE setLevel) Q_CLASSINFO("Author", "Scorpio") Q_CLASSINFO("Version", "1.0") public: enum Level { Basic = 1, Middle, Advanced, Master }; Q_ENUMS(Level) protected: QString m_name; Level m_level; int m_age; int m_score; void setLevel(const int& score) { if(score <= 60) { m_level = Basic; } else if(score < 100) { m_level = Middle; } else if(score < 150) { m_level = Advanced; } else { m_level = Master; } } public: explicit Object(QString name, QObject *parent = 0):QObject(parent) { m_name = name; setObjectName(m_name); connect(this, SIGNAL(ageChanged(int)), this, SLOT(onAgeChanged(int))); connect(this, SIGNAL(scoreChanged(int)), this, SLOT(onScoreChanged(int))); } int age()const { return m_age; } void setAge(const int& age) { m_age = age; emit ageChanged(m_age); } int score()const { return m_score; } void setScore(const int& score) { m_score = score; setLevel(m_score); emit scoreChanged(m_score); } Level level()const { return m_level; } void setLevel(const Level& level) { m_level = level; } signals: void ageChanged(int age); void scoreChanged(int score); public slots: void onAgeChanged(int age) { qDebug() << "age changed:" << age; } void onScoreChanged(int score) { qDebug() << "score changed:" << score; } }; #endif // OBJECT_H
Main函式:
#include <QCoreApplication>
#include "Object.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Object ob("object");
//設定屬性age
ob.setProperty("age", QVariant(30));
qDebug() << "age: " << ob.age();
qDebug() << "property age: " << ob.property("age").toInt();
//設定屬性score
ob.setProperty("score", QVariant(90));
qDebug() << "score: " << ob.score();
qDebug() << "property score: " << ob.property("score").toInt();
qDebug() << "Level: " << ob.level();
ob.setProperty("level", 4);
qDebug() << "level: " << ob.level();
qDebug() << "Property level: " << ob.property("level").toInt();
//內省intropection,執行時查詢物件資訊
qDebug() << "object name: " << ob.objectName();
qDebug() << "class name: " << ob.metaObject()->className();
qDebug() << "isWidgetType: " << ob.isWidgetType();
qDebug() << "inherit: " << ob.inherits("QObject");
return a.exec();
}
2、SIGNAL與SLOT巨集
SIGNAL與SLOT巨集定義在/src/corelib/kernel/Qobjectdefs.h檔案中。
Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#define QTOSTRING_HELPER(s) #s
#define QTOSTRING(s) QTOSTRING_HELPER(s)
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
# ifndef QT_NO_KEYWORDS
# define METHOD(a) qFlagLocation("0"#a QLOCATION)
# endif
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
#else
# ifndef QT_NO_KEYWORDS
# define METHOD(a) "0"#a
# endif
# define SLOT(a) "1"#a
# define SIGNAL(a) "2"#a
#endif
SIGNAL與SLOT巨集會利用預編譯器將一些引數轉化成字串,並且在前面新增上編碼。
在除錯模式中,如果signal的連接出現問題,提示警告資訊的時候還會註明對應的檔案位置。qFlagLocation 用於定位程式碼對應的行資訊,會將對應程式碼的地址資訊註冊到一個有兩個入口的表裡。
Object.h檔案中有關SIGNAL與SLOT巨集部分程式碼如下:
connect(this, SIGNAL(ageChanged(int)), this, SLOT(onAgeChanged(int)));
connect(this, SIGNAL(scoreChanged(int)), this, SLOT(onScoreChanged(int)));
通過對Object.h檔案進行預編譯,得到Object.i檔案。
使用G++進行預編譯:
g++ -E Object.h -o Object.i -I/usr/local/Trolltech/Qt-4.8.6/include/QtCore -I/usr/local/Trolltech/Qt-4.8.6/include -I.
Object.i檔案中結果如下:
connect(this, qFlagLocation("2""ageChanged(int)" "\0" "Object.h" ":" "54"), this, qFlagLocation("1""onAgeChanged(int)" "\0" "Object.h" ":" "54"));
connect(this, qFlagLocation("2""scoreChanged(int)" "\0" "Object.h" ":" "55"), this, qFlagLocation("1""onScoreChanged(int)" "\0" "Object.h" ":" "55"));
3、類的元物件
程式編譯時make呼叫MOC對工程原始碼進行解析,生成相應類的moc_xxx.cpp檔案,
const QMetaObject Object::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Object,
qt_meta_data_Object, &staticMetaObjectExtraData }
};
靜態成員staticMetaObject被填充的值如下:
const QMetaObject superdata;//元資料代表的類的基類的元資料,被填充為基類的元資料指標&QWidget::staticMetaObject
const char stringdata;//元資料的簽名標記,被填充為qt_meta_stringdata_Widget.data
const uint *data;//元資料的索引陣列的指標,被填充為qt_meta_data_Widget
const QMetaObject **extradata;//擴充套件元資料表的指標,內部被填充為函式指標qt_static_metacall。
staticMetaObjectExtraData初始化如下:
const QMetaObjectExtraData Object::staticMetaObjectExtraData = {
0, qt_static_metacall
};
QMetaObjectExtraData型別的內部成員static_metacall是一個指向Object::qt_static_metacall 的函式指標。
Object的記憶體佈局如下:
Object記憶體佈局已經包含了靜態成員staticMetaObject和
staticMetaObjectExtraData成員。
const QMetaObject *Object::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->metaObject : &staticMetaObject;
}
QObject::d_ptr->metaObject僅供動態元物件(QML物件)使用,所以一般而言,虛擬函式 metaObject() 僅返回類的 staticMetaObject。
4、元資料表
Qt程式編譯時make會呼叫MOC工具對原始檔進行分析,如果某個類包含了Q_OBJECT巨集,MOC會生成對應的moc_xxx.cpp檔案。
moc_Object.cpp檔案內容中:
Object的元資料如下:
static const uint qt_meta_data_Object[] = {
// content:內容資訊
6, // revision MOC生成程式碼的版本號
0, // classname 類名,在qt_meta_stringdata_Object陣列中索引為0
2, 14, // classinfo 類資訊,有2個cassinfo定義,
4, 18, // methods 類有4個自定義方法,即訊號與槽個數,
3, 38, // properties 屬性的位置資訊,有3個自定義屬性,
1, 50, // enums/sets 列舉的位置資訊,有一個自定義列舉,在qt_meta_stringdata_Object陣列中索引為50
0, 0, // constructors 建構函式的位置資訊
0, // flags
2, // signalCount
// classinfo: key, value //類資訊的儲存在qt_meta_stringdata_Object陣列中,
15, 7, //第一個類資訊,key的陣列索引為15,即Author,value的陣列索引為7,即Scorpio
26, 22, //第二個類資訊,key的陣列索引為26,即Version,value的陣列索引為22,即1.0
// signals: signature, parameters, type, tag, flags
39, 35, 34, 34, 0x05, //第一個自定義訊號的簽名儲存在qt_meta_stringdata_Object陣列中,
//索引是39,即ageChanged(int)
61, 55, 34, 34, 0x05, //第二個自定義訊號的簽名儲存在qt_meta_stringdata_Object陣列中,
//索引是61,即scoreChanged(int)
// slots: signature, parameters, type, tag, flags
79, 35, 34, 34, 0x0a, //第一個自定義槽函式的簽名儲存在qt_meta_stringdata_Object陣列中,
//索引是79,即onAgeChanged(int)
97, 55, 34, 34, 0x0a, //第二個自定義槽函式的簽名儲存在qt_meta_stringdata_Object陣列中,
//索引是79,即onScoreChanged(int)
// properties: name, type, flags
35, 117, 0x02495103, // 第一個自定義屬性的簽名儲存在qt_meta_stringdata_Object中,索引是35,即age
55, 117, 0x02495103, // 第二個自定義屬性的簽名儲存在qt_meta_stringdata_Object中,索引是55,即score
127, 121, 0x0009510b, // 第三個自定義屬性的簽名儲存在qt_meta_stringdata_Object中,索引是127,即level
// properties: notify_signal_id //屬性關聯的訊號編號
0,
1,
0,
// enums: name, flags, count, data
121, 0x0, 4, 54, //列舉的定義,儲存在qt_meta_stringdata_Object中,索引是121,即Level,內含4個列舉常量
// enum data: key, value //列舉資料的鍵值對
133, uint(Object::Basic), //陣列索引是133,即Basic
139, uint(Object::Middle), //陣列索引是139,即Middle
146, uint(Object::Advanced), //陣列索引是146,即Advanced
155, uint(Object::Master), //陣列索引是155,即Master
0 // eod 元資料結束標記
};
內省表是一個 uint 陣列,分為五個部分:第一部分content,即內容,分為9行。第一行revision,指MOC生成程式碼的版本號(Qt4 是6,Qt5則是7)。第二個classname,即類名,該值是一個索引,指向字串表的某一個位置(本例中就是第0位)。
static const char qt_meta_stringdata_Object[] = {
"Object\0Scorpio\0Author\0""1.0\0Version\0\0"
"age\0ageChanged(int)\0score\0scoreChanged(int)\0"
"onAgeChanged(int)\0onScoreChanged(int)\0"
"int\0Level\0level\0Basic\0Middle\0Advanced\0"
"Master\0"
};
5、訊號的實現
MOC在生成的moc_xxx.cpp檔案中實現了訊號,建立了一個指向引數的指標的陣列,並將指標陣列傳給QMetaObject::activate函式。陣列的第一個元素是返回值。本例中值是0,因為返回值是void。傳給activate函式的第三個引數是訊號的索引(本例中是0)。
// SIGNAL 0,ageChanged訊號的實現
void Object::ageChanged(int _t1)
{
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
// SIGNAL 1 scoreChanged訊號的實現
void Object::scoreChanged(int _t1)
{
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 1, _a);
}
6、槽函式的呼叫
利用槽函式在qt_static_metacall 函式的索引位置來呼叫槽函式:
void Object::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
Q_ASSERT(staticMetaObject.cast(_o));
Object *_t = static_cast<Object *>(_o);
switch (_id) {
case 0: _t->ageChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: _t->scoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 2: _t->onAgeChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 3: _t->onScoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
default: ;
}
}
}
7、元物件中的索引
在每一個QMetaObject物件中,槽、訊號以及其它的物件可呼叫函式都會分配一個從0開始的索引。索引是有順序的,訊號在第一位,槽在第二位,最後是其它函式。這個索引在內部被稱為相對索引,不包含父物件的索引位。
為了實現包含在繼承鏈中其它函式的索引,在相對索引的基礎上新增一個偏移量,得到絕對索引。絕對索引是在公開API中使用的索引,由QMetaObject::indexOf(Signal, Slot, Method) 類似的函式返回。
連線機制使用以訊號為索引的向量。但是在向量中,所有的槽也會佔有一定空間,通常在一個物件中,槽的數量要比訊號多。所以從 Qt 4.6開始,使用的是一種僅包含訊號索引的新的內部實現。
8、訊號與槽的連線
開始連線時,Qt所要做的第一件事是找出所需要的訊號和槽的索引。Qt會去查詢元物件的字串表來找出相應的索引。
然後,建立一個 QObjectPrivate::Connection 物件,將其新增到內部的連結串列中。
由於允許多個槽連線到同一個訊號,需要為每一個訊號新增一個已連線的槽的列表。每一個連線都必須包含接收物件和槽的索引。在接收物件銷燬的時候,相應的連線也能夠被自動銷燬。所以每一個接收物件都需要知道誰連線到它自己,以便能夠清理連線。
QObject物件的私有資料QObjectPrivate如下:
class Q_CORE_EXPORT QObjectPrivate : public QObjectData
{
Q_DECLARE_PUBLIC(QObject)
public:
struct ExtraData
{
ExtraData() {}
QList<QByteArray> propertyNames;
QList<QVariant> propertyValues;
};
typedef void (*StaticMetaCallFunction)(QObject *, QMetaObject::Call, int, void **);
struct Connection
{
QObject *sender;
QObject *receiver;
StaticMetaCallFunction callFunction;
// The next pointer for the singly-linked ConnectionList
Connection *nextConnectionList;
//senders linked list
Connection *next;
Connection **prev;
QBasicAtomicPointer<int> argumentTypes;
ushort method_offset;
ushort method_relative;
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
~Connection();
int method() const { return method_offset + method_relative; }
};
// ConnectionList is a singly-linked list
struct ConnectionList {
ConnectionList() : first(0), last(0) {}
Connection *first;
Connection *last;
};
struct Sender
{
QObject *sender;
int signal;
int ref;
};
QObjectPrivate(int version = QObjectPrivateVersion);
virtual ~QObjectPrivate();
void deleteChildren();
void setParent_helper(QObject *);
void moveToThread_helper();
void setThreadData_helper(QThreadData *currentData, QThreadData *targetData);
void _q_reregisterTimers(void *pointer);
bool isSender(const QObject *receiver, const char *signal) const;
QObjectList receiverList(const char *signal) const;
QObjectList senderList() const;
void addConnection(int signal, Connection *c);
void cleanConnectionLists();
static inline Sender *setCurrentSender(QObject *receiver,
Sender *sender);
static inline void resetCurrentSender(QObject *receiver,
Sender *currentSender,
Sender *previousSender);
static void clearGuards(QObject *);
static QObjectPrivate *get(QObject *o) {
return o->d_func();
}
int signalIndex(const char *signalName) const;
inline bool isSignalConnected(uint signalIdx) const;
// To allow arbitrary objects to call connectNotify()/disconnectNotify() without making
// the API public in QObject. This is used by QDeclarativeNotifierEndpoint.
inline void connectNotify(const char *signal);
inline void disconnectNotify(const char *signal);
static inline void signalSignature(const QMetaMethod &signal,
QVarLengthArray<char> *result);
public:
QString objectName;
ExtraData *extraData; // extra data set by the user
QThreadData *threadData; // id of the thread that owns the object
QObjectConnectionListVector *connectionLists;//連線連結串列向量容器
Connection *senders; // linked list of connections connected to this object
Sender *currentSender; // object currently activating the object
mutable quint32 connectedSignals[2];
// preserve binary compatibility with code compiled without Qt 3 support
// keeping the binary layout stable helps the Qt Creator debugger
void *unused;
QList<QPointer<QObject> > eventFilters;
union {
QObject *currentChildBeingDeleted;
QAbstractDeclarativeData *declarativeData; //extra data used by the declarative module
};
// these objects are all used to indicate that a QObject was deleted
// plus QPointer, which keeps a separate list
QAtomicPointer<QtSharedPointer::ExternalRefCountData> sharedRefcount;
};
每一個QObject物件都有一個連線連結串列容器QObjectConnectionListVector *connectionLists:將每一個訊號與一個 QObjectPrivate::Connection 的連結串列關聯起來。
QObject::connect函式的實現如下:
bool QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type)
{
{
const void *cbdata[] = { sender, signal, receiver, method, &type };
if (QInternal::activateCallbacks(QInternal::ConnectCallback, (void **) cbdata))
return true;
}
if (type == Qt::AutoCompatConnection) {
type = Qt::AutoConnection;
}
if (sender == 0 || receiver == 0 || signal == 0 || method == 0) {
qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
sender ? sender->metaObject()->className() : "(null)",
(signal && *signal) ? signal+1 : "(null)",
receiver ? receiver->metaObject()->className() : "(null)",
(method && *method) ? method+1 : "(null)");
return false;
}
QByteArray tmp_signal_name;
if (!check_signal_macro(sender, signal, "connect", "bind"))
return false;
const QMetaObject *smeta = sender->metaObject();
const char *signal_arg = signal;
++signal; //skip code
//在傳送者物件的元物件中將訊號的相對索引找到
int signal_index = QMetaObjectPrivate::indexOfSignalRelative(&smeta, signal, false);
if (signal_index < 0)
{
// check for normalized signatures
tmp_signal_name = QMetaObject::normalizedSignature(signal - 1);
signal = tmp_signal_name.constData() + 1;
smeta = sender->metaObject();
signal_index = QMetaObjectPrivate::indexOfSignalRelative(&smeta, signal, false);
}
if (signal_index < 0)
{
// re-use tmp_signal_name and signal from above
smeta = sender->metaObject();
signal_index = QMetaObjectPrivate::indexOfSignalRelative(&smeta, signal, true);
}
if (signal_index < 0) {
err_method_notfound(sender, signal_arg, "connect");
err_info_about_objects("connect", sender, receiver);
return false;
}
signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
int signalOffset, methodOffset;
computeOffsets(smeta, &signalOffset, &methodOffset);
int signal_absolute_index = signal_index + methodOffset;
signal_index += signalOffset;
QByteArray tmp_method_name;
int membcode = extract_code(method);
if (!check_method_code(membcode, receiver, method, "connect"))
return false;
const char *method_arg = method;
++method; // skip code
const QMetaObject *rmeta = receiver->metaObject();
//在接受者物件的元物件中將槽函式的相對索引找到
int method_index_relative = -1;
switch (membcode) {
case QSLOT_CODE:
method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(&rmeta, method, false);
break;
case QSIGNAL_CODE:
method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(&rmeta, method, false);
break;
}
if (method_index_relative < 0) {
// check for normalized methods
tmp_method_name = QMetaObject::normalizedSignature(method);
method = tmp_method_name.constData();
// rmeta may have been modified above
rmeta = receiver->metaObject();
switch (membcode) {
case QSLOT_CODE:
method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(&rmeta, method, false);
if (method_index_relative < 0)
method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(&rmeta, method, true);
break;
case QSIGNAL_CODE:
method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(&rmeta, method, false);
if (method_index_relative < 0)
method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(&rmeta, method, true);
break;
}
}
if (method_index_relative < 0) {
err_method_notfound(receiver, method_arg, "connect");
err_info_about_objects("connect", sender, receiver);
return false;
}
//檢查連線引數是否匹配
if (!QMetaObject::checkConnectArgs(signal, method))
{
qWarning("QObject::connect: Incompatible sender/receiver arguments"
"\n %s::%s --> %s::%s",
sender->metaObject()->className(), signal,
receiver->metaObject()->className(), method);
return false;
}
int *types = 0;
if ((type == Qt::QueuedConnection)
&& !(types = queuedConnectionTypes(smeta->method(signal_absolute_index).parameterTypes())))
return false;
//呼叫QMetaObjectPrivate::connect將訊號與槽進行連線
if (!QMetaObjectPrivate::connect(sender, signal_index, receiver, method_index_relative, rmeta ,type, types))
return false;
const_cast<QObject*>(sender)->connectNotify(signal - 1);
return true;
}
QObject::connect函式的主要功能是在接受者物件的元物件中將槽函式的相對索引找到,在接受者物件的元物件中將槽函式的相對索引找到,最後呼叫QMetaObjectPrivate::connect將訊號與槽進行連線。QObject及其派生類物件的元物件在建立時就有一個QObjectConnectionListVector連線連結串列容器,QObject::connect的作用就是將新的連線加入到訊號傳送者附屬的元物件的連線連結串列容器的相應訊號的連線連結串列中(一個訊號可能連線多個槽函式)。
每個QObject及其派生類物件都有一個QObjectConnectionListVector *connectionLists連線連結串列容器,將訊號的索引作為容器的索引,將每一個訊號與一個 QObjectPrivate::ConnectionList連結串列關聯起來。同時,QObjectPrivate::ConnectionList連結串列中連線的某個槽函式可能是接收者物件的槽函式連結串列中的一個。每個接收者物件的連結串列如下:
senderList 的 prev 指標是一個指標的指標。這是因為並不是真的指向上一個節點,而是指向上一個節點中的 next 指標。這個指標僅在連線銷燬時使用,並且不能向後遍歷。它允許不為第一個元素新增特殊處理。
容器中儲存的ConnectionList如下:
struct ConnectionList {
ConnectionList() : first(0), last(0) {}
Connection *first;//第一個結點
Connection *last;//最後一個結點
};
每個ConnectionList型別元素是一個雙向連結串列,儲存了訊號的所有連線。連線的型別Connection結構如下:
struct Connection
{
QObject *sender;//傳送者
QObject *receiver;//接受者
StaticMetaCallFunction callFunction;//呼叫的槽函式
// The next pointer for the singly-linked ConnectionList
Connection *nextConnectionList;
//senders linked list
Connection *next;
Connection **prev;
QBasicAtomicPointer<int> argumentTypes;
ushort method_offset;
ushort method_relative;
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
~Connection();
int method() const { return method_offset + method_relative; }
};
QMetaObjectPrivate::connect函式原始碼如下:
//將一個新的連線加入到訊號傳送者的連線連結串列容器中相應訊號的連線連結串列中,其中連線加入的連線連結串列的索引為訊號的索引
bool QMetaObjectPrivate::connect(const QObject *sender, int signal_index,
const QObject *receiver, int method_index,
const QMetaObject *rmeta, int type, int *types)
{
QObject *s = const_cast<QObject *>(sender);
QObject *r = const_cast<QObject *>(receiver);
int method_offset = rmeta ? rmeta->methodOffset() : 0;
//在元物件的元資料字串中找到回撥的函式指標qt_static_metacall
QObjectPrivate::StaticMetaCallFunction callFunction =
(rmeta && QMetaObjectPrivate::get(rmeta)->revision >= 6 && rmeta->d.extradata)
? reinterpret_cast<const QMetaObjectExtraData *>(rmeta->d.extradata)->static_metacall : 0;
QOrderedMutexLocker locker(signalSlotLock(sender),
signalSlotLock(receiver));
//如果連線型別為Qt::UniqueConnection
if (type & Qt::UniqueConnection)
{
QObjectConnectionListVector *connectionLists = QObjectPrivate::get(s)->connectionLists;
if (connectionLists && connectionLists->count() > signal_index)
{
//根據訊號索引獲取訊號的連線
const QObjectPrivate::Connection *c2 =
(*connectionLists)[signal_index].first;
int method_index_absolute = method_index + method_offset;
while (c2)
{ //如果訊號的接收者相同並且槽函式相同,即相同的連線已經存在
if (c2->receiver == receiver && c2->method() == method_index_absolute)
return false;//直接返回,
c2 = c2->nextConnectionList;//下一個訊號連線
}
}
type &= Qt::UniqueConnection - 1;
}
//建立一個新的連線
QObjectPrivate::Connection *c = new QObjectPrivate::Connection;
//設定連線的屬性
c->sender = s;
c->receiver = r;
c->method_relative = method_index;
c->method_offset = method_offset;
c->connectionType = type;
c->argumentTypes = types;
c->nextConnectionList = 0;
c->callFunction = callFunction;//設定回撥的函式指標為qt_static_metacall
QT_TRY
{ //將連線新增到傳送者的連線連結串列容器中相應的訊號對應的連線連結串列中
QObjectPrivate::get(s)->addConnection(signal_index, c);
} QT_CATCH(...) {
delete c;
QT_RETHROW;
}
c->prev = &(QObjectPrivate::get(r)->senders);
c->next = *c->prev;
*c->prev = c;
if (c->next)
c->next->prev = &c->next;
QObjectPrivate *const sender_d = QObjectPrivate::get(s);
if (signal_index < 0)
{
sender_d->connectedSignals[0] = sender_d->connectedSignals[1] = ~0;
}
else if (signal_index < (int)sizeof(sender_d->connectedSignals) * 8)
{
sender_d->connectedSignals[signal_index >> 5] |= (1 << (signal_index & 0x1f));
}
return true;
}
9、訊號的發射
使用emit發射訊號時,實際呼叫MOC實現的訊號函式,訊號函式內部呼叫了QMetaObject::activate()函式。
// SIGNAL 0,ageChanged訊號的實現
void Object::ageChanged(int _t1)
{
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
// SIGNAL 1 scoreChanged訊號的實現
void Object::scoreChanged(int _t1)
{
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 1, _a);
}
void QMetaObject::activate(QObject *sender, const QMetaObject *m,
int local_signal_index,void **argv)
{
int signalOffset;
int methodOffset;
computeOffsets(m, &signalOffset, &methodOffset);
int signal_index = signalOffset + local_signal_index;
if (!sender->d_func()->isSignalConnected(signal_index))
return; // 如果傳送的訊號沒有槽連線,直接返回
if (sender->d_func()->blockSig)
return;//如果阻塞,直接返回
int signal_absolute_index = methodOffset + local_signal_index;
void *empty_argv[] = { 0 };
if (qt_signal_spy_callback_set.signal_begin_callback != 0)
{
qt_signal_spy_callback_set.signal_begin_callback(sender, signal_absolute_index,
argv ? argv : empty_argv);
}
Qt::HANDLE currentThreadId = QThread::currentThreadId();
QMutexLocker locker(signalSlotLock(sender));
//獲取傳送者的連線連結串列容器
QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;
if (!connectionLists)
{
locker.unlock();
if (qt_signal_spy_callback_set.signal_end_callback != 0)
qt_signal_spy_callback_set.signal_end_callback(sender, signal_absolute_index);
return;
}
++connectionLists->inUse;
//從傳送者的連線連結串列容器中使用訊號索引作為索引,獲取相應的連線連結串列
const QObjectPrivate::ConnectionList *list;
if (signal_index < connectionLists->count())
list = &connectionLists->at(signal_index);
else
list = &connectionLists->allsignals;
do {
//索取傳送的訊號的連線連結串列的第一個連線
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;
do
{
if (!c->receiver)
continue;//如果連線的接收者為空,繼續
QObject * const receiver = c->receiver;
const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId;
// 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_absolute_index, c, argv ? argv : empty_argv);
continue;
#ifndef QT_NO_THREAD
}
//阻塞佇列連線型別
else if (c->connectionType == Qt::BlockingQueuedConnection)
{
locker.unlock();
if (receiverInSameThread)
{
qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
"Sender is %s(%p), receiver is %s(%p)",
sender->metaObject()->className(), sender,
receiver->metaObject()->className(), receiver);
}
QSemaphore semaphore;
QCoreApplication::postEvent(receiver, new QMetaCallEvent(c->method_offset, c->method_relative,
c->callFunction,
sender, signal_absolute_index,
0, 0,
argv ? argv : empty_argv,
&semaphore));
semaphore.acquire();
locker.relock();
continue;
#endif
}
QObjectPrivate::Sender currentSender;
QObjectPrivate::Sender *previousSender = 0;
if (receiverInSameThread)
{
currentSender.sender = sender;
currentSender.signal = signal_absolute_index;
currentSender.ref = 1;
previousSender = QObjectPrivate::setCurrentSender(receiver, ¤tSender);
}
//獲取連線的回撥函式指標
const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
const int method_relative = c->method_relative;
//如果連線的方法的偏移小於接收者的元物件的方法的偏移
if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset())
{
//we compare the vtable to make sure we are not in the destructor of the object.
locker.unlock();
if (qt_signal_spy_callback_set.slot_begin_callback != 0)
qt_signal_spy_callback_set.slot_begin_callback(receiver, c->method(), argv ? argv : empty_argv);
//根據接收者的方法偏移,接收者等引數呼叫qt_static_metacall回撥函式
callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);
if (qt_signal_spy_callback_set.slot_end_callback != 0)
qt_signal_spy_callback_set.slot_end_callback(receiver, c->method());
locker.relock();
}
else
{
const int method = method_relative + c->method_offset;
locker.unlock();
if (qt_signal_spy_callback_set.slot_begin_callback != 0)
{
qt_signal_spy_callback_set.slot_begin_callback(receiver,
method,
argv ? argv : empty_argv);
}
//根據接收者、接收者的方法索引等引數呼叫傳送元物件的metacall
metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
if (qt_signal_spy_callback_set.slot_end_callback != 0)
qt_signal_spy_callback_set.slot_end_callback(receiver, method);
locker.relock();
}
if (receiverInSameThread)
QObjectPrivate::resetCurrentSender(receiver, ¤tSender, previousSender);
if (connectionLists->orphaned)
break;
} while (c != last && (c = c->nextConnectionList) != 0);
if (connectionLists->orphaned)
break;
} while (list != &connectionLists->allsignals &&
//start over for all signals;
((list = &connectionLists->allsignals), true));
--connectionLists->inUse;
Q_ASSERT(connectionLists->inUse >= 0);
if (connectionLists->orphaned)
{
if (!connectionLists->inUse)
delete connectionLists;
}
else if (connectionLists->dirty)
{
sender->d_func()->cleanConnectionLists();
}
locker.unlock();
if (qt_signal_spy_callback_set.signal_end_callback != 0)
qt_signal_spy_callback_set.signal_end_callback(sender, signal_absolute_index);
}
metacall函式內部呼叫了qt_metacall函式。
int QMetaObject::metacall(QObject *object, Call cl, int idx, void **argv)
{
if (QMetaObject *mo = object->d_ptr->metaObject)
return static_cast<QAbstractDynamicMetaObject*>(mo)->metaCall(cl, idx, argv);
else
return object->qt_metacall(cl, idx, argv);
}
int Object::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 < 4)
qt_static_metacall(this, _c, _id, _a);
_id -= 4;
}
#ifndef QT_NO_PROPERTIES
else if (_c == QMetaObject::ReadProperty)
{
void *_v = _a[0];
switch (_id) {
case 0: *reinterpret_cast< int*>(_v) = age(); break;
case 1: *reinterpret_cast< int*>(_v) = score(); break;
case 2: *reinterpret_cast< Level*>(_v) = level(); break;
}
_id -= 3;
}
else if (_c == QMetaObject::WriteProperty)
{
void *_v = _a[0];
switch (_id) {
case 0: setAge(*reinterpret_cast< int*>(_v)); break;
case 1: setScore(*reinterpret_cast< int*>(_v)); break;
case 2: setLevel(*reinterpret_cast< Level*>(_v)); break;
}
_id -= 3;
} else if (_c == QMetaObject::ResetProperty) {
_id -= 3;
} else if (_c == QMetaObject::QueryPropertyDesignable) {
_id -= 3;
} else if (_c == QMetaObject::QueryPropertyScriptable) {
_id -= 3;
} else if (_c == QMetaObject::QueryPropertyStored) {
_id -= 3;
} else if (_c == QMetaObject::QueryPropertyEditable) {
_id -= 3;
} else if (_c == QMetaObject::QueryPropertyUser) {
_id -= 3;
}
#endif // QT_NO_PROPERTIES
return _id;
}
qt_metacall函式內部呼叫了qt_static_metacall函式。
10、槽函式的呼叫
槽函式最終通過qt_static_metacall函式根據引數呼叫相應的槽函式。
void Object::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod)
{
Q_ASSERT(staticMetaObject.cast(_o));
Object *_t = static_cast<Object *>(_o);
switch (_id) {
case 0: _t->ageChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: _t->scoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 2: _t->onAgeChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 3: _t->onScoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
default: ;
}
}
}
11、函式呼叫流程分析
在onAgeChanged(int age)槽函式內部斷點除錯。
得到的函式呼叫棧如下:
函式呼叫棧分析:
Object::qt_metacall函式內部呼叫了Object::setAge函式,setAge內部呼叫Object::ageChanged訊號函式,ageChanged訊號函式內部呼叫了QMetaObject::activate函式,activate函式內部呼叫Object::qt_static_metacall函式,最終qt_static_metacall函式內部呼叫了槽函式onAgeChanged。
因此在本例中,當呼叫ob.setProperty("age", QVariant(30));設定屬性時,觸發了QMetaProperty::Write函式的呼叫,進而呼叫MOC實現的moc_Object.cpp檔案中的Object::qt_metacall,qt_metacall內部呼叫setAge函式,setAge函式內部發射訊號ageChanged,即呼叫Object::ageChanged訊號函式,Object::ageChanged函式內部呼叫了Object物件的元物件的QMetaObject::activate函式,activate函式內部呼叫了Object::qt_static_metacall函式,最終qt_static_metacall內部實現對槽函式onAgeChanged的呼叫。
本例中,訊號和槽處於同一執行緒,連線型別為直接連線,因此屬於同步呼叫,是最簡單的呼叫型別。QMetaObject::activate函式內部實際上根據Object物件的元物件中的訊號連線連結串列容器查詢得到訊號對應的:qt_static_metacall回撥函式,進而回調的。
三、訊號槽的標準C++實現
1、Qt元物件的模擬實現
Object類的實現:
#ifndef OBJECT_H
#define OBJECT_H
#include<map>
#include <iostream>
#include <cstring>
using namespace std;
//巨集定義
#define SLOT(a) #a
#define SIGNAL(a) #a
#define cpp_slots
#define cpp_signals protected
#define cpp_emit
class Object;
//元物件系統,負責蒐集訊號與槽的名稱
struct MetaObject
{
//訊號組
const char * signal;
//槽組
const char * slot;
//啟用某個訊號,idx為訊號索引
static void active(Object * sender, int idx);
};
//被連線物件資訊
struct Connection
{
Object * receiver;//訊號的接收者
int method;//槽函式索引
};
//儲存訊號索引與連線物件對映
typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
//訊號和槽的索引查詢函式,返回訊號或槽的索引
static int find_string(const char * str, const char * substr)
{
if (strlen(str) < strlen(substr))
return -1;
int idx = 0;
int len = strlen(substr);
bool start = true;
const char * pos = str;
while (*pos)
{
if (start && !strncmp(pos, substr, len) && pos[len] == '\n')
return idx;
start = false;
if (*pos == '/n')
{
idx++;
start = true;
}
pos++;
}
return -1;
}
class Object
{
static MetaObject meta;//靜態元物件宣告
void metacall(int idx);//宣告元方法呼叫函式
public:
Object()
{
}
//建立連線
static void cpp_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
{
cout << "connecting a signal to slot..." << endl;
//從元物件資料表中檢視訊號和槽是否存在
int sig_idx = find_string(sender->meta.signal, sig);
int slt_idx = find_string(receiver->meta.slot, slt);
//如果沒有找到訊號或者槽
if (sig_idx == -1 || slt_idx == -1)
{
perror("signal or slot not found!");
}
else
{
//建立一個連線,連線記憶體儲接收者和槽函式的索引
Connection c = { receiver, slt_idx };
cout << "add a signal index and an Connection of receiver to sender's Connection map..." << endl;
//將訊號的索引和接收者的資訊儲存到訊號發射者的map容器中
sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
cout << "connected success." << endl;
}
}
void emitSignal()//公有測試函式,傳送一個訊號
{
cout << "emiting a signal..." << endl;
cpp_emit valueChanged();
}
cpp_signals:
void valueChanged();//訊號宣告
public cpp_slots:
void onValueChanged()//槽函式
{
cout << "Value Changed."<< endl;
}
friend class MetaObject;
private:
ConnectionMap connections;//連線鍵值對
};
#endif // OBJECT_H
moc_Object.cpp實現:
#include "Object.h"
//訊號的名稱
static const char signalNames[] = "valueChanged\n";
//槽的名稱
static const char slotNames[] = "onValueChanged\n";
//靜態元物件的填充
MetaObject Object::meta = { signalNames, slotNames };
//元方法呼叫函式的實現,根據連線的索引回撥槽函式
void Object::metacall(int idx)
{
switch (idx) {
case 0:
onValueChanged();
break;
default:
break;
};
}
//訊號的實現
void Object::valueChanged()
{
MetaObject::active(this, 0);
}
//啟用訊號
void MetaObject::active(Object* sender, int idx)
{
ConnectionMapIt it;
std::pair<ConnectionMapIt, ConnectionMapIt> ret;
ret = sender->connections.equal_range(idx);
for (it = ret.first; it != ret.second; ++it)
{
Connection c = (*it).second;
c.receiver->metacall(c.method);//根據索引呼叫元方法
}
}
2、訊號槽模擬使用
Main.cpp檔案:
#include <iostream>
#include "Object.h"
using namespace std;
int main(int argc, char *argv[])
{
char p[32] = SLOT(Object);
cout << "cur_value: " << p << endl;
Object obj1, obj2;
//連線訊號和槽
Object::cpp_connect(&obj1, SLOT(valueChanged), &obj2, SIGNAL(onValueChanged));
//發射一個訊號進行測試
obj1.emitSignal();
getchar();
return 0;
}
四、訊號槽的開源實現
1、sigslot
sigslot是訊號槽的一個非常精煉的C++實現,作者是Sarah Thompson,sigslot實現只有一個頭檔案sigslot.h,跨平臺且執行緒安全。在WebRTC中,sigslot .h是其基礎的事件處理框架, 在多個模組的訊息通知,響應處理中被使用。
sigslot庫官網:
http://sigslot.sourceforge.net/
Sigslot使用示例如下:
#include "sigslot.h"
#include <string>
#include <stdio.h>
#include <iostream>
#include <windows.h>
using namespace sigslot;
using namespace std;
class CSender
{
public:
sigslot::signal2<string, int> m_pfnsigDanger;
void Panic()
{
static int nVal = 0;
char szVal[20] = { 0 };
sprintf_s(szVal,20, "help--%d", nVal);
m_pfnsigDanger(szVal, nVal++);
}
};
class CReceiver :public sigslot::has_slots<>
{
public:
void OnDanger(string strMsg, int nVal)
{
//printf("%s ==> %d", strMsg.c_str(), nVal);
cout << strMsg.c_str() << " ==> " << nVal << endl;
}
};
int main()
{
CSender sender;
CReceiver recever;
cout << "create object ok..." << endl;
sender.m_pfnsigDanger.connect(&recever, &CReceiver::OnDanger);
cout << "connect succ!" << endl;
while (1)
{
cout << "in while..." << endl;
sender.Panic();
Sleep(2000);
cout << "end of sleep" << endl;
}
return 0;
}
如果在Qt工程中使用sigslot.h,sigslot.h中的emit函式名會和Qt中的emit巨集衝突,修改方法有兩個,一是將sigslot.h的emit改成其他名字,二是在.pro檔案中新增DEFINES+=QT_NO_EMIT,禁用Qt的emit巨集。
2、Boost.Signals
Boost.Signals實現了signals/slots模式,訊號(signals)被髮射,而插槽(slots)接收該訊號。
#include <iostream>
#include "boost/signals.hpp"
void firstSlot() {
std::cout << "void firstSlot()";
}
class secondSlot {
public:
void operator()() const {
std::cout <<
"void secondSlot::operator()() const ";
}
};
int main()
{
boost::signal<void ()> sig;
sig.connect(&firstSlot);
sig.connect(secondSlot());
std::cout << "Emitting a signal... ";
sig();
}
插槽函式的執行順序是隨機的,可以使用分組引數來控制呼叫順序。
sig.connect(1,&firstSlot);
sig.connect(2,secondSlot());
3、Qt訊號槽實現與Boost訊號槽實現的區別
Boost.Signals | Qt Signals 和 Slots |
---|---|
一個訊號就是一個物件 | 訊號只能是成員函式 |
發出訊號類似於函式呼叫 | 發出訊號類似於函式呼叫,Qt 提供了一個 emit 關鍵字來完成這個操作 |
訊號可以是全域性的、區域性的或者是成員物件 | 訊號只能是成員函式 |
任何能夠訪問到訊號物件的程式碼都可以發出訊號 | 只有訊號的擁有者才能發出訊號 |
槽是任何可被呼叫的函式或者函式物件 | 槽是經過特別設計的成員函式 |
可以有返回值,返回值可以在多個槽中使用 | 沒有返回值 |
同步的 | 同步的或者佇列的 |
非執行緒安全 | 執行緒安全,可以跨執行緒使用 |
當且僅當槽是可追蹤的時候,槽被銷燬時,連線自動斷開 | 槽被銷燬時,連線都會自動斷開(因為所有槽都是可追蹤的) |
型別安全(編譯器檢查) | 型別安全(執行期檢查) |
引數列表必須完全一致 | 槽可以忽略訊號中多餘的引數 |
訊號、槽可以是模板 | 訊號、槽不能是模板 |
C++ 直接實現 | 通過由 moc 生成的元物件實現(moc 以及元物件系統都是 C++ 直接實現的) |
沒有內省機制 | 可以通過內省發現,可以通過元物件呼叫,連線可以從資原始檔中自動推斷出 | <