QML 垃圾收集删除仍在使用的对象

IT技术 javascript qt garbage-collection qml qtquick2
2021-02-28 13:35:15

我在多个场合遇到过这个问题,对象是动态创建的,无论它们是用 QML 还是 C++ 创建的。对象在仍在使用时被删除,导致无明显原因的硬崩溃。这些对象一直被引用并成为其他对象的父对象,一直到根对象,所以我发现 QML 在它们的引用计数仍然高于零时删除这些对象很奇怪。

到目前为止,我找到的唯一解决方案是在 C++ 中创建对象并将所有权显式设置为 CPP,从而无法从 QML 中删除对象。

起初我认为这可能是育儿方面的问题,因为我使用的是QObject派生类,并且动态实例化的 QML 方法Item为父级传递了 an ,而QtObject它甚至没有带有父级属性——它没有从QObject.

但是后来我尝试了一个Qobject暴露和使用Item父级派生,最后甚至尝试使用只是为了确保对象是正确的父级,但这种行为仍然存在。

这是一个产生这种行为的例子,不幸的是我无法将它扁平化为单一来源,因为Components的深层嵌套破坏了它:

// ObjMain.qml
Item {
    property ListModel list : ListModel { }
    Component.onCompleted: console.log("created " + this + " with parent " + parent)
    Component.onDestruction: console.log("deleted " + this)
}

// Uimain.qml
Item {
    id: main
    width: childrenRect.width
    height: childrenRect.height
    property Item object
    property bool expanded : true
    Loader {
        id: li
        x: 50
        y: 50
        active: expanded && object && object.list.count
        width: childrenRect.width
        height: childrenRect.height
        sourceComponent: listView
    }
    Component {
        id: listView
        ListView {
            width: contentItem.childrenRect.width
            height: contentItem.childrenRect.height
            model: object.list
            delegate: Item {
                id: p
                width: childrenRect.width
                height: childrenRect.height
                Component.onCompleted: Qt.createComponent("Uimain.qml").createObject(p, {"object" : o})
            }
        }
    }
    Rectangle {
        width: 50
        height: 50
        color: "red"

        MouseArea {
            anchors.fill: parent
            acceptedButtons: Qt.RightButton | Qt.LeftButton
            onClicked: {
                if (mouse.button == Qt.RightButton) {
                    expanded = !expanded
                } else {
                    object.list.append({ "o" : Qt.createComponent("ObjMain.qml").createObject(object) })
                }
            }
        }
    }
}

// main.qml

Window {
    visible: true
    width: 1280
    height: 720

    ObjMain {
        id: obj
    }

    Uimain {
        object: obj
    }
}

该示例是一个简单的对象树构建器,左侧按钮向节点添加叶子,右侧按钮折叠节点。重现该错误所需要的只是创建一个深度为 3 的节点,然后折叠并展开根节点,控制台输出显示:

qml: created ObjMain_QMLTYPE_0(0x1e15bb8) with parent QQuickRootItem(0x1e15ca8)
qml: created ObjMain_QMLTYPE_0(0x1e5afc8) with parent ObjMain_QMLTYPE_0(0x1e15bb8)
qml: created ObjMain_QMLTYPE_0(0x1e30f58) with parent ObjMain_QMLTYPE_0(0x1e5afc8)
qml: deleted ObjMain_QMLTYPE_0(0x1e30f58)

object最深的节点被删除没有理由,即使它是父父节点Item,并在列表模型中的JS对象引用。尝试向最深节点添加新节点会使程序崩溃。

行为是一致的,无论树的结构如何,只有第二层节点存活,当树折叠时所有更深的节点都丢失。

错误不在于用作存储的列表模型,我已经用 JS 数组和 a 进行了测试,QList但对象仍然丢失。此示例仅使用列表模型来保存 C++ 模型的额外实现。到目前为止,我发现的唯一补救措施是完全拒绝 QML 对对象的所有权。虽然这个例子产生了相当一致的行为,但在生产代码中,自发删除通常是完全任意的。

关于垃圾收集器 - 我之前测试过它,并注意到它非常自由 - 创建和删除 100 MB 内存的对象并没有触发垃圾收集以释放该内存,但在这种情况下只有少数value几百字节的对象被仓促删除。

根据文档,不应删除具有父对象或被 JS 引用的对象,在我的情况下,两者都是有效的:

该对象归 JavaScript 所有。当对象作为方法调用的返回值返回给 QML 时,如果没有剩余的 JavaScript 引用并且没有 QObject::parent(),QML 将跟踪它并删除它

正如 Filip 的回答中提到的,如果对象是由不在被删除的对象中的函数创建的,则不会发生这种情况,因此它可能与与 QML 对象关联的含糊提及的 JS 状态有关,但我本质上是至于为什么会发生删除,仍然一无所知,所以这个问题实际上仍然没有答案。

任何想法是什么原因造成的?

更新:九个月后,这个关键错误的开发仍然为零同时,我发现了几个额外的场景,其中仍在使用的对象被删除,对象的创建位置无关紧要的场景以及简单地在主 qml 文件中创建对象的解决方法不适用。最奇怪的部分是对象在“未引用”时没有被销毁,而是在“重新引用”时被销毁。也就是说,当引用它们的视觉对象被销毁时,它们并没有被销毁,而是在它们被重新创建时被销毁。

