一、信号槽的基本概念
关于QT信号槽的基本概念大家都懂,通过信号槽机制,QT使对象间的通信变得非常简单:
A对象声明信号(signal),B对象实现与之参数相匹配的槽(slot),通过调用connect进行连接,合适的时机A对象使用emit把信号带上参数发射出去,B对象的槽会就接收到响应。
信号槽机制有一些特点:
1. 类型安全:只有参数匹配的信号与槽才可以连接成功(信号的参数可以更多,槽会忽略多余的参数)。
2. 线程安全:通过借助QT自已的事件机制,信号槽支持跨线程并且可以保证线程安全。
3. 松耦合:信号不关心有哪些或者多少个对象与之连接;槽不关心自己连接了哪些对象的哪些信号。这些都不会影响何时发出信号或者信号如何处理。
4. 信号与槽是多对多的关系:一个信号可以连接多个槽,一个槽也可以用来接收多个信号。
使用这套机制,类需要继承QObject并在类中声明Q_OBJECT。下面就对信号槽的实现做一些剖析,了解了这些在使用的时候就不会踩坑喽。
二、信号与槽的定义
槽:用来接收信号,可以被看作是普通成员函数,可以被直接调用。支持public,protected,private修饰,用来定义可以调用连接到此槽的范围。
1. public slots:
2. void testslot(const QString& strSeqId);
信号:只需要声明信号名与参数列表即可,就像是一个只有声明没有实现的成员函数。
1. signals:
2. void testsignal(const QString&);
QT会在moc的cpp文件中实现它(参考下面代码)。下面代码中调用activate的第三个参数是类中信号的序列号。
1. // SIGNAL 0
2. void CTestObject:: testsignal (const QString & _t1)
3. {
4. void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
5. QMetaObject::activate(this, &staticMetaObject, 0, _a);
6. }
三、信号槽的连接与触发
通过调用connect()函数建立连接,会把连接信息保存在sender对象中;调用desconnect()函数来取消。
connect函数的最后一个参数来用指定连接类型(因为有默认,我们一般不填写),后面会再提到它。
1. static bool connect(const QObject *sender, const QMetaMethod &signal,
2. const QObject *receiver, const QMetaMethod &method,
3. Qt::ConnectionType type = Qt::AutoConnection);
一切就绪,发射!在sender对象中调用:
1. emit testsignal(“test”);
1. # define emit
上面代码可以看到emit被定义为空,这样在发射信号时就相当于直接调用QT为我们moc出来的函数testsignal(constQString & _t1)。
具体的操作由QMetaObject::activate()来处理:遍历所有receiver并触发它们的slots。针对不同的连接类型,这里的派发逻辑会有不同。
四、不同的连接类型剖析
QueuedConnection:向receiver所在线程的消息循环发送事件,此事件得到处理时会调用slot,像Win32的::PostMessage。
BlockingQueuedConnection:处理方式和QueuedConnection相同,但发送信号的线程会等待信号处理结束再继续,像Win32的::SendMessage。
DirectConnection:在当前线程直接调用receiver的slot,这种类型无法支持跨线程的通信。
AutoConnection:当前线程与receiver线程相同时,直接调用slot,否则同QueuedConnection类型。
1. QObject * const receiver = c->receiver;
2. const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId;
3.
4. // determine if this connection should be sent immediately or
5. // put into the event queue
6. if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
7. || (c->connectionType == Qt::QueuedConnection)) {
8. queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);
9. continue;
10. #ifndef QT_NO_THREAD
11. } else if (c->connectionType == Qt::BlockingQueuedConnection) {
12. locker.unlock();
13. if (receiverInSameThread) {
14. qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
15. "Sender is %s(%p), receiver is %s(%p)",
16. sender->metaObject()->className(), sender,
17. receiver->metaObject()->className(), receiver);
18. }
19. QSemaphore semaphore;
20. QCoreApplication::postEvent(receiver, new QMetaCallEvent(c->method_offset, c->method_relative,
21. c->callFunction,
22. sender, signal_absolute_index,
23. 0, 0,
24. argv ? argv : empty_argv,
25. &semaphore));
26. semaphore.acquire();
27. locker.relock();
28. continue;
29. #endif
30. }
31.
32. // 接下来的代码会直接在当前线程调用receiver的slot函数
五、QT对象所属线程的概念
这里要引入QObject的所属线程概念,看一下QObject的构造函数(随便选择一个重载)就一目了然了。
如果指定父对象并且父对象的当前线程数据有效,则继承,否则把创建QObject的线程作为所属线程。
1. QObject::QObject(QObject *parent)
2. : d_ptr(new QObjectPrivate)
3. {
4. Q_D(QObject);
5. d_ptr->q_ptr = this;
6. d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current();
7. d->threadData->ref();
8. if (parent) {
9. QT_TRY {
10. if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))
11. parent = 0;
12. setParent(parent);
13. } QT_CATCH(...) {
14. d->threadData->deref();
15. QT_RETHROW;
16. }
17. }
18. qt_addObject(this);
19. }
通过activate()的代码可以看到,除了信号触发线程与接收者线程相同的情况能直接调用到slot,其它情况都依赖事件机制,也就是说receiver线程必须要有QT的eventloop,否则slot函数是没有机会触发的!
当我们奇怪为什么信号发出slot却不被触发时,可以检查一下是否涉及到跨线程,接收者的线程是否存在激活的eventloop。
所幸,我们可以通过调用QObject的方法movetothread,来更换对象的所属线程,将有需求接收信号的对象转移到拥有消息循环的线程中去以确保slot能正常工作。
有一个和对象所属线程相关的坑:QObject::deletelater() 。从源码可以看出,这个调用也只是发送了一个事件,等对象所属线程的消息循环获取控制权来处理这个事件时做真正的delete操作。
所以调用这个方法要谨慎,确保对象所属线程具有激活的eventloop,不然这个对象就被泄露了!
1. void QObject::deleteLater()
2. {
3. QCoreApplication::postEvent(this, new QEvent(QEvent::DeferredDelete));
4. }
六、强制线程切换
当对象中的一些接口需要确保在具有消息循环的线程中才能正确工作时,可以在接口处进行线程切换,这样无论调用者在什么线程都不会影响对象内部的操作。
下面的类就是利用信号槽机制来实现线程切换与同步,所有对testMethod()的调用都会保证执行在具有事件循环的线程中。
1. class CTestObject : public QObject
2. {
3. Q_OBJECT
4.
5. public:
6. CTestObject(QObject *parent = NULL)
7. : QObject(parent)
8. {
9. // 把自己转移到带有事件循环的QThread
10. this->moveToThread(&m_workThread);
11.
12. // 外部调用一律通过信号槽转移到对象内部的工作线程
13. // 连接类型选择为Qt::BlockingQueuedConnection来达到同步调用的效果
14. connect(this, SIGNAL(signalTestMethod(const QString &)), this, SLOT(slotTestMethod(const QString &)), Qt::BlockingQueuedConnection);
15.
16. m_workThread.start();
17. }
18. ~CTestObject();
19.
20. void testMethod(const QString& strArg)
21. {
22. if (QThread::currentThreadId() == this->d_func()->threadData->threadId)
23. {
24. // 如果调用已经来自对象所属线程,直接处理
25. slotTestMethod(strArg);
26. }
27. else
28. {
29. // 通过发送信号,实现切换线程处理
30. emit signalTestMethod(strArg);
31. }
32. }
33.
34. signals:
35. void signalTestMethod(const QString&);
36.
37. private slots:
38. void slotTestMethod(const QString& strArg)
39. {
40. // 方法的具体实现
41. }
42.
43. private:
44. QThread m_workThread;
45. };