Loading

Qt源码阅读(三) 对象树管理

对象树管理

个人经验总结,如有错误或遗漏,欢迎各位大佬指正 🥳


对象树的作用

众所周知,在 Qt中,我们可以通过setParent函数为 QObject 对象设置一个父对象。

当为一个对象设置父对象时,有几个主要的作用:

  1. 内存管理:当父对象被析构时,它会自动析构其所有的子对象。这意味着无需手动管理子对象的销毁,减轻了开发人员的负担,并确保在不再需要这些子对象时,它们会被正确地释放。

  2. 继承父对象样式表:如果父对象设置了样式表(Style Sheet),它的子对象也会继承这些样式。这使得在应用程序中实现一致的外观和样式变得更加容易。通过简单地设置父对象的样式表,可以将相同的样式应用于其所有子对象,避免了对每个子对象逐个设置样式的繁琐工作。

  3. 对象间关系管理:子对象可以通过parent函数来获取父对象的指针。父对象可以通过children来获取所有的子对象,也可以通过findChildren来找到指定的对象。

总而言之,设置父对象的作用主要是让父对象自动的去管理子对象,方便的进行对象间关系的处理,以及子对象能够继承父对象的样式表。这样可以简化对象的管理和样式的设置,提高代码的可读性和可维护性。

本篇文章,我们将结合源码,分析一下QObject是怎么进行内存管理的。

设置父对象(setParent)

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

我们可以看到,setParent就是调用了QObjectPrivate类的setParent_helper


所以,让我们进一步分析setParent_helper

setParent_helper是 Qt 内部的一个辅助函数,用于处理对象之间的父子关系。它被用于在设置父对象时执行一些额外的操作,以确保对象树的正确管理。

void QObjectPrivate::setParent_helper(QObject *o)
{
    // ...
    
    // 如果要设置的父对象就是当前的父对象,直接返回
#1
    if (o == parent)
        return;
#1
    
    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 {
#2
		// 把当前对象从父对象的子对象列表中删除
                parentD->children.removeAt(index);
                if (sendChildEvents && parentD->receiveChildEvents) {
                    QChildEvent e(QEvent::ChildRemoved, q);
                    QCoreApplication::sendEvent(parent, &e);
                }
#2
            }
        }
    }
    
#3
    // 设置父对象
    parent = o;
#3
    
    if (parent) {
        // ...
	
#4
        // 父对象添加子对象,并发送事件
        parent->d_func()->children.append(q);
        if(sendChildEvents && parent->d_func()->receiveChildEvents) {
            if (!isWidget) {
                QChildEvent e(QEvent::ChildAdded, q);
                QCoreApplication::sendEvent(parent, &e);
            }
        }
#4
    }
   
    // ...
}

这个函数有两个重要之处:

  1. 把当前对象从父对象的子对象列表中删除,并发送一个ChildRemove事件。这里在下面讲到QObject对象的析构时会有用。

// 把当前对象从父对象的子对象列表中删除
parentD->children.removeAt(index);
if (sendChildEvents && parentD->receiveChildEvents) {
    QChildEvent e(QEvent::ChildRemoved, q);
    QCoreApplication::sendEvent(parent, &e);
}
  1. 将当前对象加入父对象的子对象列表中,并发送一个ChildAdded事件。
// 父对象添加子对象,并发送事件
parent->d_func()->children.append(q);
if(sendChildEvents && parent->d_func()->receiveChildEvents) {
    if (!isWidget) {
        QChildEvent e(QEvent::ChildAdded, q);
        QCoreApplication::sendEvent(parent, &e);
    }
}

QObject对象的析构(~QObject)

当一个QObject对象析构的时候,自动析构所有的子对象

这个特性在我们使用窗口部件时非常有用。因为一个界面可能包含了很多子控件,比如按钮、标签等等。而当一个小窗口被关闭时,我们无需逐个进行析构操作,只需要由Qt的对象树自动进行析构即可。

🏷️注意:如果完全将对象的管理交给父子关系,可能会有问题。因为这意味着所有创建的对象,只会在父对象析构的时候才会去析构子对象。如果使用不当,会导致软件的内存随着使用时间的增加而增加。
对于QWidget,我们可以设置一个属性DeleteOnClose,即在窗口关闭时,自动去析构窗口。

