Qt 信号槽与原编译系统
Q_Object和signal,slot的实现原理
在Qt编程中,Q_OBJECT
宏、信号(signal)和槽(slot)是实现Qt对象间通信的核心机制。理解它们的实现原理对于深入掌握Qt框架非常重要。以下是对它们的分析,并结合实际的Qt5代码进行说明。
Q_OBJECT
宏
Q_OBJECT
宏通常在Qt对象的类定义中使用,它是Qt元对象系统的核心部分。它主要有两个功能:
- 启用信号和槽机制。
- 支持Qt的动态属性系统和运行时类型信息。
实现原理
Q_OBJECT
宏在编译时会触发moc(Meta-Object Compiler,元对象编译器)的处理。moc会生成一个与类同名的C++源文件(以.moc
为扩展名),其中包含该类的元数据(如信号和槽的注册信息)。
实际代码示例
// MyClass.h #includeclass MyClass : public QObject { Q_OBJECT public: MyClass(QObject *parent = nullptr); ~MyClass(); signals: void mySignal(); public slots: void mySlot(); };
在这个例子中,Q_OBJECT
宏告诉moc生成MyClass
的元数据。
信号和槽
信号和槽机制是Qt提供的一种对象间通信方式,信号用于发出事件通知,槽则是处理信号的函数。它们可以在不同对象间进行连接,槽函数会在信号发出时被调用。
实现原理
- 信号(signal):在头文件中声明信号时,信号实际上被moc工具处理为一个函数,函数体为空,但其存在让Qt的元对象系统能够将其识别为信号。
- 槽(slot):槽是普通的成员函数,但是在元对象系统中被注册为可响应信号的函数。
实际代码示例
// MyClass.cpp #include "MyClass.h" #includeMyClass::MyClass(QObject *parent) : QObject(parent) {} MyClass::~MyClass() {} void MyClass::mySlot() { std::cout << "Slot called!" << std::endl; } // main.cpp #include#include "MyClass.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); MyClass obj; QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot); emit obj.mySignal(); // This will call mySlot() and print "Slot called!" return app.exec(); }
运行机制
- moc工具:在编译时,moc工具会解析含有
Q_OBJECT
宏的类,并生成相应的元对象代码。该代码包含信号和槽的注册信息、动态属性和类型信息等。 - 运行时:Qt的
QObject
类及其子类会维护一个QMetaObject
,其中包含类的元数据。信号发射时,通过QMetaObject
找到相应的槽并调用。
QMetaObject设计
QMetaObject
是Qt元对象系统的核心类,它保存了Qt对象的元数据信息,例如类名、信号、槽、属性等。QMetaObject
使得Qt可以实现动态属性系统、信号与槽机制,以及类型信息的运行时访问。
QMetaObject的定义
class Q_CORE_EXPORT QMetaObject { public: struct Q_CORE_EXPORT Call {}; struct Q_CORE_EXPORT Connection {}; struct Q_CORE_EXPORT InvokeMetaMethod {}; struct Q_CORE_EXPORT Slot {}; struct Q_CORE_EXPORT Constructor {}; struct Q_CORE_EXPORT Destructor {}; const char *className() const; const QMetaObject *superClass() const; int methodOffset() const; int methodCount() const; int propertyOffset() const; int propertyCount() const; int signalOffset() const; int signalCount() const; // 获取方法、属性等的元数据 QMetaMethod method(int index) const; QMetaProperty property(int index) const; // 调用方法 void invokeMethod(QObject *object, const char *member, Qt::ConnectionType type, QGenericArgument val0 = QGenericArgument(0), QGenericArgument val1 = QGenericArgument(), ...); // 获取对象实例 QObject *newInstance(QGenericArgument val0 = QGenericArgument(0), QGenericArgument val1 = QGenericArgument(), ...) const; // 查找和连接信号与槽 int indexOfSignal(const char *signal) const; int indexOfSlot(const char *slot) const; // 静态成员 static const QMetaObject *staticMetaObject(const char *typeName); };
QMetaObject的组成部分
- 基本信息:返回类名和父类的元对象。
- 方法、属性的偏移和计数:返回方法、属性和信号的偏移和计数。
- 元数据获取:返回指定索引的方法和属性的元数据。
- 方法调用:通过元数据调用对象的方法,或创建对象实例。
- 信号和槽:返回信号和槽的索引。
QMetaObject的生成与使用
QMetaObject
的元数据通常由moc(Meta-Object Compiler)工具生成。moc工具解析含有Q_OBJECT
宏的类,并生成相应的元对象代码。
QMetaObject在运行时的使用
在运行时,QMetaObject
主要用于以下几种情况:
- 信号和槽的连接与调用:通过
QObject::connect
连接信号和槽,信号发出时通过QMetaObject
找到相应的槽并调用它。 - 属性系统:可以动态访问和修改对象的属性。
- 动态类型信息:通过
QMetaObject
可以获取类名、父类信息等。
信号发射函数 Q_EMIT 被定义为 空,是怎样调用到 MyClass::qt_metacall函数的
在Qt中,信号的发射和槽的调用是通过元对象系统和元对象编译器(moc)生成的代码实现的。尽管信号的定义通常为空函数,但信号的实际处理是在元对象系统中进行的。下面我们详细分析信号发射的过程以及MyClass::qt_metacall
函数的调用。
信号的定义和发射
当我们在类中定义一个信号时,实际上它只是一个声明,不会有具体的实现。例如:
signals: void mySignal();
这个信号在编译时通过moc工具生成相应的代码。moc工具会为信号生成一个唯一的标识符,并将其注册到元对象系统中。
信号的发射过程
当我们使用emit
关键字发射信号时,实际上调用的是QObject::activate
函数,这个函数负责处理信号的连接和调用。
QObject::activate 的实现
static void activate(QObject *sender, int signal_index, void **argv);
这个函数的实现大致如下:
void QObject::activate(QObject *sender, int signal_index, void **argv) { // 查找信号的连接列表 const QObjectPrivate *d = sender->d_func(); const QMetaObject *meta = sender->metaObject(); const QMetaObjectPrivate *priv = meta->d; QSignalSpyCallbackSet spyCallbacks = QSignalSpyCallbackSet(); // 获取信号的连接 QObjectPrivate::Connection *c = d->connections(signal_index); // 遍历所有连接的槽 while (c) { // 调用槽函数 c->receiver->qt_metacall(QMetaObject::InvokeMetaMethod, c->method_offset, argv); c = c->nextConnectionList; } }
qt_metacall 的实现
每个包含Q_OBJECT
宏的类都会由moc工具生成一个qt_metacall
函数。这个函数用于调用连接到信号的槽函数。
int MyClass::qt_metacall(QMetaObject::Call call, int id, void **argv) { id = QObject::qt_metacall(call, id, argv); if (id < 0) return id; if (call == QMetaObject::InvokeMetaMethod) { if (id < 1) { // 假设只有一个槽 void (MyClass::*_t)() = id ? nullptr : &MyClass::mySlot; if (_t) (this->*_t)(); id -= 1; } } return id; }
实际代码示例
// MyClass.h #include#includeclass MyClass : public QObject { Q_OBJECT public: MyClass(QObject *parent = nullptr); signals: void mySignal(); public slots: void mySlot() { std::cout << "Slot called!" << std::endl; } }; // MyClass.cpp #include "MyClass.h" // main.cpp #include#include "MyClass.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); MyClass obj; QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot); emit obj.mySignal(); // 这将调用 mySlot() 并打印 "Slot called!" return app.exec(); }
Qt::BlockingQueuedConnection 是怎样实现的
Qt::BlockingQueuedConnection
是 Qt 中的一种信号槽连接类型,它确保信号发射者和槽函数在不同线程时,发射信号的线程会等待槽函数执行完成。这种机制在需要同步跨线程操作时非常有用。
实现原理
- 信号发射线程发射信号时:
- 将信号的调用信息(包括槽函数的参数)封装成事件对象(
QMetaCallEvent
)。 - 将事件对象投递到槽函数所属线程的事件队列中。
- 等待槽函数执行完成。
- 槽函数所属线程处理事件时:
- 从事件队列中取出事件对象。
- 调用槽函数。
- 通知信号发射线程槽函数执行完成。
实际实现
信号发射线程部分
void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) { // ... 其他代码 ... // 创建 QMetaCallEvent 对象,封装信号调用信息 QMetaCallEvent *event = new QMetaCallEvent(sender, signal_index, meta, argv); // 获取槽函数所属的线程 QThread *receiverThread = c->receiver->thread(); if (receiverThread == QThread::currentThread()) { // 同一线程中直接调用 c->receiver->qt_metacall(QMetaObject::InvokeMetaMethod, c->method_offset, argv); } else { // 跨线程,使用 BlockingQueuedConnection QMutex mutex; QWaitCondition waitCondition; event->setWaitCondition(&mutex, &waitCondition); // 将事件投递到槽函数所属线程的事件队列中 QCoreApplication::postEvent(c->receiver, event); // 等待槽函数执行完成 mutex.lock(); while (!event->isHandled()) { waitCondition.wait(&mutex); } mutex.unlock(); } // ... 其他代码 ... }
槽函数所属线程部分
bool QCoreApplication::notify(QObject *receiver, QEvent *event) { // ... 其他代码 ... if (event->type() == QEvent::MetaCall) { QMetaCallEvent *metaCallEvent = static_cast(event); // 调用槽函数 receiver->qt_metacall(QMetaObject::InvokeMetaMethod, metaCallEvent->methodOffset(), metaCallEvent->args()); // 通知发射线程槽函数执行完成 if (metaCallEvent->isBlocking()) { QMutex *mutex = metaCallEvent->waitMutex(); QWaitCondition *waitCondition = metaCallEvent->waitCondition(); mutex->lock(); metaCallEvent->setHandled(true); waitCondition->wakeAll(); mutex->unlock(); } } // ... 其他代码 ... return false; }
QMetaCallEvent 类
class QMetaCallEvent : public QEvent { public: QMetaCallEvent(QObject *sender, int signalId, const QMetaObject *meta, void **args) : QEvent(QEvent::MetaCall), m_sender(sender), m_signalId(signalId), m_meta(meta), m_args(args), m_handled(false), m_waitCondition(nullptr), m_mutex(nullptr) {} void setWaitCondition(QMutex *mutex, QWaitCondition *waitCondition) { m_mutex = mutex; m_waitCondition = waitCondition; } bool isBlocking() const { return m_waitCondition != nullptr; } bool isHandled() const { return m_handled; } void setHandled(bool handled) { m_handled = handled; } QWaitCondition* waitCondition() const { return m_waitCondition; } QMutex* waitMutex() const { return m_mutex; } private: QObject *m_sender; int m_signalId; const QMetaObject *m_meta; void **m_args; bool m_handled; QWaitCondition *m_waitCondition; QMutex *m_mutex; };
Qt::QueuedConnection 是怎样实现的
Qt::QueuedConnection
的核心思想是将信号发射事件放入接收者线程的事件队列中,等待接收者线程的事件循环处理该事件。信号发射后立即返回,不等待槽函数的执行。
实现原理
- 信号发射线程发射信号时:
- 将信号的调用信息(包括槽函数的参数)封装成事件对象(
QMetaCallEvent
)。 - 将事件对象投递到槽函数所属线程的事件队列中。
- 信号发射线程立即返回,不等待槽函数执行。
- 槽函数所属线程处理事件时:
- 从事件队列中取出事件对象。
- 调用槽函数。
Qt::QueuedConnection 的实现
信号发射线程部分
void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) { // ... 其他代码 ... // 创建 QMetaCallEvent 对象,封装信号调用信息 QMetaCallEvent *event = new QMetaCallEvent(sender, signal_index, meta, argv); // 获取槽函数所属的线程 QThread *receiverThread = c->receiver->thread(); if (receiverThread == QThread::currentThread()) { // 同一线程中直接调用 c->receiver->qt_metacall(QMetaObject::InvokeMetaMethod, c->method_offset, argv); } else { // 跨线程,使用 QueuedConnection // 将事件投递到槽函数所属线程的事件队列中 QCoreApplication::postEvent(c->receiver, event); } // ... 其他代码 ... }
槽函数所属线程部分
bool QCoreApplication::notify(QObject *receiver, QEvent *event) { // ... 其他代码 ... if (event->type() == QEvent::MetaCall) { QMetaCallEvent *metaCallEvent = static_cast(event); // 调用槽函数 receiver->qt_metacall(QMetaObject::InvokeMetaMethod, metaCallEvent->methodOffset(), metaCallEvent->args()); } // ... 其他代码 ... return false; }
QObject::connect其他两种连接类型参数是怎样实现的?和上面又有什么区别
QObject::connect
函数提供了五种连接类型:Qt::AutoConnection
、Qt::DirectConnection
、Qt::QueuedConnection
、Qt::BlockingQueuedConnection
和 Qt::UniqueConnection
。前面我们已经讨论了 Qt::QueuedConnection
和 Qt::BlockingQueuedConnection
。接下来我们详细分析 Qt::AutoConnection
和 Qt::DirectConnection
的实现,以及它们与前两种连接类型的区别。
Qt::AutoConnection
Qt::AutoConnection
是默认的连接类型。它根据信号发射和槽函数是否在同一个线程中,自动选择使用 Qt::DirectConnection
或 Qt::QueuedConnection
。
实现原理
- 同一线程:
- 如果信号发射和槽函数在同一个线程中,使用
Qt::DirectConnection
。 - 不同线程:
- 如果信号发射和槽函数不在同一个线程中,使用
Qt::QueuedConnection
。
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::AutoConnection);
代码示例
void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) { // ... 其他代码 ... QThread *senderThread = QThread::currentThread(); QThread *receiverThread = receiver->thread(); if (connectionType == Qt::AutoConnection) { if (senderThread == receiverThread) { // 同一线程,使用 DirectConnection receiver->qt_metacall(QMetaObject::InvokeMetaMethod, methodOffset, argv); } else { // 不同线程,使用 QueuedConnection QMetaCallEvent *event = new QMetaCallEvent(sender, signal_index, meta, argv); QCoreApplication::postEvent(receiver, event); } } // ... 其他代码 ... }
Qt::DirectConnection
Qt::DirectConnection
直接在信号发射线程中调用槽函数。无论信号和槽是否在同一个线程中,都会同步执行槽函数。
实现原理
信号发射线程直接调用槽函数,不通过事件队列。
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::DirectConnection);
代码示例
void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) { // ... 其他代码 ... if (connectionType == Qt::DirectConnection) { // 直接调用槽函数 receiver->qt_metacall(QMetaObject::InvokeMetaMethod, methodOffset, argv); } // ... 其他代码 ... }
Qt::UniqueConnection
Qt::UniqueConnection
确保信号和槽之间只有一个连接。如果尝试创建重复的连接,会返回 false
,并且不会创建重复的连接。
实现原理
检查是否已有相同的连接,如果有则不创建新的连接。
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::UniqueConnection);
代码示例
bool QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type) { // ... 其他代码 ... if (type & Qt::UniqueConnection) { // 检查是否已经有相同的连接 if (isConnected(sender, signal, receiver, method)) { return false; } } // 根据类型创建连接 switch (type & ~Qt::UniqueConnection) { case Qt::AutoConnection: // 创建 AutoConnection break; case Qt::DirectConnection: // 创建 DirectConnection break; case Qt::QueuedConnection: // 创建 QueuedConnection break; case Qt::BlockingQueuedConnection: // 创建 BlockingQueuedConnection break; default: // 处理默认情况 break; } // ... 创建连接的代码 ... return true; }
检查是否已有相同连接
bool QObject::isConnected(const QObject *sender, const char *signal, const QObject *receiver, const char *method) { // 遍历所有连接,检查是否已有相同的连接 // 这里假设有一个连接列表 connections for (const auto &connection : connections) { if (connection.sender == sender && strcmp(connection.signal, signal) == 0 && connection.receiver == receiver && strcmp(connection.method, method) == 0) { return true; } } return false; }
使用示例
#include#include#includeclass MyObject : public QObject { Q_OBJECT public: MyObject(QObject *parent = nullptr) : QObject(parent) {} signals: void mySignal(); public slots: void mySlot1() { qDebug() << "Slot 1 called!"; } void mySlot2() { qDebug() << "Slot 2 called!"; } void mySlot3() { qDebug() << "Slot 3 called!"; } }; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); MyObject obj; // 使用 Qt::AutoConnection | Qt::UniqueConnection 连接信号和槽 QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot1, Qt::AutoConnection | Qt::UniqueConnection); QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot2, Qt::AutoConnection | Qt::UniqueConnection); QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot3, Qt::AutoConnection | Qt::UniqueConnection); emit obj.mySignal(); // 调用顺序不确定 return app.exec(); } #include "main.moc"
为什么说,如果一个信号连接了多个槽函数,emit时,槽函数的调用顺序是不确定的?
在 Qt 中,如果一个信号连接了多个槽函数,发射信号时槽函数的调用顺序是不确定的。这是因为 Qt 的信号槽机制并不保证槽函数的调用顺序。以下是一些原因和详细解释:
1. 连接顺序不确定
在信号与槽连接时,槽函数的连接顺序不一定是按代码中出现的顺序。不同的连接操作可能在不同的时间点执行,特别是在复杂的应用中,这可能会导致连接顺序的变化。
2. 多线程环境
在多线程环境中,信号和槽可能跨线程连接。信号发射时,槽函数的调用可能被安排在不同的线程中执行,这进一步增加了调用顺序的不确定性。例如,使用 Qt::QueuedConnection
时,槽函数会被放入槽所在线程的事件队列中,事件处理的顺序依赖于线程的事件循环。
3. 内部实现
Qt 的信号槽机制内部是通过链表或其他数据结构来维护连接关系的。这些数据结构在插入和遍历时可能不会保留插入的顺序。即使在单线程环境下,内部的实现细节也可能导致槽函数的调用顺序不确定。
4. 文档说明
Qt 的官方文档明确指出,不应该依赖槽函数的调用顺序,因为这一顺序在不同的版本或不同的运行环境中可能会变化。例如:
Note: The order in which the slots are called when multiple slots are connected to a signal is undefined.
实际代码示例
#include#include#includeclass MyObject : public QObject { Q_OBJECT public: MyObject(QObject *parent = nullptr) : QObject(parent) {} signals: void mySignal(); public slots: void mySlot1() { qDebug() << "Slot 1 called!"; } void mySlot2() { qDebug() << "Slot 2 called!"; } void mySlot3() { qDebug() << "Slot 3 called!"; } }; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); MyObject obj; QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot1); QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot2); QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot3); emit obj.mySignal(); // 调用顺序不确定 return app.exec(); } #include "main.moc"
总结
由于连接顺序、线程环境和内部实现的影响,信号连接多个槽函数时,槽函数的调用顺序是不确定的。因此,Qt 不保证槽函数的调用顺序,并且在不同版本或环境中可能会变化。因此,依赖槽函数调用顺序的代码设计是不可靠的,应该避免。