QObject* 列表的内存管理导致 QML 中出现 "Cannot read property X of null" 错误
Memory management for list of QObject* results in "Cannot read property X of null" errors in QML
我需要创建 QObject*
的动态列表(表示自定义模型)并将它们公开给 QML。问题是 QML 试图重新使用以前删除的 QObject*
,这最终会在运行时出现错误:
qrc:/MyWidget.qml:6: TypeError: Cannot read property 'value' of null
这是我的模型:
#include <QObject>
class SubModel : public QObject
{
Q_OBJECT
public:
SubModel(int value) : m_value(value) {}
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
int value() { return m_value; }
void setValue(int value)
{
m_value = value;
emit valueChanged();
}
signals:
void valueChanged();
private:
int m_value;
};
这是包含 SubModel
列表的 QAbstractListModel
(注意 createSubModels()
方法):
#include <QAbstractListModel>
#include <QObject>
#include <QVariant>
#include <memory>
#include <vector>
class ModelList : public QAbstractListModel
{
Q_OBJECT
public:
enum ModelRole
{
SubModelRole = Qt::UserRole
};
Q_ENUM(ModelRole)
void setSubModels(std::vector<std::unique_ptr<SubModel>> subModels)
{
beginResetModel();
m_subModels = std::move(subModels);
endResetModel();
}
int rowCount(const QModelIndex& parent = QModelIndex()) const override
{
return m_subModels.size();
}
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
{
if (!index.isValid()) {
return {};
}
switch (role) {
case ModelRole::SubModelRole:
return QVariant::fromValue<SubModel*>(m_subModels[index.row()].get());
}
return {};
}
bool setData(const QModelIndex& index, const QVariant& value, int role) override
{
Q_UNUSED(index);
Q_UNUSED(value);
Q_UNUSED(role);
return false;
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roles;
roles[ModelRole::SubModelRole] = "submodel";
return roles;
}
Q_INVOKABLE void createSubModels()
{
std::vector<std::unique_ptr<SubModel>> subModels;
for (int i = 0; i < rand() % 5 + 1; i++) {
subModels.push_back(std::make_unique<SubModel>(rand() % 100));
}
setSubModels(std::move(subModels));
}
private:
std::vector<std::unique_ptr<SubModel>> m_subModels;
};
这是我使用 SubModel
实例的 QML 小部件(注意输入的 property
):
import QtQuick 2.12
import MyLib.SubModel 1.0
Text {
property SubModel subModel;
text: subModel.value
}
这是 main.qml
文件:
import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 2.15
Window {
visible: true
Column {
Button {
text: "Create list"
onClicked: modelList.createSubModels();
}
Column {
Repeater {
model: modelList
//// This does generate errors
MyWidget {
subModel: model.modelData
}
//// This does generate errors
// Text {
// property var data: modelData
// text: data.value
// }
//// This does NOT generate errors!
// Text {
// text: modelData.value;
// }
}
}
}
}
最后,这里是 main.cpp
文件:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <iostream>
#include "models.h"
int main(int argc, char* argv[])
{
qmlRegisterUncreatableType<SubModel>(
"MyLib.SubModel", 1, 0, "SubModel", "This type can't be created in QML");
auto modelList = std::make_unique<ModelList>();
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
QQmlContext* rootContext = engine.rootContext();
rootContext->setContextProperty("modelList", modelList.get());
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
第一次点击 "Create list"
按钮时效果很好。但第二次,初始 SubModule
指针被删除,QML 尝试访问 nullptr
的 value
属性,导致警告。然后替换 m_subModels
并有效更新 GUI。我不明白为什么 QML 在我发出 beginResetModel()
信号时尝试访问模型。
我知道我可以更改 ModelList
并使 data()
returns 成为实际的 int
值而不是 SubModel
指针,但这是不可接受的因为我实际上在实际代码中有几个子模型(关注点分离)。
我想到的解决方案:
- 正在 QML 中检查
if submodel !== null
,但这看起来不太好
- 将
std::unique_ptr
替换为 new
但这会造成内存泄漏
- 在 Qt 中使用原始指针
parent
但在退出之前内存永远不会被释放
- 在析构函数中使用
QSharedPointer
和 deleteLater()
但这并不总是有效
- 在析构函数中将
QSharedPointer
与 setObjectOwnership(JavaScriptOwnership)
结合使用,但这似乎很老套(未测试)
- 使用
QList<QObject*>
而不是 QAbstractListModel
但这会产生相同的错误
- 使用
QQmlListProperty
而不是 QAbstractListModel
但这会产生相同的错误
- 重新使用现有的
SubModel*
而不是重新创建列表,但这看起来很复杂
我在这个问题上花了几天时间,找不到合适的解决方案。你有什么想法请保持我的“子模型”不变,并在重新创建列表时避免 QML 警告?
我上传了所有项目文件 here 以轻松重现问题(使用 cmake
构建)。
编辑:问题似乎与我对 property
的使用有关,因为如果我不将 modelData
存储为 property
则不会出现错误。
您的代码似乎运行良好。这就是 Repeater 的工作原理。
当您的模型被重置时,Repeater 开始以相反的顺序一个一个地删除它的 QQuickItems。
// qtdeclarative/src/quick/items/qquickrepeater.cpp
if (d->model) {
// We remove in reverse order deliberately; so that signals are emitted
// with sensible indices.
for (int i = d->deletables.count() - 1; i >= 0; --i) {
if (QQuickItem *item = d->deletables.at(i)) {
if (complete)
emit itemRemoved(i, item);
d->model->release(item);
}
}
for (QQuickItem *item : qAsConst(d->deletables)) {
if (item)
item->setParentItem(nullptr);
}
}
根据 Repeater 的策略,项目可能不会被完全删除,而是汇集起来以备将来使用。
在那一刻 Item 本身仍然存在,但您的模型数据不存在。
Text {
property var data: modelData // modelData is undefined at the moment
text: data.value // "data" is undefined. You see the warning message
}
因此,修复警告消息的最佳方法是检查是否定义了 modelData。
Text {
property var data: modelData
text: data ? data.value : ""
}
由于@samdavydov 解释的原因,Repeater 中的项目在模型重置期间保留,但通过取消引用 m_subModels
,子模型被删除(因为 unique_ptr)并且视图无效,因为正如你提到的 destroyed
信号。
通过在重置期间交换两个向量,旧的 SubModels 将在内存中保留一点,直到函数退出,此时重置已经完成并正在使用新的 SubModels:
void setSubModels(std::vector<std::unique_ptr<SubModel>> subModels)
{
beginResetModel();
m_subModels.swap(subModels);
endResetModel();
} //RAII deletes old SubModel at this point
感谢你自己在我提到将它们保存在内存中之后找到了交换 ;-)
我需要创建 QObject*
的动态列表(表示自定义模型)并将它们公开给 QML。问题是 QML 试图重新使用以前删除的 QObject*
,这最终会在运行时出现错误:
qrc:/MyWidget.qml:6: TypeError: Cannot read property 'value' of null
这是我的模型:
#include <QObject>
class SubModel : public QObject
{
Q_OBJECT
public:
SubModel(int value) : m_value(value) {}
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
int value() { return m_value; }
void setValue(int value)
{
m_value = value;
emit valueChanged();
}
signals:
void valueChanged();
private:
int m_value;
};
这是包含 SubModel
列表的 QAbstractListModel
(注意 createSubModels()
方法):
#include <QAbstractListModel>
#include <QObject>
#include <QVariant>
#include <memory>
#include <vector>
class ModelList : public QAbstractListModel
{
Q_OBJECT
public:
enum ModelRole
{
SubModelRole = Qt::UserRole
};
Q_ENUM(ModelRole)
void setSubModels(std::vector<std::unique_ptr<SubModel>> subModels)
{
beginResetModel();
m_subModels = std::move(subModels);
endResetModel();
}
int rowCount(const QModelIndex& parent = QModelIndex()) const override
{
return m_subModels.size();
}
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
{
if (!index.isValid()) {
return {};
}
switch (role) {
case ModelRole::SubModelRole:
return QVariant::fromValue<SubModel*>(m_subModels[index.row()].get());
}
return {};
}
bool setData(const QModelIndex& index, const QVariant& value, int role) override
{
Q_UNUSED(index);
Q_UNUSED(value);
Q_UNUSED(role);
return false;
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roles;
roles[ModelRole::SubModelRole] = "submodel";
return roles;
}
Q_INVOKABLE void createSubModels()
{
std::vector<std::unique_ptr<SubModel>> subModels;
for (int i = 0; i < rand() % 5 + 1; i++) {
subModels.push_back(std::make_unique<SubModel>(rand() % 100));
}
setSubModels(std::move(subModels));
}
private:
std::vector<std::unique_ptr<SubModel>> m_subModels;
};
这是我使用 SubModel
实例的 QML 小部件(注意输入的 property
):
import QtQuick 2.12
import MyLib.SubModel 1.0
Text {
property SubModel subModel;
text: subModel.value
}
这是 main.qml
文件:
import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 2.15
Window {
visible: true
Column {
Button {
text: "Create list"
onClicked: modelList.createSubModels();
}
Column {
Repeater {
model: modelList
//// This does generate errors
MyWidget {
subModel: model.modelData
}
//// This does generate errors
// Text {
// property var data: modelData
// text: data.value
// }
//// This does NOT generate errors!
// Text {
// text: modelData.value;
// }
}
}
}
}
最后,这里是 main.cpp
文件:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <iostream>
#include "models.h"
int main(int argc, char* argv[])
{
qmlRegisterUncreatableType<SubModel>(
"MyLib.SubModel", 1, 0, "SubModel", "This type can't be created in QML");
auto modelList = std::make_unique<ModelList>();
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
QQmlContext* rootContext = engine.rootContext();
rootContext->setContextProperty("modelList", modelList.get());
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
第一次点击 "Create list"
按钮时效果很好。但第二次,初始 SubModule
指针被删除,QML 尝试访问 nullptr
的 value
属性,导致警告。然后替换 m_subModels
并有效更新 GUI。我不明白为什么 QML 在我发出 beginResetModel()
信号时尝试访问模型。
我知道我可以更改 ModelList
并使 data()
returns 成为实际的 int
值而不是 SubModel
指针,但这是不可接受的因为我实际上在实际代码中有几个子模型(关注点分离)。
我想到的解决方案:
- 正在 QML 中检查
if submodel !== null
,但这看起来不太好 - 将
std::unique_ptr
替换为new
但这会造成内存泄漏 - 在 Qt 中使用原始指针
parent
但在退出之前内存永远不会被释放 - 在析构函数中使用
QSharedPointer
和deleteLater()
但这并不总是有效 - 在析构函数中将
QSharedPointer
与setObjectOwnership(JavaScriptOwnership)
结合使用,但这似乎很老套(未测试) - 使用
QList<QObject*>
而不是QAbstractListModel
但这会产生相同的错误 - 使用
QQmlListProperty
而不是QAbstractListModel
但这会产生相同的错误 - 重新使用现有的
SubModel*
而不是重新创建列表,但这看起来很复杂
我在这个问题上花了几天时间,找不到合适的解决方案。你有什么想法请保持我的“子模型”不变,并在重新创建列表时避免 QML 警告?
我上传了所有项目文件 here 以轻松重现问题(使用 cmake
构建)。
编辑:问题似乎与我对 property
的使用有关,因为如果我不将 modelData
存储为 property
则不会出现错误。
您的代码似乎运行良好。这就是 Repeater 的工作原理。
当您的模型被重置时,Repeater 开始以相反的顺序一个一个地删除它的 QQuickItems。
// qtdeclarative/src/quick/items/qquickrepeater.cpp
if (d->model) {
// We remove in reverse order deliberately; so that signals are emitted
// with sensible indices.
for (int i = d->deletables.count() - 1; i >= 0; --i) {
if (QQuickItem *item = d->deletables.at(i)) {
if (complete)
emit itemRemoved(i, item);
d->model->release(item);
}
}
for (QQuickItem *item : qAsConst(d->deletables)) {
if (item)
item->setParentItem(nullptr);
}
}
根据 Repeater 的策略,项目可能不会被完全删除,而是汇集起来以备将来使用。
在那一刻 Item 本身仍然存在,但您的模型数据不存在。
Text {
property var data: modelData // modelData is undefined at the moment
text: data.value // "data" is undefined. You see the warning message
}
因此,修复警告消息的最佳方法是检查是否定义了 modelData。
Text {
property var data: modelData
text: data ? data.value : ""
}
由于@samdavydov 解释的原因,Repeater 中的项目在模型重置期间保留,但通过取消引用 m_subModels
,子模型被删除(因为 unique_ptr)并且视图无效,因为正如你提到的 destroyed
信号。
通过在重置期间交换两个向量,旧的 SubModels 将在内存中保留一点,直到函数退出,此时重置已经完成并正在使用新的 SubModels:
void setSubModels(std::vector<std::unique_ptr<SubModel>> subModels)
{
beginResetModel();
m_subModels.swap(subModels);
endResetModel();
} //RAII deletes old SubModel at this point
感谢你自己在我提到将它们保存在内存中之后找到了交换 ;-)