您的位置:首页 > 编程语言 > Qt开发

Qt信号与槽工作机制--译文

2015-05-28 21:05 459 查看
本文翻译自How Qt Signals and Slots Work。如有错误,敬请指正。

Qt is well known for its signals and slots mechanism. But how does it work? In this blog post, we will explore the internals of QObject and QMetaObject and discover how signals and slot work under the hood.In this blog article, I show portions of Qt5 code, sometimes edited for formatting and brevity.

Qt因为它的信号与槽机制而出名,但是它是怎样工作的呢?在本文中,我们将探索QObject和QMetaObject的内在联系以及发现在该联系下信号与槽是怎样工作的。在本文中,我将展示部分Qt5的代码,有时为了格式以及让表达简洁会进行适当的编辑。

Signals and Slots

First, let us recall how signals and slots look like by showing the official example.The header looks like this:

首先让我们通过展示官方例程来回忆一下信号与槽长什么样。

头文件如下:

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);
};


cpp文件如下:

void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}


其他类就可以像下面列举的一样使用Counter类。

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

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


This is the original syntax that has almost not changed since the beginning of Qt in 1992.

But even if the basic API has not changed since the beginning, its implementation has been changed several times. New features have been added and a lot happened under the hood. There is no magic involved and this blog post will show you how it works.

这个语法从1992年Qt开始基本上就没有变过。即使这个基础的API从开始没有改变,但是它的实现方式已经改动过好几次了。在其里面新的特性已经被添加进去。这没有什么神秘之处,本文将想你展示信号与槽是怎样工作的。

MOC, the Meta Object Compiler

The Qt signals/slots and property system are based on the ability to introspect the objects at runtime. Introspection means being able to list the methods and properties of an object and have all kinds of information about them such as the type of their arguments.

QtScript and QML would have hardly been possible without that ability.

Qt的信号与槽以及属性系统是基于在运行期间对对象的内省(introspect )能力。内省(introspect )意味着列出对象的方法以及属性还有关于它们的各种信息,比如参数的类型。

如果没有内省(introspect )的功能,QtScript和QML将很难实现。

C++ does not offer introspection support natively, so Qt comes with a tool to provide it. That tool is MOC. It is a code generator (and NOT a preprocessor like some people call it).

C++并没有提供原生的内省支持,所以Qt使用一个工具为内省提供支持。工具就是MOC,它是个代码生成器(不是人们所说的预处理器)。

It parses the header files and generates an additional C++ file that is compiled with the rest of the program. That generated C++ file contains all the information required for the introspection.

MOC从语法上解析头文件并生成额外的C++文件,该文件与其余的程序一起编译。生成C++文件包含内省所需要的所有信息。

Qt has sometimes been criticized by language purists because of this extra code generator. I will let the Qt documentation respond to this criticism. There is nothing wrong with code generators and the MOC is of a great help.

Qt有时候会因为额外的代码生成并且为了保持语言的纯正而变得严格。我将引用Qt的文档以此来解释其严苛性。虽然很对语法要求严苛,但是代码生成器是没有错的并且MOC是有很大的帮助。

Magic Macros

Can you spot the keywords that are not pure C++ keywords?
signals, slots, Q_OBJECT, emit, SIGNAL, SLOT
. Those are known as the Qt extension to C++. They are in fact simple macros, defined in qobjectdefs.h

你有注意到Qt有的关键字但是C++没有的吗?
signals, slots, Q_OBJECT, emit, SIGNAL, SLOT
。这些都是Qt扩展C++而来的。他们事实上是定义在qobjectdefs.h中简单的宏。

#define signals public
#define slots /* nothing */


That is right, signals and slots are simple functions: the compiler will handle them them like any other functions. The macros still serve a purpose though: the MOC will see them.

Signals were protected in Qt4 and before. They are becoming public in Qt5 in order to enable the new syntax.

signals和slots是一个简单的函数,编译器处理它们就像处理其他函数一样。那些宏就只有一个目的:让MOC能够识别它们。

在Qt4和之前的版本中,Signals是protectd的,但是在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.

Q_OBJECT 定义了一些函数和一个静态的
QMetaObject
。这些函数在MOC生成的C++文件中实现。

#define emit /* nothing */


emit is an empty macro. It is not even parsed by MOC. In other words, emit is just optional and means nothing (except being a hint to the developer).

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


Those macros just use the preprocessor to convert the parameter into a string, and add a code in front.

In debug mode we also annotate the string with the file location for a warning message if the signal connection did not work. This was added in Qt 4.5 in a compatible way. In order to know which strings have the line information, we use
qFlagLocation
which will register the string address in a table with two entries.

这些宏仅仅是使用预处理器将参数转换为string并且在其前面添加代码。

在debug模式下,如果信号与槽的连接无法工作,我们会在string添加文件位置的注释给开发者提供警告信息。这个功能使用一种相互兼容的方式添加在Qt4.5版本中。为了知道哪个string的行号信息,我们使用
qFlagLocation
qFlagLocation
会在两个条目的表格中注册string的地址的变量。

MOC Generated Code

