QT原始碼分析:QObject
QT框架裡面最大的特色就是在C++的基礎上增加了元物件系統(Meta-Object System),而元物件系統裡面最重要的內容就是訊號與槽機制,這個機制是在C++語法的基礎上實現的,使用了函式、函式指標、回撥函式等概念。當然與我們自己去寫函式所不同的是槽與訊號機制會自動幫我們生成部分程式碼,比如我們寫的訊號函式就不需要寫它的實現部分,這是因為在我們編譯程式的時候,編譯器會自動生成這一部分程式碼,當我們呼叫connect函式的時候,系統會自動將訊號函式與槽函式相連線,於是當我們呼叫訊號函式的時候,系統就會自動回撥槽函式,不管你是在同一執行緒下呼叫或者在不同執行緒下呼叫,系統都會自動評估,並在合理的時候觸發函式,以此來保證執行緒的安全。訊號與槽機制是執行緒安全的,這可以使得我們在呼叫的時候不用再額外的增加過多保證執行緒同步的程式碼,為了實現元物件系統,QT把所有相關實現寫在了QObject類中,所以當你想使用元物件系統的時候,你所寫的類需要繼承自QObject,包括QT自帶的所有類都是繼承自QObject,所以分析QObject的程式碼,對了解QT的元物件機制有非常大的幫助,我並不打算把QObject類的每一行程式碼都寫下來,只想把其中比較關鍵的內容或者對分析QT原始碼有幫助的內容介紹一下。
1.巨集Q_OBJECT
這個巨集展開以後是如下定義:
#define Q_OBJECT \ public: \ QT_WARNING_PUSH \ Q_OBJECT_NO_OVERRIDE_WARNING \ 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 \ private: \ Q_OBJECT_NO_ATTRIBUTES_WARNING \ Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \ QT_WARNING_POP \ struct QPrivateSignal {}; \ QT_ANNOTATE_CLASS(qt_qobject, "")
你可以看到這個巨集定義了一些函式,並且函式名都帶有meta,所以不難猜到這些函式和QT的元物件系統是有關係的,實際上你在qobject.cpp裡面是找不到這些函式的實現的,它們的實現都在moc_qobject.cpp裡面。QT的元物件系統是這樣處理的,當你編譯你的工程時,它會去遍歷所有C++檔案,當發現某一個類的私有部分有宣告Q_OBJECT這個巨集時,就會自動生成一個moc_*.cpp的檔案,這個檔案會生成訊號的實現函式,Q_OBJECT巨集裡面定義的那些函式也會在這個檔案裡面實現,並生成與類相關的元物件。這就是為什麼我們定義一個訊號的時候,不需要實現它,因為它的實現已經寫在moc_*.cpp
2.Q_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)
這個巨集是定義一個屬性,屬性也是元物件系統的內容之一,實際上我們在做介面設計的時候經常會用到屬性,比如修改Label的顯示內容,需要用到Text屬性,修改窗體長寬等等,在你做介面設計的時候,屬性編輯框裡面所顯示的就是當前物件的所有屬性,而這些屬性的定義就是用上面的巨集來定義的。實際上屬性和變數是有點相似的,都是讀值和寫值的功能,那為什麼不直接對變數操作就好了?雖然看起來相似,但是還是有不同點,第一屬性可以定義為可讀寫的,也可以定義為只讀的,比如有些資料我們只在類的內部做修改不允許在外部做修改,但是有時候又需要在外部檢視這個值,就可以設定為只讀屬性,而變數是做不到這點的,你把變數放在public部分,那麼這個變數就可以在任何地方被修改,這就破壞了類的封裝性。第二屬性可以定義訊號,當屬性變化的時候觸發訊號,這樣我們可以在訊號觸發時做一些工作,比如當你設定LineEdit為readonly時,你會發現輸入框的背景顏色被改變了,這就可以通過屬性變化的訊號來處理。
3.Q_DECLARE_PRIVATE(QObject)
這個巨集的定義如下:
#define Q_DECLARE_PRIVATE(Class) \ inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \ inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \ friend class Class##Private;
這個巨集首先建立了兩個行內函數,返回值都是QObjectPrivate *,並且宣告QObjectPrivate 為友元類,QObjectPrivate這個類是在qobject_p.h中定義,它繼承至QObjectData,你可以看到d_func()是將d_prt強制轉換為QObjectPrivate *型別,而d_prt這個指標在QObject裡面定義的是QObjectData的指標型別,所以這裡可以進行強轉,QObjectPrivate這個類主要存放QOject類需要用到的一些子物件,變數等。為什麼要介紹這個巨集,如果你有看QT原始碼習慣的話,你會發現幾乎每一個類都用到了這個巨集,我們自己寫的類會經常把類內部用的變數宣告在private部分,但是QT原始碼並不是這樣做的,它的做法是給每個類建立一個以類名+Private的類,例如QObject對應的就是QObjectPrivate,這個類實際上就是用來存放QObject需要用到的所有私有變數和私有物件,而QObject更多的是函式實現,你去看其他的原始碼也是如此,子物件宣告在Q*Private中,而本類只實現函式。
4.建構函式
QObject::QObject(QObject *parent) : d_ptr(new QObjectPrivate) { Q_D(QObject); d_ptr->q_ptr = this; d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current(); d->threadData->ref(); if (parent) { QT_TRY { if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData)) parent = 0; setParent(parent); } QT_CATCH(...) { d->threadData->deref(); QT_RETHROW; } } #if QT_VERSION < 0x60000 qt_addObject(this); #endif if (Q_UNLIKELY(qtHookData[QHooks::AddQObject])) reinterpret_cast<QHooks::AddQObjectCallback>(qtHookData[QHooks::AddQObject])(this); }
(1)首先第一步就建立d_ptr指標。
(2)Q_D(QObject);這個巨集你可以在QT的很多原始碼裡面看到。它展開以後是下面的樣子:#define Q_D(Class) Class##Private * const d = d_func();
d_fun()函式前面講到了,其實就是返回d_ptr了。所以這個巨集的意思是定義一個指標d指向d_ptr;
(3)d_ptr->q_ptr = this;
q_ptr是QOject型別,這裡把this指標賦給了它,所以使得QObjectPrivate可以回撥QOject的函式。
(4)初始化threadData
5.moveToThread
(1)如果要移動的執行緒和Object本身就是同一執行緒,那麼直接返回
Q_D(QObject); if (d->threadData->thread == targetThread) { // object is already in this thread return; }
(2)如果parent不為空,不允許移動到其他執行緒,子類必需與父類在同一執行緒。
if (d->parent != 0) { qWarning("QObject::moveToThread: Cannot move objects with a parent"); return; }
(3)如果物件是窗體類,不允許移動到執行緒,窗體類必需在主執行緒執行,在子執行緒去直接呼叫窗體控制元件都是不安全的,可能導致程式崩潰,合理的做法是通過訊號槽機制。
if (d->isWidget) { qWarning("QObject::moveToThread: Widgets cannot be moved to a new thread"); return; }
(4)只有在物件所線上程才能將物件移動到另一個執行緒,不能在其他執行緒將物件移動到某個執行緒,這種操作是不被允許的。
QThreadData *currentData = QThreadData::current(); QThreadData *targetData = targetThread ? QThreadData::get2(targetThread) : Q_NULLPTR; if (d->threadData->thread == 0 && currentData == targetData) { // one exception to the rule: we allow moving objects with no thread affinity to the current thread currentData = d->threadData; } else if (d->threadData != currentData) { qWarning("QObject::moveToThread: Current thread (%p) is not the object's thread (%p).\n" "Cannot move to target thread (%p)\n", currentData->thread.load(), d->threadData->thread.load(), targetData ? targetData->thread.load() : Q_NULLPTR); #ifdef Q_OS_MAC qWarning("You might be loading two sets of Qt binaries into the same process. " "Check that all plugins are compiled against the right Qt binaries. Export " "DYLD_PRINT_LIBRARIES=1 and check that only one set of binaries are being loaded."); #endif return; }
(5)執行緒轉移
//為轉移執行緒準備,遍歷所有子物件,並給每一個子物件傳送一個QEvent::ThreadChange的事件。 d->moveToThread_helper(); if (!targetData) targetData = new QThreadData(0); //為轉移事件上鎖 QOrderedMutexLocker locker(¤tData->postEventList.mutex, &targetData->postEventList.mutex); currentData->ref(); //遍歷所有子物件及自身,將currentData的postEventList裡面的物件轉移到targetData,將所有子物件及自身的threadData設定為targetData d_func()->setThreadData_helper(currentData, targetData); locker.unlock(); currentData->deref();
6.connect函式
connet的重構函式很多,這裡選擇其中一個來分析。
QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type)
(1)首選sender,receiver不能為空,signal必須是Signal型別,也就是宣告在signals:下面,method不能為建構函式,不滿足這幾個條件則返回。
if (sender == 0 || receiver == 0 || signal.methodType() != QMetaMethod::Signal || method.methodType() == QMetaMethod::Constructor) { qWarning("QObject::connect: Cannot connect %s::%s to %s::%s", sender ? sender->metaObject()->className() : "(null)", signal.methodSignature().constData(), receiver ? receiver->metaObject()->className() : "(null)", method.methodSignature().constData() ); return QMetaObject::Connection(0); }
(2)檢查signal和method是否真實存在,在編譯期即使傳入的訊號不存在也不會報錯,在執行期會檢查是否存在,所以在寫connect函式的時候要仔細檢查,儘量使用&ClassName::functionName的方式讓系統自動補全,當然也可以通過connect的返回值來判斷呼叫是否成功,如呼叫不成功則丟擲異常。
int signal_index; int method_index; { int dummy; QMetaObjectPrivate::memberIndexes(sender, signal, &signal_index, &dummy); QMetaObjectPrivate::memberIndexes(receiver, method, &dummy, &method_index); } const QMetaObject *smeta = sender->metaObject(); const QMetaObject *rmeta = receiver->metaObject(); if (signal_index == -1) { qWarning("QObject::connect: Can't find signal %s on instance of class %s", signal.methodSignature().constData(), smeta->className()); return QMetaObject::Connection(0); } if (method_index == -1) { qWarning("QObject::connect: Can't find method %s on instance of class %s", method.methodSignature().constData(), rmeta->className()); return QMetaObject::Connection(0); }
(3)檢查signal和method的引數個數和型別是否是一致的,不一致則返回。
if (!QMetaObject::checkConnectArgs(signal.methodSignature().constData(), method.methodSignature().constData())) { qWarning("QObject::connect: Incompatible sender/receiver arguments" "\n %s::%s --> %s::%s", smeta->className(), signal.methodSignature().constData(), rmeta->className(), method.methodSignature().constData()); return QMetaObject::Connection(0); }
(4)如果你設定的連線方式為QueuedConnection,那麼所有的引數都必須是元資料型別,自定義的型別,如自定義的結構體或列舉必須註冊為元資料型別,否則無法作為訊號和槽的引數,因為最終要將這些引數加入到訊息佇列裡面。
int *types = 0; if ((type == Qt::QueuedConnection) && !(types = queuedConnectionTypes(signal.parameterTypes()))) return QMetaObject::Connection(0);
(5)所有的檢查完畢,呼叫QMetaObject的Connection函式,而QMetaObject的Connection會建立一個Connection的物件,這個物件會儲存訊號和槽的函式物件,然後會把這個Connection物件儲存到sender的一個數組中,當你觸發訊號的時候,因為Connection物件儲存在了sender中,所以可以找到原來繫結的槽函式,然後回撥槽函式。
QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect( sender, signal_index, signal.enclosingMetaObject(), receiver, method_index, 0, type, types)); return handle;