Qt/Qml 中对象生命期的陷阱

在 Qml 中访问 Qt 的对象(包括属性、方法、信号)非常的方便,但是即使是经验老道的 Qt 程序员,也容易被这其中的机制坑到。然后 Qml 程序就像一个定时炸弹一样,不知道怎么就冒出一个 Crash,而且 crash 栈还在 libQml 库中,无法定位问题。

先看几个例子吧。。。

例1:用 slot 返回 QObject

class MyItemModel : public QObject
{
    Q_OBJECT
public slots:
    QVariant itemAt(int index) { return QVariant::fromValue(items_.at(index)); }
    QObject* objectAt(int index) { return items_.at(index); }
private:
    QVector<QObject*> items_;
};

例2:数组属性(property)的变化通知(notify)

​class Vehicle : public QObject // 机动车
{
    Q_OBJECT
    Q_PROPERTY(QVariantList wheels READ wheels NOTIFY onWheelsChanged) // 所有轮子
public:
    QVariantList wheels() const { return QVariant::fromValue(wheels_).toList(); }
signals:
    void onWheelsChanged();
public:
    void changedToDoubleWheel() { // 改成双排轮胎
        for (auto w : wheels_)
            delete w;
        wheels_.clear();
        for (int i = 0; i < 8; ++i)
            wheels_.append(new Wheel);
        emit onWheelsChanged();
    }
private:
    QVector<Wheel*> wheels_;
};

例3:使用 Collections 管理单例对象

class StateColors: public QObject
{
    Q_OBJECT
public slots:
    StateColor* get(QByteArray const & name) {
        auto color = colors_.value(name);
        if (color == nullptr) {
            color = new StateColor(QColor(name));
            colors_.insert(name, color);
        }
        return color ;
    }
private:
    QMap<QByteArray, StateColor*> colors_;
};

存在问题的分析

  • 例1

从 slot 中返回 QObject* 或者”派生类“的指针, Qml 将接管该对象的生命期。当 Qml 不需要使用该对象了,就会 destroy 该对象,这不是我们希望的。因为 C++ 已经管理(在 items_ 数组中)该对象,稍后如果访问该对象就会 Crash。

即使返回 QVariant 封装的 QObject* ,问题也是同样的。

  • 例2

通过属性返回的对象(QObject),没有生命期的问题。但是这里 C++ 层删除了对象,却没有提前通知 Qml,仍然会导致内存非法访问,程序会 Crash。

  • 例3

与例1一样的问题,但是因为是单例对象,有一些限制,解决方案不一样。

问题的后果

是不是只要像上面的例子那样写代码,就会出现问题呢?

如果是的,那可能在开发阶段就会发现这样写不行,然后去找原因,或者尝试换个方案,这影响的只是开发进度,不熟悉的人就要加班了。

但是,现实可能更加残酷。

实际在,上面的代码在开发阶段几乎很少出现问题,偶尔遇到也会因为感到莫名其妙,怀疑是 Qt、Qml 的问题,而不去深究。

然后到测试手中,可能也是一个老大难问题,不容易复现,偶尔出现了,给开发看,也还是没有头绪。而且每一次出现,都要看dump、看日志,浪费了大量时间。

这种问题为什么是偶先的呢?

因为 Qml (准确的说是 js 引擎)并不会立即释放没有用的对象,等到它真正释放的时候,已经不知道做了多少操作了。运气好的话(其实宁可不要这种运气),内存一直不紧张,也就不会释放,一切看起来很正常。所以当偶尔出现的时候,问题很难排查。

针对性的完善措施

要时刻提醒自己2条

  • 特别注意 slot 中尽量不要返回对象
  • 修改内部状态,通知 Qml 变化时,要注意先后顺序

下面我们逐个看看怎么去完善上面例子中的代码。

  • 例1

如果保存的对象,仅仅由 MyItemModel 管理,那么可以设置其 parent 为 MyItemModel。

void MyItemModel::appendItem(QObject * item) {
    item->setParent(this); // <--------
    items_.append(item);
}

当时一个对象有 parent 时,Qml 不会托管其生命期。这是最简单的完善方法。 

另一种方案是,改成通过属性(Q_PROPERTY)属性,Qml 不会管理属性的生命期,即使属性是 QObject* 类型的。

当然,这里是个数组,必须用数组属性。Qt 提供了 QQmlListProperty<T>,可以用来从 Qml 访问 Qt 中的数组。

​class MyItemModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<QObject> items READ wheels CONSTANT)
public:
    QQmlListProperty<QObject> items() const;
private:
    QVector<Wheel*> wheels_;
};

其中 items() 方法的实现:

static int itemCount(QQmlListProperty<QObject>*list) {
    return reinterpret_cast<MyItemModel* >(list->data)->items_.size();
}
static QObject* itemAt(QQmlListProperty<QObject>*list, int index) {
    return reinterpret_cast<MyItemModel* >(list->data)->items_.at(index);
}

QQmlListProperty<QObject> MyItemModel ::items()
{
    return {this, this, &itemCount, &itemAt};
}
  • 例2

当我们要删除老的 QObject 对象时,要考虑其是否被 Qml 引用。在删除之前要让 Qml 清除引用。

​class Vehicle : public QObject // 机动车
{

    void changedToDoubleWheel() { // 改成双排轮胎
        auto wheels = wheels_; // save for delete
        wheels_.clear();
        emit onWheelsChanged(); // notify Qml to release references to old Wheels
        for (auto w : wheels)
            delete w;
        for (int i = 0; i < 8; ++i)
            wheels_.append(new Wheel);
        emit onWheelsChanged();
    }

};

这有点类似 QAbstractItemModel 的 beginRemoveRows/endRemoveRows。所以如果有动态增删数组元素的需求,最好用 QAbstractItemModel 这类机制。

  • 例3

与例1一样的问题,但是因为是单例对象,没有合理的 parent。这里可用另一种方式来完善。

class StateColors: public QObject
{
    StateColor* get(QByteArray const & name) {
        auto color = colors_.value(name);
        if (color == nullptr) {
            color = new StateColor(QColor(name));
            QQmlEngine::setObjectOwnership(color, QQmlEngine::CppOwnership); // <----
            colors_.insert(name, color);
        }
        return color ;
    }
};

我们可以明确告知 Qml 这个对象由 C++ 层管理,通过 setObjectOwnership 为 CppOwnership。 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值