在 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。