好消息是,即使对于在 QML 中创建的对象,仍然可以将所有权设置为 C++,因此不会失去在 QML 中创建对象的灵活性。调用函数来保护和删除每个对象会带来一些不便,但至少可以避免 QtQuick 有问题的生命周期管理。不得不喜欢 QML 的“便利性”——被迫回到手动对象生命周期管理。

3个回答

我在多个场合遇到过这个问题,对象是动态创建的,不管它们是用 QML 还是 C++ 创建的

对象只有在设置了 JavaScriptOwnership 时才会考虑进行垃圾回收,如果它们是

  1. 由 JavaScript/QML 直接创建
  2. 所有权明确设置为 JavaScriptOwnership
  3. 该对象是从 Q_INVOKABLE 方法返回的,并且之前没有调用过 setObjectOwnership()。

在所有其他情况下,对象被假定为 C++ 所有并且不考虑进行垃圾收集。

起初我认为这可能是育儿问题,因为我使用的是 QObject 派生类,并且动态实例化的 QML 方法为父级传递了一个 Item,而 QtObject 甚至没有带有父级属性 - 它没有公开来自 QObject。

Qt 对象树与 Qml 对象树完全不同。QML 只关心它自己的对象树。

    delegate: Item {
        id: p
        width: childrenRect.width
        height: childrenRect.height
        Component.onCompleted: Qt.createComponent("Uimain.qml").createObject(p, {"object" : o})
    }

在委托的 onCompleted 处理程序中动态创建的对象的组合必然会导致错误。

当您折叠树时,代理会被销毁,并且它们的所有子代都会被销毁,其中包括您动态创建的对象。是否仍然存在对孩子的实时引用并不重要。

本质上,您没有为树提供稳定的后备存储 - 它由一堆嵌套的委托组成,可以随时消失。

现在,在某些情况下,QML 拥有的对象会意外删除:任何 C++ 引用都不算作垃圾收集器的引用;这包括 Q_PROPERTY。在这种情况下,您可以:

  1. 明确设置 CppOwnership
  2. 使用 QPointer<> 来保存引用以处理消失的对象。
  3. 在 QML 中保存对对象的显式引用。

QML 在管理内存方面不是 C++。QML 旨在处理内存的分配和释放。我认为你发现的问题只是这个的结果。

如果动态对象创建太深,一切似乎都被删除了。因此,您创建的对象是否是数据的一部分并不重要 - 它们也会被销毁。

不幸的是,我的知识到此结束。

该问题的解决方法之一(证明我之前的陈述)是将数据结构的创建从动态 UI qml 文件中移出:

  1. 例如在main.qml 中放置对象创建函数

function createNewObject(parentObject) {
    parentObject.list.append({ "o" : Qt.createComponent("ObjMain.qml").createObject(parentObject) })
}
  1. 在您的代码中改用此函数:

// fragment of the Uimain.qml file
    MouseArea {
        anchors.fill: parent
        acceptedButtons: Qt.RightButton | Qt.LeftButton
        onClicked: {
            if (mouse.button == Qt.RightButton) {
                expanded = !expanded
            } else {
                createNewObject(object)
            }
        }
    }
你误解了代码,当加载器设置为非活动时释放的是 UI 部分,而不是数据部分,UI 部分只是引用了数据部分,但即使在它被释放后,数据仍然以对象树为父对象,并且模型引用。
2021-04-23 13:35:15
@ddriver 对我来说,只有第一级保持不变,从第二级开始一切都被破坏了。
2021-04-27 13:35:15
而且,如果你说的有关系的话,二级数据节点也会丢失,而受影响的只是三级及更深的节点。
2021-05-01 13:35:15
据说不影响sourceComponent我因此排除了它。你能更好地解释为什么它看起来是正确的吗?
2021-05-04 13:35:15
这是有效的,因此除了使用 C++ 和显式所有权之外,它是另一种处理该问题的方法,不幸的是,它没有回答为什么删除对象的问题,可能与与 QML 对象关联的 JS 状态有关,文档是在这个问题上比较模糊。
2021-05-11 13:35:15

在 .js 文件中创建一个数组,然后var myArray = []; 在该.js.文件的顶层创建该数组的一个实例

现在您可以引用您附加到的任何对象,myArray 包括动态创建的对象。

只要 Javascript 变量保持定义,垃圾收集器就不会删除它们,因此,如果您将一个变量定义为全局对象,然后将该 Javascript 文件包含在您的 qml 文档中,只要主 QML 在范围内,它就会一直存在。


在名为:backend.js 的文件中

var tiles = [];

function create_square(new_square) {
    var component = Qt.createComponent("qrc:///src_qml/src_game/Square.qml");
    var sq = component.createObject(background, { "backend" : new_square });
    sq.x = new_square.tile.x
    sq.y = new_square.tile.y
    sq.width = new_square.tile.width;
    sq.height = new_square.tile.height;
    tiles[game.board.getIndex(new_square.tile.row, new_square.tile.col)] = sq;
    sq.visible = true;
}

编辑

让我更清楚地解释一下这如何适用于您的特定树示例。

通过使用该行,property Item object 您无意中将其强制为 Item 的一个属性,在 QML 中对其进行了不同的处理。具体来说,就垃圾收集而言,属性属于一组独特的规则,因为 QML 引擎可以简单地开始删除任何对象的属性以减少运行所需的内存。

相反,在 QML 文档的顶部,包含以下行:

import "./object_file.js" as object_file

然后在文件object_file.js 中,包含以下行:

 var object_hash = []; 

现在,您可以object_hash 随时保存动态创建的组件,并通过引用

object_file.object_hash

目的。

无需疯狂更改所有权等