Qt原始碼閱讀(三) 物件樹管理

師從名劍山發表於2023-03-29

物件樹管理

個人經驗總結,如有錯誤或遺漏,歡迎各位大佬指正 ?

@

設定父物件的作用

眾所周知,Qt中,有為物件設定父物件的方法——setParent

而設定父物件的作用主要有,在父物件析構的時候,會自動去析構其子物件。如果是一個視窗物件,如果其父物件設定了樣式表(Style Sheet),子物件也會繼承父物件的樣式

所以,這篇文章,我們們主要看一下setParent的原始碼以及QObject是怎麼進行物件管理的。

設定父物件(setParent)

我們可以看到,setParent這個函式就是呼叫了QObjectPrivate類的setParent_helper這個函式。

void QObject::setParent(QObject *parent)
{
    Q_D(QObject);
    Q_ASSERT(!d->isWidget);
    d->setParent_helper(parent);
}

所以,我們進一步分析setParent_helper這個函式

完整原始碼

void QObjectPrivate::setParent_helper(QObject *o)
{
    Q_Q(QObject);
	// 不能把自己設為父物件
    Q_ASSERT_X(q != o, Q_FUNC_INFO, "Cannot parent a QObject to itself");
#ifdef QT_DEBUG
	// 檢查物件樹的迴圈
    const auto checkForParentChildLoops = qScopeGuard([&](){
        int depth = 0;
        auto p = parent;
        while (p) {
            if (++depth == CheckForParentChildLoopsWarnDepth) {
                qWarning("QObject %p (class: '%s', object name: '%s') may have a loop in its parent-child chain; "
                         "this is undefined behavior",
                         q, q->metaObject()->className(), qPrintable(q->objectName()));
            }
            p = p->parent();
        }
    });
#endif

	// 如果要設定的父物件就是當前的父物件,直接返回
    if (o == parent)
        return;

    if (parent) {
        QObjectPrivate *parentD = parent->d_func();
        if (parentD->isDeletingChildren && wasDeleted
            && parentD->currentChildBeingDeleted == q) {
            // don't do anything since QObjectPrivate::deleteChildren() already
            // cleared our entry in parentD->children.
        } else {
            const int index = parentD->children.indexOf(q);
            if (index < 0) {
                // we're probably recursing into setParent() from a ChildRemoved event, don't do anything
            } else if (parentD->isDeletingChildren) {
                parentD->children[index] = 0;
            } else {
				// 如果物件已經存在父物件的列表中,將原先存在的物件刪除,併傳送事件
                parentD->children.removeAt(index);
                if (sendChildEvents && parentD->receiveChildEvents) {
                    QChildEvent e(QEvent::ChildRemoved, q);
                    QCoreApplication::sendEvent(parent, &e);
                }
            }
        }
    }
	// 設定父物件
    parent = o;
    if (parent) {
        // object hierarchies are constrained to a single thread
        if (threadData != parent->d_func()->threadData) {
            qWarning("QObject::setParent: Cannot set parent, new parent is in a different thread");
            parent = nullptr;
            return;
        }
		// 父物件新增子物件,併傳送事件
        parent->d_func()->children.append(q);
        if(sendChildEvents && parent->d_func()->receiveChildEvents) {
            if (!isWidget) {
                QChildEvent e(QEvent::ChildAdded, q);
                QCoreApplication::sendEvent(parent, &e);
            }
        }
    }
    if (!wasDeleted && !isDeletingChildren && declarativeData && QAbstractDeclarativeData::parentChanged)
        QAbstractDeclarativeData::parentChanged(declarativeData, q, o);
}

