一,、信號槽的基本概念 關(guān)于QT信號槽的基本概念大家都懂,,通過信號槽機制,QT使對象間的通信變得非常簡單: A對象聲明信號(signal),,B對象實現(xiàn)與之參數(shù)相匹配的槽(slot),,通過調(diào)用connect進行連接,合適的時機A對象使用emit把信號帶上參數(shù)發(fā)射出去,,B對象的槽會就接收到響應(yīng),。
信號槽機制有一些特點: 1. 類型安全:只有參數(shù)匹配的信號與槽才可以連接成功(信號的參數(shù)可以更多,,槽會忽略多余的參數(shù)),。 2. 線程安全:通過借助QT自已的事件機制,,信號槽支持跨線程并且可以保證線程安全。 3. 松耦合:信號不關(guān)心有哪些或者多少個對象與之連接,;槽不關(guān)心自己連接了哪些對象的哪些信號。這些都不會影響何時發(fā)出信號或者信號如何處理。 4. 信號與槽是多對多的關(guān)系:一個信號可以連接多個槽,一個槽也可以用來接收多個信號,。
使用這套機制,類需要繼承QObject并在類中聲明Q_OBJECT,。下面就對信號槽的實現(xiàn)做一些剖析,了解了這些在使用的時候就不會踩坑嘍,。 二,、信號與槽的定義 槽:用來接收信號,,可以被看作是普通成員函數(shù),可以被直接調(diào)用,。支持public,,protected,,private修飾,,用來定義可以調(diào)用連接到此槽的范圍。 1. public slots: 2. void testslot(const QString& strSeqId); 信號:只需要聲明信號名與參數(shù)列表即可,,就像是一個只有聲明沒有實現(xiàn)的成員函數(shù),。 1. signals: 2. void testsignal(const QString&); QT會在moc的cpp文件中實現(xiàn)它(參考下面代碼)。下面代碼中調(diào)用activate的第三個參數(shù)是類中信號的序列號,。 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. } 三,、信號槽的連接與觸發(fā) 通過調(diào)用connect()函數(shù)建立連接,會把連接信息保存在sender對象中,;調(diào)用desconnect()函數(shù)來取消。 connect函數(shù)的最后一個參數(shù)來用指定連接類型(因為有默認,,我們一般不填寫),,后面會再提到它。 1. static bool connect(const QObject *sender, const QMetaMethod &signal, 2. const QObject *receiver, const QMetaMethod &method, 3. Qt::ConnectionType type = Qt::AutoConnection); 一切就緒,,發(fā)射,!在sender對象中調(diào)用: 1. emit testsignal(“test”); 1. # define emit 上面代碼可以看到emit被定義為空,這樣在發(fā)射信號時就相當于直接調(diào)用QT為我們moc出來的函數(shù)testsignal(constQString & _t1),。 具體的操作由QMetaObject::activate()來處理:遍歷所有receiver并觸發(fā)它們的slots,。針對不同的連接類型,,這里的派發(fā)邏輯會有不同。 四,、不同的連接類型剖析 QueuedConnection:向receiver所在線程的消息循環(huán)發(fā)送事件,,此事件得到處理時會調(diào)用slot,像Win32的::PostMessage,。 BlockingQueuedConnection:處理方式和QueuedConnection相同,,但發(fā)送信號的線程會等待信號處理結(jié)束再繼續(xù),像Win32的::SendMessage,。 DirectConnection:在當前線程直接調(diào)用receiver的slot,,這種類型無法支持跨線程的通信。 AutoConnection:當前線程與receiver線程相同時,,直接調(diào)用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. // 接下來的代碼會直接在當前線程調(diào)用receiver的slot函數(shù) 五,、QT對象所屬線程的概念 這里要引入QObject的所屬線程概念,,看一下QObject的構(gòu)造函數(shù)(隨便選擇一個重載)就一目了然了。 如果指定父對象并且父對象的當前線程數(shù)據(jù)有效,,則繼承,,否則把創(chuàng)建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()的代碼可以看到,,除了信號觸發(fā)線程與接收者線程相同的情況能直接調(diào)用到slot,,其它情況都依賴事件機制,也就是說receiver線程必須要有QT的eventloop,,否則slot函數(shù)是沒有機會觸發(fā)的,! 當我們奇怪為什么信號發(fā)出slot卻不被觸發(fā)時,可以檢查一下是否涉及到跨線程,,接收者的線程是否存在激活的eventloop,。 所幸,我們可以通過調(diào)用QObject的方法movetothread,,來更換對象的所屬線程,,將有需求接收信號的對象轉(zhuǎn)移到擁有消息循環(huán)的線程中去以確保slot能正常工作。
有一個和對象所屬線程相關(guān)的坑:QObject::deletelater() ,。從源碼可以看出,,這個調(diào)用也只是發(fā)送了一個事件,等對象所屬線程的消息循環(huán)獲取控制權(quán)來處理這個事件時做真正的delete操作,。 所以調(diào)用這個方法要謹慎,,確保對象所屬線程具有激活的eventloop,不然這個對象就被泄露了,!
1. void QObject::deleteLater() 2. { 3. QCoreApplication::postEvent(this, new QEvent(QEvent::DeferredDelete)); 4. } 六,、強制線程切換 當對象中的一些接口需要確保在具有消息循環(huán)的線程中才能正確工作時,,可以在接口處進行線程切換,這樣無論調(diào)用者在什么線程都不會影響對象內(nèi)部的操作,。 下面的類就是利用信號槽機制來實現(xiàn)線程切換與同步,,所有對testMethod()的調(diào)用都會保證執(zhí)行在具有事件循環(huán)的線程中。 1. class CTestObject : public QObject 2. { 3. Q_OBJECT 4. 5. public: 6. CTestObject(QObject *parent = NULL) 7. : QObject(parent) 8. { 9. // 把自己轉(zhuǎn)移到帶有事件循環(huán)的QThread 10. this->moveToThread(&m_workThread); 11. 12. // 外部調(diào)用一律通過信號槽轉(zhuǎn)移到對象內(nèi)部的工作線程 13. // 連接類型選擇為Qt::BlockingQueuedConnection來達到同步調(diào)用的效果 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. // 如果調(diào)用已經(jīng)來自對象所屬線程,,直接處理 25. slotTestMethod(strArg); 26. } 27. else 28. { 29. // 通過發(fā)送信號,,實現(xiàn)切換線程處理 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. // 方法的具體實現(xiàn) 41. } 42. 43. private: 44. QThread m_workThread; 45. }; 七、參考資料 源碼參考:Qt\4.8.4\src\corelib\kernel\qobject.cpp 《InsideQt Series》系列文章:http://www./qt |
|