We will now go over portion of the code generated by moc in Qt5.

我们接下来将深入使用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;
}


We see here the implementation of
Counter::metaObject()
and
Counter::staticMetaObject
. They are declared in the Q_OBJECT macro.
QObject::d_ptr->metaObject
is only used for dynamic meta objects (QML Objects), so in general, the virtual function
metaObject()
just returns the
staticMetaObject
of the class.

在这里我们将看到
Counter::metaObject()
Counter::staticMetaObject
的具体实现。它们是在Q_OBJECT 这个宏中声明的。
QObject::d_ptr->metaObject
仅仅用于动态的元对象(如QML对象),所以,虚函数
metaObject()
仅仅返回
staticMetaObject
类。

The staticMetaObject is constructed in the read-only data. QMetaObject as defined in qobjectdefs.h looks like this:

staticMetaObject
是由一些只读数据构造的。
QMetaObject
定义在qobjectdefs.h中,就像下面的样子:

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;
};


The
d
indirection is there to symbolize that all the member should be private. They are not private in order to keep it a POD and allow static initialization.

d
间接的去标记所有的成员本应private的。但为了保持POD和允许静态初始化,它们都不是private。

The
QMetaObject
is initialized with the meta object of the parent object (
QObject::staticMetaObject
in this case) as
superdata. stringdata
and
data
are initialized with some data explained later in this article.
static_metacall
is a function pointer initialized to
Counter::qt_static_metacall
.

QMetaObject
使用元对象的父对象初始化(在本例中是
QObject::staticMetaObject
)。像
superdata. stringdata
and
data
被某些值初始化,稍后我们会提到。
static_metacall
是一个函数指针,初始化为
Counter::qt_static_metacall


Introspection Tables

First, let us analyze the integer data of QMetaObject.

首先让我们分析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
};