言归正传,我们将目光移动至QObject的析构函数中,在这个函数里,就能看到所有的秘密。(源码之下无秘密嘛~)

QObject::~QObject()
{
    Q_D(QObject);
    d->wasDeleted = true;
    // ...

// #1
    if (!d->isWidget && d->isSignalConnected(0)) {
        emit destroyed(this);
    }
// #1
    
	// ...

// #2 
    QObjectPrivate::ConnectionData *cd = d->connections.loadRelaxed();
    if (cd) {
        // ...
        
        // disconnect all receivers
        int receiverCount = cd->signalVectorCount();
        for (int signal = -1; signal < receiverCount; ++signal) {
            // ...
            while (QObjectPrivate::Connection *c = connectionList.first.loadRelaxed()) {
            	// ...
            }
        }

        /* Disconnect all senders:
         */
        while (QObjectPrivate::Connection *node = cd->senders) {
            // ...
        }

        // invalidate all connections on the object and make sure
        // activate() will skip them
        cd->currentConnectionId.storeRelaxed(0);
    }

    // ...
// #2 

// #3
    if (!d->children.isEmpty())
        d->deleteChildren();
// #3

	// ...

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

首先,我们看到的第一个点:destroyed信号的发出

// #1
    if (!d->isWidget && d->isSignalConnected(0)) {
        emit destroyed(this);
    }
// #1

根据官方文档所说,这个信号在QObject对象析构之后发出。我们可以搭配deleteLater进行使用。具体参看本人的博客Qt源码阅读(五)-deleteLater


接着往下走,我们可以看到在析构函数中,会将建立的所有信号槽连接都取消连接。

// #2
if (cd) {
    // ...
    
    // disconnect all receivers
    int receiverCount = cd->signalVectorCount();
    for (int signal = -1; signal < receiverCount; ++signal) {
        // ...
    }

    /* Disconnect all senders:
     */
    while (QObjectPrivate::Connection *node = cd->senders) {
        // ...
    }
}
// #2

再往下走,就到我们此次的目的:子对象的管理

// #3
    if (!d->children.isEmpty())
        d->deleteChildren();
// #3

可以看到这里调用了deleteChildren函数。让我们分析deleteChildren所作的操作。

void QObjectPrivate::deleteChildren()
{
	// ...
    for (int i = 0; i < children.count(); ++i) {
        currentChildBeingDeleted = children.at(i);
        children[i] = 0;
        delete currentChildBeingDeleted;
    }
    children.clear();
	// ...
}

其实也没有啥神秘的,就是遍历子对象列表,将子对象一个一个的进行删除


目光回到QObject的析构函数中,看到最后一个点:将自己从父对象的对象树中移除。

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

setParent_helper这个函数我们前面分析了,关键的步骤在于:

  1. 将自己从父对象列表中删除
parentD->children.removeAt(index);
if (sendChildEvents && parentD->receiveChildEvents) {
    QChildEvent e(QEvent::ChildRemoved, q);
    QCoreApplication::sendEvent(parent, &e);
}
  1. 同时,由于函数的入参我们设置的是nullptr,所以在添加至父对象列表中的先决条件判断就不成立,也就不会添加。
if (parent) {
        // ...
	
#4
        // 父对象添加子对象,并发送事件
        // ...
#4
    }

看到这里,我们也就基本了解了QObject析构时会做的操作:

  1. 如果不是widget,发射**destroyed**信号。
  2. 断开对象所建立的信号槽连接。
  3. 删除所有子对象。
  4. 将自己从父对象的对象树中删除

总结

经过上面的分析,你应该意识到源码本身并没有那么神秘😏。最重要的是,在阅读源码时,我们不应该盲目地进行阅读,而是应该有一个明确的目的或问题(结合Qt的帮助文档)。这样你就能快速地找到你想要了解的内容,而不会迷失在源码的海洋中。所以,源码只是一种工具,我们需要有目标和问题来引导我们的阅读。

创作不易,如果对您有帮助,点赞、关注、收藏支持一下!不甚感激!😊

posted @ 2023-03-29 22:10  师从名剑山  阅读(609)  评论(0编辑  收藏  举报