因英语水平有限,但是发现该文讲的很是精彩,故转载翻译过来,如有疏漏错误的地方请多多指正
原文出处:http://woboq.com/blog/how-qt-signals-slots-work.html
1.序言
Qt众所周知的就是他的信号和槽的机制,但是它是怎么工作的呢?在这篇博文中,我们将深入探索一下信号与槽机值里面的的QObject与QMetaObject底层是如何实现的。
在这片文章中,我(作者)将展示QT5的代码,有些代码为了简洁与格式的需要,做了一定的修改。
2.信号与槽(Signals and Slots)
首先让我们来重新看一下Qt官方的信号与槽的例子
头文件里面大致时这样子的:
<span style="font-family:Comic Sans MS;">class Counter : public QObject
{
Q_OBJECT
int m_value;
public:
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
};
Somewhere in the .cpp file, we implement setValue()
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
</span>
那么这个Counter类的信号与槽可以这样来使用:
<span style="font-family:Comic Sans MS;">Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));
a.setValue(12); // a.value() == 12, b.value() == 12</span>
这样连接信号与槽的写法从1992年Qt创建开始就从未被更改过。
尽管API借口从未发生改变,但是底层的实现已经历经几次更改,一些新的特性已经加入到底层的实现之中,这也不是很难懂的,我将在这篇文章中展示它是如何工作的。
3.MOC,元对象编译器(Meta Object Complier)
Qt的信号与槽和属性系统都是基于对象在运行时的内省(introspection),内省功能是对于实现信号和槽时必须的,并且允许应用程序的开发人员在运行时获得有关QObject的子类的“元信息”,包括一个含有对象的类名以及它所支持的信号与槽的列表。这一机制也广泛的支持属性和文本翻译,同时也为QtScript和QML奠定了基础。
C++自身时不提供内省的,所以Qt提供了一个工具,就是MOC。该工具将会自动的注入一些元对象的必要代码(不像是用户主动的调用与处理器一样)。
元对象编译器解析头文件(包含类,比如上面定义的Counter)并且自动生成一个额外的C++文件,该文件将会和客户编写的代码一起进行编译链接,这个新产生的C++源文件将包含内省需要的的所有信息。
Qt有时会因为这个额外的自动生成代码的机制被那些所谓的纯粹的语言学家批评,那是因为他们没有看过这篇文章《Why Doesn't Qt Use Templates for Signals and Slots?》,MOC对于Qt来说不仅没有拖了后腿,而且还使其如虎添翼。
4.神奇的宏定义(Magic Macros)
我们能列举出在Qt里面不是C++原生的关键字么?signals,slots,Q_OBJECT,emit,SIGNAL,SLOT,这些被当做是QT对于C++的一种拓展,这些个宏定义在Qt的源码文件qobjectdefs.h(/usr/include/qt4/QtCore/qobjectdefs.h)里面。
<span style="font-family:Comic Sans MS;">#define signals public
#define slots /* nothing */</span>
这是对的,信号和槽是普通的函数,编译器将像其他不同的函数一样处理他们,这些宏定义将会通过MOC展示出来。在5.0版本之前,信号是被保护(protected:)起来的,为了适应Qt5里面的新语法,信号将变成公共的(public:)。
下面我们来展开qobjectdefs.h里面的内容看一下:
<span style="font-family:Comic Sans MS;">#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 **);
</span>
Q_OBJECT声明了一些函数以及一个QMetaObject,这些函数将会在MOC自动生成的C++代码里面进行定义。
<span style="font-family:Comic Sans MS;">#define emit /* nothing */</span>
emit是一个空的宏定义,这个宏定义不被MOC解析,换句话说,emit关键字时一个可选的选项,这样写可以使程序更好的表达它的意思。
<span style="font-family:Comic Sans MS;">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</span>
这些宏定义就是为了在预处理的过程中将参数转化为字符串(#在预处理的过程中可将参数转化为字符串),并加入一些码制在字符串的前面。
在调试模式的时候,我们也会注释掉文件定位信息这一行(
<span style="font-family:Comic Sans MS;"># define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)</span>
),这样如果信号与槽不正常工作的话,那么也会正常的发出警告信息。这个方法在Qt4.5的版本里面就已经包含了(译者注:这个怎么实现呀?,知道的告知一下),为了知道信号与槽的函数对应代码中的哪一行,我们用qFlagLocation函数来将SLOT(a)字符串和SIGNAL(a)字符串(字符串里面包含了信号与槽的函数名字以及各自的行号)的地址注册一下,方便后面查找信号和槽的需要。下面给出qFlagLocation函数:
<span style="font-family:Comic Sans MS;">const char *qFlagLocation(const char *method)
{
static int idx = 0;//static类型的,每次进入该函数都会使用上一次退出该函数时的值
flagged_locations[idx] = method;
idx = (idx+1) % flagged_locations_count;
return method;
}</span>
5.MOC自动生成的代码(MOC Generated Code)
QMetaObject
<span style="font-family:Comic Sans MS;">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; }</span>
我们看到了这里实现了Counter::metaObject()和Counter::staticMetaObject,他们在一开始Q_OBJECT宏里面就已经定义了。QObject::d_ptr->metaObject仅仅只有在QML里面使用,所以一般来说,虚函数metaObject()只是返回staticMetaObject这个类,而且staticMetaObject实在只读数据区里面构建的。
QMetaObject这个类定义在qobjectdefs.h这个文件中:
<span style="font-family:Comic Sans MS;">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; };</span>
d表明所有的成员应该都是私有的(private),为了满足POD(译者注:Plain old data structure, 缩写为POD, 是C++语言的标准中定义的一类数据结构[1],POD适用于需要明确的数据底层操作的系统中。POD通常被用在系统的边界处,即指不同系统之间只能以底层数据的形式进行交互,系统的高层逻辑不能互相兼容。比如当对象的字段值是从外部数据中构建时,系统还没有办法对对象进行语义检查和解释,这时就适用POD来存储数据。)和允许静态初始化的需求,这里将他们声明成public的。
QMetaObject被父对象的元数据(superdata)初始化(QObject::staticMetaObject也是这样),stringdata和data被一些文章后面提到的一些数据初始化,static_metacall是一个函数指针,且被初始化为Counter::qt_static_metacall.
(内省表)Introspection Tables
<span style="font-family:Comic Sans MS;">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
};</span>
一开始的13个int类型的数据组成了头部,当有两列的情况下,第一行表示个数,第二行表示在项描述数组里面的开始索引。
字符串表(String Table)
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_t qt_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
这里的工作基本上就是将字符串 "Counter\0valueChanged\0\0newValue\0setValue\0""value\0"里面的以'\0'为分割符,一个一个的放在QByteArray类型的数组里面。
信号(Signals)
MOC也实现了信号,他们只是一些简单的函数,只要负责创建一个指向参数的数组并将他们传递给QMetaObject::activate()函数,第一个是函数的返回值,在我们的例子里面,因为函数的返回值是void,所以把他设为0,第三个传给activate函数的参数是信号索引值index(这里是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);
}
调用一个槽(Calling a Slot)
在qt_static_metacall里面通过索引来调用一个槽也是可能的:void Counter::qt_static_metacall(QObject *_o, QMetaObject::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。
索引注意事项( Note About Indexes)
连接是如何工作的(How Connecting Works)
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;
}
}
}
信号的发射(Signal Emission)
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_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, int signalOffset, int local_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
/* ... Skipped some debugging and QML hooks, and some sanity check ... */
/* We lock a mutex because all operations in the connectionLists are thread safe */
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 */
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;
const bool receiverInSameThread = QThread::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)) {
/* 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::StaticMetaCallFunction callFunction = 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);
}