The first 13
`int
consists of the header. When there are two columns, the first column is the count and the second column is the index in this array where the description starts.

In this case we have 2 methods, and the methods description starts at index 14.

前13个
int
由头文件组成。共2列,第1列是计数,第2列标记在这个数组中描述开始的索引。

在这个例子中,我们有2个方法,方法的描述从数组中的第14开始。

The method descriptions are composed of 5 int. The first one is the name, it is an index in the string table (we will look into the details later). The second integer is the number of parameters, followed by the index at which one can find the parameter description. We will ignore the tag and flags for now. For each function, moc also saves the return type of each parameter, their type and index to the name.

方法的描述由5个int组成,依次是name,该名字在字符串列表(稍后将会看到)中的索引值。第2个是标记参数个数的整型值argc,接下来是可以找到参数的描述的索引值。我们将忽略tag和flags。在每个函数中,moc将保存每个参数返回值的类型、参数的类型和名字name在数组中的index。

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


This is basically a static array of
QByteArray
. the
QT_MOC_LITERAL
macro creates a static
QByteArray
that references a particular index in the string below.

这是个基本的
QByteArray
QT_MOC_LITERAL
这个宏创建一个在下面的字符串中引用特定的index作为静态的
QByteArray


Signals

The MOC also implements the signals. They are simple functions that just create an array of pointers to the arguments and pass that to
QMetaObject::activate
. The first element of the array is the return value. In our example it is 0 because the return value is void.

MOC同样也实现singals函数。singals是一个简单的函数,仅仅是创建指向数组的指针,该数组存放signals函数的参数并将其传递给
QMetaObject::activate
。数组的第1个元素是返回值,在我们的例程中0代表返回值是void。

The 3rd parameter passed to activate is the signal index (0 in that case).

第3个参数传递给activate是信号的索引值(本例中是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

It is also possible to call a slot by its index in the
qt_static_metacall
function:

qt_static_metacall
函数中可通过它的index去调用一个slot函数。

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: ;
}
}


The array pointers to the argument is the same format as the one used for the signal. _a[0] is not touched because everything here returns void.

参数是指向的指针与前面用过的signals一样格式。_a[0] 我们并不关心,因为在这里所有的返回值都是void。

A Note About Indexes.

In each QMetaObject, the slots, signals and other invokable methods of that object are given an index, starting from 0. They are ordered so that the signals come first, then the slots and then the other methods. This index is called internally the relative index. They do not include the indexes of the parents.

在每个QMetaObject,slots, signals和其他调用的方法的类里面都有相应的从0开始的索引。它们按顺序排放,signals是第一个,然后是slots,接下来是其他的方法。该索引被称作内部的相对索引。它们并不包括父类的索引。

But in general, we do not want to know a more global index that is not relative to a particular class, but include all the other methods in the inheritance chain. To that, we just add an offset to that relative index and get the absolute index. It is the index used in the public API, returned by functions like
QMetaObject::indexOf{Signal,Slot,Method}


但是通常来说,我们并不想要去知道一个与特定类相对应的全局索引,只关心在继承结构中的索引。因此,我们只在相对的索引中添加偏移量因此得到绝对的索引。在公共的API中使用这个索引值,
QMetaObject::indexOf{Signal,Slot,Method}
返回的是索引值。

The connection mechanism uses a vector indexed by signals. But all the slots waste space in the vector and there are usually more slots than signals in an object. So from Qt 4.6, a new internal signal index which only includes the signal index is used.

信号与槽连接的机制是使用一个向量,该向量是由信号来做索引的。但是所有的slots在向量中都会浪费空间并且在对象中,slots总是比signals多。所以从Qt4.6开始,一个内在的信号索引仅包括那个被使用的信号的索引。

While developing with Qt, you only need to know about the absolute method index. But while browsing the Qt’s QObject source code, you must be aware of the difference between those three.

当在使用Qt开发时,你只要知道关于方法的绝对索引值。但是当查看Qt的QObject源码时,你必须要注意这三者之间的差别。

How Connecting Works

The first thing Qt does when doing a connection is to find out the index of the signal and the slot. Qt will look up in the string tables of the meta object to find the corresponding indexes.

当信号与槽连接时,Qt做的第一件事就是找出signal和slot的索引值。Qt将会从元对象中的字符串列表中找出正确的索引值。

Then a
QObjectPrivate::Connection
object is created and added in the internal linked lists.

之后就会在内部链表里面创建和添加一个
QObjectPrivate::Connection
的对象。

What information needs to be stored for each connection? We need a way to quickly access the connections for a given signal index. Since there can be several slots connected to the same signal, we need for each signal to have a list of the connected slots. Each connection must contain the receiver object, and the index of the slot. We also want the connections to be automatically destroyed when the receiver is destroyed, so each receiver object needs to know who is connected to him so he can clear the connection.

在每个signal与slot的连接中都保存什么信息呢?我们需要使用一种方式能够快速的从给定的signal的索引值去访问连接。由于一个signal可以连接多个slots,所以我们必须为每个signal创建一个之连接的slots的列表。当接收的类被销毁,我们也需要这些连接能够自动的销毁。为了能够清除连接,因此每个接收的对象需要知道是谁与之连接。

Here is the
QObjectPrivate::Connection
as defined in qobject_p.h :

以下是定义在qobject_p.h中的
QObjectPrivate::Connection
.

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;
}
}
};


Each object has then a connection vector: It is a vector which associates for each of the signals a linked lists of
QObjectPrivate::Connection
.

每个对象都有一个连接的向量:该向量与每个信号的
QObjectPrivate::Connection
类型链表相关联。

Each object also has a reversed lists of connections the object is connected to for automatic deletion. It is a doubly linked list.

每个对象也保留一个连接的对象的链表,主要是为了能够自动删除,这是个双向链表。



Linked lists are used because they allow to quickly add and remove objects. They are implemented by having the pointers to the next/previous nodes within
QObjectPrivate::Connection


Note that the prev pointer of the senderList is a pointer to a pointer. That is because we don’t really point to the previous node, but rather to the pointer to the next in the previous node. This pointer is only used when the connection is destroyed, and not to iterate backwards. It allows not to have a special case for the first item.

使用链表是因为我们需要快速的添加和删除对象。是由指向前面或者后面节点的
QObjectPrivate::Connection
的指针实现的。我们注意到:在发送链表中的指向前面的是一个指向指针的指针。是因为我们并不真正的要去指向前面的节点,我们是要指向前面节点的指向下一个节点的指针。这个指针只有在连接删除的时候才使用到,并且不能向后迭代。在第一个类型中并不允许使用特别的例子。



Signal Emission

When we call a signal, we have seen that it calls the MOC generated code which calls
QMetaObject::activate
.

我们调用一个signals,我们前面看到它是调用MOC产生一个叫做
QMetaObject::activate
的代码。

Here is an annotated version of its implementation from qobject.cpp

下面的是在qobject.cpp中添加注释的
QMetaObject::activate
版本。

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;

/* 判断是否有与该信号相连接的接收对象 */
if (!sender->d_func()->isSignalConnected(signal_index))
return; // nothing connected to these signals, and no spy

/* 省略一些调试代码以及QML的相关代码以及一些常规检测 */

/* 给信号量加锁,因为在这connectionLists 里所有的操作都是线程安全的 */
QMutexLocker locker(signalSlotLock(sender));

/* 获取与该信号相关的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) {
/* ... Skipped....  Qt5-style connection to function pointer */
} else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
/* 如果我们有callFunction(一个指向qt_static_metacall的指针,该指针由MOC产生的),我们将会调用它。我们也需要去检车保存的metodOffset 是否是有效的。 */
locker.unlock(); // 在调用实际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();
}

// 检查该对象没有被槽函数删除
if (connectionLists->orphaned) break;
} while (c != last && (c = c->nextConnectionList) != 0);
}


Conclusion

We saw how connections are made and how signals slots are emitted. What we have not seen is the implementation of the new Qt5 syntax, but that will be for another post.

我们看到了连接是怎么建立的以及signals是怎么发射的。但是我们并不知道在Qt5的信誉发中是怎么实现的,我们将在其他的文章中展示出来。

Update: The part 2 is available.

更新:第2篇文章How Qt Signals and Slots Work - Part 2 - Qt5 New Syntax已更新完毕。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: