【写在前面】
在 Qml 中,很多时候我们需要动态创建一些 Qml 对象,通常是:createComponent + createObject 或 createQmlObject。
然而,最近工作中却出现了一个相当难以察觉的问题:动态创建的窗口在某些时刻会被莫名其妙的删除,我花了很多时间才定位到关键位置。
其根本原因在于:未给动态创建的对象分配 parent ( 即:没有任何对象持有其引用 ),结果就是,当 Qml 引擎运行垃圾回收时,这些对象会被错误清除掉。
【正文开始】
这里先直接上代码来看清我的意图,main.qml:
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello World")
color: "#a00000"
Row {
Button {
text: "创建"
onClicked: {
let component = Qt.createComponent("TestWindow.qml");
if (component.status === Component.Ready) {
component.createObject();
}
}
}
Button {
text: "回收"
onClicked: {
qmlApi.collectGarbage();
}
}
}
}
可以看到,创建按钮动态创建了一个窗口,而回收按钮则直接运行 Qml 的垃圾收集器。
这部分需要借助 C++ 来运行:
class QmlApi : public QObject
{
Q_OBJECT
public:
QmlApi(QQmlEngine *engine, QObject *parent = nullptr)
: QObject(parent)
, m_engine(engine)
{
}
Q_INVOKABLE void collectGarbage() {
if (m_engine)
m_engine->collectGarbage();
}
QQmlEngine *m_engine = nullptr;
};
TestWindow.qml 则只是一个简单的窗口,有一个活动的矩形:
import QtQuick 2.15
import QtQuick.Window 2.15
Window {
width: 640
height: 480
visible: true
title: qsTr("Test Window")
Rectangle {
width: 100
height: 100
anchors.centerIn: parent
color: "green"
RotationAnimation on rotation {
loops: Animation.Infinite
from: 0
to: 360
}
}
}
运行代码可以看到,一旦我们运行垃圾收集器,刚刚创建出的窗口全部都会被清除:
很明显,这根本不是我们的本意,究其原因,是创建的窗口不再可访问:
垃圾收集器将尝试通过定位和处理脚本环境中不再可访问的对象来回收内存。
通常情况下,您不需要调用此函数;当 QJSEngine 决定这样做是明智的时(即,当创建了一定数量的新对象时),垃圾收集器将自动被调用。但是,您可以调用此函数来明确请求应尽快执行垃圾收集。
因此,正确的解决方法是:持有创建后的对象。
例如可以使用变量存储,又或者给这些窗口一个 parent ( 隐式持有 )。
【结语】
实际上,由于 JS( Qml ) 的垃圾回收对开发者是不可见的,并且其运行时机也不确定,所以我们在动态创建对象时需要更加谨慎,来避免潜在可能的问题。