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 尝试访问 nullptrvalue 属性,导致警告。然后替换 m_subModels 并有效更新 GUI。我不明白为什么 QML 在我发出 beginResetModel() 信号时尝试访问模型。

我知道我可以更改 ModelList 并使 data() returns 成为实际的 int 值而不是 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

感谢你自己在我提到将它们保存在内存中之后找到了交换 ;-)