片段分析

  1. 一些先決條件的判斷

    • 判斷設定的父物件是否是自己

      	// 不能把自己設為父物件 
      	Q_ASSERT_X(q != o, Q_FUNC_INFO, "Cannot parent a QObject to itself"); 
      	/*...*/ 
      	// 如果要設定的父物件就是當前的父物件,直接返回 
      	if (o == parent)    return;
      
    • 判斷原來的父物件是否處於正在刪除子物件的過程中,並且當前物件已經被刪除了,如果是,則什麼都不做(有點迷惑)

      	if (parentD->isDeletingChildren && wasDeleted            
      		&& parentD->currentChildBeingDeleted == q) {
      	            // don't do anything since QObjectPrivate::deleteChildren() 
      	            //already  cleared our entry in parentD->children.
      	 }
      
    • 判斷是不是透過從ChildRemoved事件遞迴到setParent()

      if (index < 0) {                
      	// we're probably recursing into setParent() from a ChildRemoved event,
      	// don't do anything 
      } else if (parentD->isDeletingChildren) {    
      	parentD->children[index] = 0; 
      }
      
    • 判斷物件是不是已存在父物件的列表中,如果存在,就將物件刪除,併傳送事件

      else { 			
      // 如果物件已經存在父物件的列表中,將原先存在的物件刪除,併傳送事件                
      	parentD->children.removeAt(index);                
      	if (sendChildEvents && parentD->receiveChildEvents) {                    
      		QChildEvent e(QEvent::ChildRemoved, q);
      		QCoreApplication::sendEvent(parent, &e);                
      	}            
      }
      
  2. 設定父物件,這裡有一個限制,就是新設定的父物件,必須和當前物件在同一個執行緒,否則不能設定

    // 設定父物件
        parent = o;
        if (parent) {
            // object hierarchies are constrained to a single thread
            if (threadData != parent->d_func()->threadData) {
                qWarning("QObject::setParent: Cannot set parent, \
                 new parent is in a different thread");
                parent = nullptr;
                return;
            }
    		// 父物件新增子物件,併傳送事件
            parent->d_func()->children.append(q);
            if(sendChildEvents && parent->d_func()->receiveChildEvents) {
                if (!isWidget) {
                    QChildEvent e(QEvent::ChildAdded, q);
                    QCoreApplication::sendEvent(parent, &e);
                }
            }
        }
    

物件的刪除

  然後就是物件的管理,也就是在父物件析構的時候,自動析構掉所有的子物件。這一個在我們使用視窗部件的時候很有用,因為一個介面可能有很多個子控制元件,比如按鈕、label等,這時候,如果一個小視窗被關閉,我們也不需要一個一個的去析構,由Qt的物件樹去進行析構就好了。

QObject::~QObject()
{
   /*...*/

	// 刪除子物件
    if (!d->children.isEmpty())
        d->deleteChildren();

#if QT_VERSION < 0x60000
    qt_removeObject(this);
#endif
    if (Q_UNLIKELY(qtHookData[QHooks::RemoveQObject]))
        reinterpret_cast<QHooks::RemoveQObjectCallback>(qtHookData[QHooks::RemoveQObject])(this);

    Q_TRACE(QObject_dtor, this);

    if (d->parent)        // remove it from parent object
        d->setParent_helper(nullptr);
}

將所有的子物件進行刪除,遍歷容器,按照子物件所加入進來的順序進行析構

void QObjectPrivate::deleteChildren()
{
	// 清空子物件
    Q_ASSERT_X(!isDeletingChildren, "QObjectPrivate::deleteChildren()", "isDeletingChildren already set, did this function recurse?");
    isDeletingChildren = true;
    // delete children objects
    // don't use qDeleteAll as the destructor of the child might
    // delete siblings
    for (int i = 0; i < children.count(); ++i) {
        currentChildBeingDeleted = children.at(i);
        children[i] = 0;
        delete currentChildBeingDeleted;
    }
    children.clear();
    currentChildBeingDeleted = nullptr;
    isDeletingChildren = false;
}

夾帶私貨時間

在使用Qt的物件樹這個功能的時候,可能會遇到一種問題,會導致程式崩潰:就是手動的管理(也就是直接delete)一個有父物件的QObject,為什麼會出現這樣的情況呢,因為,你在delete子物件之後,並沒有把這個物件從父物件的物件樹裡移除。在父物件進行析構的時候,還是會去遍歷子物件容器,一個一個析構。這個時候,就會出現,一個物件指標被刪除了兩次,自然就會崩潰。

那麼,如果非要自己管理這個物件,有什麼辦法呢?我們從物件樹下手,有兩種辦法:

  1. 使用deleteLater

    就是呼叫QObject物件的deleteLater函式,來實現刪除。關於deleteLater的分析,可以看這個大佬的文章Qt 中 deleteLater() 函式的使用

    QObject *object = new QObject();
    QObject *m_child = new QObject(object);
    
    // 需要手動刪除的時候
    m_child->deleteLater();
    
  2. 先將父物件設定為空,再直接delete

    QObject *object = new QObject();
    QObject *m_child = new QObject(object);
    
    // 需要手動刪除的時候
    m_child->setParent(nullptr);
    delete m_child;
    m_child = nullptr;
    
  3. 先將父物件設定為空,再直接delete

    QObject *object = new QObject();
    QObject *m_child = new QObject(object);
    
    // 需要手動刪除的時候
    m_child->setParent(nullptr);
    delete m_child;
    m_child = nullptr;
    

個人建議使用第一種方法,也就是呼叫deleteLater

相關文章