Qt源码阅读(三) 对象树管理
对象树管理
个人经验总结,如有错误或遗漏,欢迎各位大佬指正 🥳
对象树的作用
众所周知,在 Qt中,我们可以通过setParent
函数为 QObject 对象设置一个父对象。
当为一个对象设置父对象时,有几个主要的作用:
-
内存管理:当父对象被析构时,它会自动析构其所有的子对象。这意味着无需手动管理子对象的销毁,减轻了开发人员的负担,并确保在不再需要这些子对象时,它们会被正确地释放。
-
继承父对象样式表:如果父对象设置了样式表(Style Sheet),它的子对象也会继承这些样式。这使得在应用程序中实现一致的外观和样式变得更加容易。通过简单地设置父对象的样式表,可以将相同的样式应用于其所有子对象,避免了对每个子对象逐个设置样式的繁琐工作。
-
对象间关系管理:子对象可以通过
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
}
// ...
}
这个函数有两个重要之处:
- 把当前对象从父对象的子对象列表中删除,并发送一个
ChildRemove
事件。这里在下面讲到QObject对象的析构时会有用。
// 把当前对象从父对象的子对象列表中删除
parentD->children.removeAt(index);
if (sendChildEvents && parentD->receiveChildEvents) {
QChildEvent e(QEvent::ChildRemoved, q);
QCoreApplication::sendEvent(parent, &e);
}
- 将当前对象加入父对象的子对象列表中,并发送一个
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
这个函数我们前面分析了,关键的步骤在于:
- 将自己从父对象列表中删除
parentD->children.removeAt(index);
if (sendChildEvents && parentD->receiveChildEvents) {
QChildEvent e(QEvent::ChildRemoved, q);
QCoreApplication::sendEvent(parent, &e);
}
- 同时,由于函数的入参我们设置的是
nullptr
,所以在添加至父对象列表中的先决条件判断就不成立,也就不会添加。
if (parent) {
// ...
#4
// 父对象添加子对象,并发送事件
// ...
#4
}
看到这里,我们也就基本了解了QObject析构时会做的操作:
- 如果不是widget,发射
**destroyed**
信号。 - 断开对象所建立的信号槽连接。
- 删除所有子对象。
- 将自己从父对象的对象树中删除
总结
经过上面的分析,你应该意识到源码本身并没有那么神秘😏。最重要的是,在阅读源码时,我们不应该盲目地进行阅读,而是应该有一个明确的目的或问题(结合Qt的帮助文档)。这样你就能快速地找到你想要了解的内容,而不会迷失在源码的海洋中。所以,源码只是一种工具,我们需要有目标和问题来引导我们的阅读。
创作不易,如果对您有帮助,点赞、关注、收藏支持一下!不甚感激!😊