QML 性能考虑因素和建议
时间考虑因素
作为应用程序开发人员,您必须努力使渲染引擎达到一致的每秒 60 帧刷新率。60 FPS 意味着每帧之间大约有 16 毫秒的处理时间,其中包括将绘制基元上传到图形硬件所需的处理时间。
实际上,这意味着应用程序开发人员应该
- 尽可能使用异步、事件驱动编程
- 使用工作线程进行重要处理
- 绝不手动旋转事件循环
- 在阻塞函数中每帧花费的时间绝不超过几毫秒
否则将导致跳帧,从而对用户体验造成严重影响。
注意: 有一种模式很诱人,但绝对不能使用,那就是创建自己的QEventLoop 或调用QCoreApplication::processEvents() 以避免阻塞在从 QML 调用的 C++ 代码块中。这样做很危险,因为当信号处理程序或绑定中进入事件循环时,QML 引擎会继续运行其他绑定、动画、转换等。这些绑定会产生副作用,例如破坏包含事件循环的层次结构。
剖析
最重要的提示是:使用Qt Creator 中包含的QML Profiler 。了解在应用程序中花费时间的地方,可让您专注于实际存在的问题区域,而不是可能存在的问题区域。更多信息,请参阅Qt Creator: Profiling QML Applications(QML Profiler 应用程序剖析)。
确定哪些绑定运行得最频繁,或应用程序在哪些功能上花费的时间最多,就能决定是否需要优化问题区域,或重新设计应用程序的某些实现细节,以提高性能。如果不进行剖析就试图优化代码,很可能只会带来微小而非显著的性能提升。
JavaScript 代码
大多数 QML 应用程序中都有大量 JavaScript 代码,形式包括动态函数、信号处理器和属性绑定表达式。这通常不是问题。由于 QML 引擎的一些优化,如绑定编译器的优化,它(在某些用例中)可以比调用 C++ 函数更快。不过,必须注意确保不会意外触发不必要的处理。
类型转换
使用 JavaScript 的一个主要代价是,在大多数情况下,当访问 QML 类型的属性时,会创建一个 JavaScript 对象,该对象的外部资源包含底层的 C++ 数据(或对它的引用)。在大多数情况下,这样做的成本相当低,但在其他情况下,成本可能会相当高。将 C++QVariantMap Q_PROPERTY 分配给 QML "变体 "属性就是一个代价高昂的例子。虽然特定类型的序列(QList of int, qreal, bool,QString, andQUrl )应该不贵,但其他列表类型的转换成本也很高(创建一个新的 JavaScript 数组,并逐个添加新类型,以及从 C++ 类型实例到 JavaScript 值的每种类型转换)。
某些基本属性类型(如 "string "和 "url "属性)之间的转换也很昂贵。使用最匹配的属性类型可以避免不必要的转换。
如果必须将QVariantMap 暴露给 QML,请使用 "var "属性而不是 "variant "属性。一般来说,在QtQuick 2.0 及更新版本的所有用例中,"property var "都应被视为优于 "property variant"(注意 "property variant "已被标记为过时),因为它允许存储真正的 JavaScript 引用(可减少某些表达式中所需的转换次数)。
解析属性
属性解析需要时间。虽然在某些情况下可以缓存并重复使用查询结果,但最好还是尽可能避免做不必要的工作。
在下面的示例中,我们有一个经常运行的代码块(在本例中,它是一个显式循环的内容;但也可以是一个常用的绑定表达式),在这个代码块中,我们多次解析了带有 "rect "id 及其 "color "属性的对象:
// bad.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which, value) { console.log(which + " = " + value); } Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { printValue("red", rect.color.r); printValue("green", rect.color.g); printValue("blue", rect.color.b); printValue("alpha", rect.color.a); } var t1 = new Date(); console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations"); } }
相反,我们可以在代码块中只解析一次公共基础:
// good.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which, value) { console.log(which + " = " + value); } Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { var rectColor = rect.color; // resolve the common base. printValue("red", rectColor.r); printValue("green", rectColor.g); printValue("blue", rectColor.b); printValue("alpha", rectColor.a); } var t1 = new Date(); console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations"); } }
仅这一个简单的改动就大大提高了性能。请注意,上述代码还可以进一步改进(因为在循环处理过程中被查找的属性从未改变),方法是将属性解析从循环中移出,如下所示:
// better.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which, value) { console.log(which + " = " + value); } Component.onCompleted: { var t0 = new Date(); var rectColor = rect.color; // resolve the common base outside the tight loop. for (var i = 0; i < 1000; ++i) { printValue("red", rectColor.r); printValue("green", rectColor.g); printValue("blue", rectColor.b); printValue("alpha", rectColor.a); } var t1 = new Date(); console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations"); } }
属性绑定
如果属性绑定表达式引用的任何属性发生变化,该表达式将被重新评估。因此,绑定表达式应尽可能简单。
如果在一个循环中进行了一些处理,但只有处理的最终结果是重要的,通常最好是更新一个临时累加器,然后将其赋值给需要更新的属性,而不是增量更新属性本身,以避免在累加的中间阶段触发对绑定表达式的重新评估。
下面这个假想的示例说明了这一点:
// bad.qml import QtQuick Item { id: root width: 200 height: 200 property int accumulatedValue: 0 Text { anchors.fill: parent text: root.accumulatedValue.toString() onTextChanged: console.log("text binding re-evaluated") } Component.onCompleted: { var someData = [ 1, 2, 3, 4, 5, 20 ]; for (var i = 0; i < someData.length; ++i) { accumulatedValue = accumulatedValue + someData[i]; } } }
onCompleted 处理程序中的循环会导致 "text "属性绑定被重新评估六次(这会导致依赖于文本值的任何其他属性绑定以及 onTextChanged 信号处理程序每次都要重新评估,并且每次都要显示文本)。在这种情况下,这显然是不必要的,因为我们实际上只关心累积的最终值。
可以重写如下:
// good.qml import QtQuick Item { id: root width: 200 height: 200 property int accumulatedValue: 0 Text { anchors.fill: parent text: root.accumulatedValue.toString() onTextChanged: console.log("text binding re-evaluated") } Component.onCompleted: { var someData = [ 1, 2, 3, 4, 5, 20 ]; var temp = accumulatedValue; for (var i = 0; i < someData.length; ++i) { temp = temp + someData[i]; } accumulatedValue = temp; } }
序列提示
如前所述,有些序列类型速度很快(例如QList<int>、QList<qreal>、QList<bool>、QList<QString>、QStringList 和QList<QUrl>),而其他类型则要慢得多。除了尽可能使用这些类型而不是较慢的类型外,您还需要注意其他一些与性能相关的语义,以获得最佳性能。
QObject 首先,序列类型有两种不同的实现方式:一种适用于序列是Q_PROPERTY 的情况(我们称之为引用序列),另一种适用于序列是从QObject 的Q_INVOKABLE 函数返回的情况(我们称之为复制序列)。
引用序列通过QMetaObject::property() 读取和写入,因此是以QVariant 的形式读取和写入的。这意味着,从 JavaScript 中改变序列中任何元素的值将导致三个步骤的发生:从QObject 读取完整的序列(以QVariant 的形式,但随后会被转换为正确类型的序列);改变序列中指定索引处的元素;以及将完整的序列写回QObject (以QVariant 的形式)。
复制序列要简单得多,因为实际序列存储在 JavaScript 对象的资源数据中,因此不会发生读/修改/写循环(而是直接修改资源数据)。
因此,写入引用序列的元素比写入复制序列的元素要慢得多。事实上,写入一个 N 元素参考序列的单个元素,其代价相当于将一个 N 元素的复制序列分配给该参考序列,因此在计算过程中,通常最好先修改临时复制序列,然后再将结果分配给参考序列。
假设存在以下 C++ 类型(并已注册到 "Qt.example "命名空间):
class SequenceTypeExample : public QQuickItem { Q_OBJECT Q_PROPERTY (QList<qreal> qrealListProperty READ qrealListProperty WRITE setQrealListProperty NOTIFY qrealListPropertyChanged) public: SequenceTypeExample() : QQuickItem() { m_list << 1.1 << 2.2 << 3.3; } ~SequenceTypeExample() {} QList<qreal> qrealListProperty() const { return m_list; } void setQrealListProperty(const QList<qreal> &list) { m_list = list; emit qrealListPropertyChanged(); } signals: void qrealListPropertyChanged(); private: QList<qreal> m_list; };
下面的示例以紧密循环的方式写入引用序列的元素,导致性能低下:
// bad.qml import QtQuick import Qt.example SequenceTypeExample { id: root width: 200 height: 200 Component.onCompleted: { var t0 = new Date(); qrealListProperty.length = 100; for (var i = 0; i < 500; ++i) { for (var j = 0; j < 100; ++j) { qrealListProperty[j] = j; } } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
"qrealListProperty[j] = j"
表达式导致内循环中的QObject 属性读取和写入,使这段代码非常不理想。取而代之的是功能相当但速度更快的代码:
// good.qml import QtQuick import Qt.example SequenceTypeExample { id: root width: 200 height: 200 Component.onCompleted: { var t0 = new Date(); var someData = [1.1, 2.2, 3.3] someData.length = 100; for (var i = 0; i < 500; ++i) { for (var j = 0; j < 100; ++j) { someData[j] = j; } qrealListProperty = someData; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
其次,如果属性中的任何元素发生变化,就会发出属性变化信号。如果在序列属性中对某一元素有许多绑定,最好创建一个与该元素绑定的动态属性,并在绑定表达式中使用该动态属性作为符号,而不是序列元素,因为只有当其值发生变化时,才会导致重新评估绑定。
这是一种不常见的使用情况,大多数客户都不会遇到,但值得注意的是,万一你发现自己在做类似的事情:
// bad.qml import QtQuick import Qt.example SequenceTypeExample { id: root property int firstBinding: qrealListProperty[1] + 10; property int secondBinding: qrealListProperty[1] + 20; property int thirdBinding: qrealListProperty[1] + 30; Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { qrealListProperty[2] = i; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
请注意,尽管在循环中只有索引 2 处的元素被修改,但三个绑定都将重新评估,因为变化信号的粒度是整个属性都发生了变化。因此,添加中间绑定有时是有益的:
// good.qml import QtQuick import Qt.example SequenceTypeExample { id: root property int intermediateBinding: qrealListProperty[1] property int firstBinding: intermediateBinding + 10; property int secondBinding: intermediateBinding + 20; property int thirdBinding: intermediateBinding + 30; Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { qrealListProperty[2] = i; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
在上述示例中,每次只对中间绑定进行重新评估,从而大大提高了性能。
值类型提示
值类型属性(字体、颜色、vector3d 等)具有与QObject 属性类似的属性,其通知语义也与序列类型属性不同。因此,上述针对序列的提示也适用于值类型属性。虽然值类型的问题通常较少(因为值类型的子属性数量通常远远少于序列中的元素数量),但不必要地重新评估绑定数量的任何增加都会对性能产生负面影响。
一般性能提示
语言设计产生的一般 JavaScript 性能考虑因素也适用于 QML。最突出的是
- 尽可能避免使用 eval()
- 不要删除对象的属性
常见的界面元素
文本元素
计算文本布局是一项缓慢的操作。请考虑尽可能使用PlainText
格式而不是StyledText
,因为这样可以减少布局引擎的工作量。如果您不能使用PlainText
(因为您需要嵌入图片,或使用标记来指定具有特定格式(粗体、斜体等)的字符范围,而不是整个文本),那么您应该使用StyledText
。
只有当文本可能是(但很可能不是)StyledText
时,才应使用AutoText
,因为该模式会产生解析成本。不应该使用RichText
模式,因为StyledText
只需很少的费用就能提供几乎所有的功能。
图片
图像是任何用户界面的重要组成部分。遗憾的是,由于加载时间、内存消耗量和使用方式等原因,图片也是问题的一大来源。
异步加载
图片通常都比较大,因此最好确保加载图片时不会阻塞用户界面线程。将 QML Image 元素的 "asynchronous"(异步)属性设为true
,就能从本地文件系统异步加载图片(远程图片总是异步加载),这样不会对用户界面的美观造成负面影响。
异步 "属性设置为true
的图像元素将在低优先级工作线程中加载图像。
显式源代码大小
如果您的应用程序加载了大尺寸图像,但在小尺寸元素中显示,请将 "sourceSize "属性设置为正在渲染的元素的尺寸,以确保内存中保留的是图像的较小缩放比例版本,而不是大尺寸版本。
请注意,更改 sourceSize 会导致重新加载图像。
避免运行时合成
请记住,您可以在应用程序中提供预合成图像资源(例如,提供具有阴影效果的元素),从而避免在运行时进行合成工作。
避免平滑图像
仅在需要时启用image.smooth
。它在某些硬件上运行速度较慢,而且如果图像以自然尺寸显示,则不会产生视觉效果。
绘画
避免多次绘制同一区域。使用 "项 "而不是 "矩形 "作为根元素,可避免多次绘制背景。
使用锚点定位元素
使用锚点而不是绑定来相对定位项会更有效。请看下面这个使用绑定将 rect2 相对于 rect1 定位的例子:
Rectangle { id: rect1 x: 20 width: 200; height: 200 } Rectangle { id: rect2 x: rect1.x y: rect1.y + rect1.height width: rect1.width - 20 height: 200 }
使用锚点可以更有效地实现这一目的:
Rectangle { id: rect1 x: 20 width: 200; height: 200 } Rectangle { id: rect2 height: 200 anchors.left: rect1.left anchors.top: rect1.bottom anchors.right: rect1.right anchors.rightMargin: 20 }
使用绑定进行定位(将绑定表达式分配给可视对象的 x、y、宽和高属性,而不是使用锚点)的速度相对较慢,但可以获得最大的灵活性。
如果布局不是动态的,那么指定布局的最有效方法就是对 x、y、宽度和高度属性进行静态初始化。项的坐标总是相对于父对象而言的,因此,如果想固定偏移父对象的 0,0 坐标,就不应使用锚点。在下面的示例中,子矩形对象位于同一位置,但所示的锚点代码不如通过静态初始化使用固定定位的代码节省资源:
Rectangle { width: 60 height: 60 Rectangle { id: fixedPositioning x: 20 y: 20 width: 20 height: 20 } Rectangle { id: anchorPositioning anchors.fill: parent anchors.margins: 20 } }
模型和视图
大多数应用程序至少会有一个模型为视图提供数据。为了达到最佳性能,应用程序开发人员需要了解一些语义。
自定义 C++ 模型
通常需要用 C++ 编写自己的自定义模型,与 QML 中的视图一起使用。虽然任何此类模型的最佳实现在很大程度上取决于它必须满足的用例,但一些一般准则如下:
- 尽可能异步
- 在(低优先级)工作线程中完成所有处理
- 对后端操作进行批处理,以尽量减少(可能很慢的)I/O 和 IPC
需要注意的是,建议使用低优先级的工作线程,以尽量降低 GUI 线程陷入困境的风险(这可能会导致更差的感知性能)。此外,请记住同步和锁定机制可能是导致性能缓慢的重要原因,因此应注意避免不必要的锁定。
ListModel QML 类型
QML 提供了一个ListModel 类型,可用于向ListView 输送数据。只要使用得当,它足以满足大多数用例的要求,而且性能相对较高。
在工作线程内填充
ListModel 可以在 JavaScript 的(低优先级)工作线程中填充元素。开发人员必须在 中明确调用 上的 "sync()",以便将更改同步到主线程。更多信息,请参阅 文档。WorkerScript ListModel WorkerScript
请注意,使用WorkerScript 元素将导致创建一个单独的 JavaScript 引擎(因为 JavaScript 引擎是按线程创建的)。这将导致内存使用量增加。不过,多个WorkerScript 元素都将使用同一个工作线程,因此一旦应用程序已经使用了一个WorkerScript 元素,使用第二个或第三个 元素对内存的影响就可以忽略不计了。
不要使用动态角色
QtQuick 2 中的ListModel 元素比QtQuick 1 中的性能要高得多。性能的提高主要来自对给定模型中每个元素内角色类型的假设--如果类型不变,缓存性能就会显著提高。如果不同元素之间的角色类型可以动态变化,那么这种优化就变得不可能,模型的性能也会下降一个数量级。
因此,默认情况下动态类型是禁用的;开发人员必须专门设置模型的布尔 "dynamicRoles "属性,以启用动态类型(并承受随之而来的性能下降)。如果可以重新设计应用程序以避免动态键入,我们建议您不要使用动态键入。
视图
视图委托应尽可能简单。委托中只需有足够的 QML 来显示必要的信息。任何不是立即需要的附加功能(例如,如果点击后显示更多信息)都应在需要时再创建(请参阅即将介绍的 "懒初始化 "部分)。
以下列表很好地总结了设计委托时应注意的事项:
- 委托中的元素越少,创建的速度就越快,因此视图滚动的速度也就越快。
- 尽量减少委托中绑定的数量;尤其是在委托中使用锚点而不是绑定进行相对定位。
- 避免在委托中使用ShaderEffect 元素。
- 切勿在委托中启用剪切功能。
您可以设置视图的cacheBuffer
属性,以允许异步创建和缓冲可见区域外的委托。对于非繁琐且不太可能在单帧内创建的视图委托,建议使用cacheBuffer
。
请注意,cacheBuffer
会将额外的委托保存在内存中。因此,利用cacheBuffer
所带来的价值必须与额外的内存使用量相平衡。开发人员应使用基准测试来找出最适合其使用情况的值,因为在某些罕见的情况下,使用cacheBuffer
会增加内存压力,导致滚动时帧速率降低。
视觉效果
Qt Quick 2包含多项功能,允许开发人员和设计人员创建极具吸引力的用户界面。流畅性、动态过渡以及视觉效果都能在应用程序中发挥巨大作用,但在使用 QML 中的某些功能时必须小心谨慎,因为它们可能会影响性能。
动画
一般来说,动画属性会导致引用该属性的绑定重新评估。通常,这正是我们所希望的,但在其他情况下,最好是在执行动画之前禁用绑定,然后在动画完成后重新分配绑定。
避免在动画期间运行 JavaScript。例如,应避免为 x 属性动画的每一帧运行复杂的 JavaScript 表达式。
开发人员在使用脚本动画时应特别小心,因为脚本动画是在主线程中运行的(因此如果完成时间过长,可能会导致跳帧)。
粒子
粒子 Qt Quick Particles模块可将精美的粒子效果无缝集成到用户界面中。但是,每个平台的图形硬件能力各不相同,而粒子模块无法将参数限制在硬件所能支持的范围内。要渲染的粒子越多(越大),图形硬件的速度就越快,才能以 60 FPS 的速度渲染。影响更多粒子需要更快的 CPU。因此,必须在目标平台上仔细测试所有粒子效果,以校准在 60 FPS 下可以渲染的粒子数量和大小。
需要注意的是,在不使用粒子系统时(例如在不可见的元素上)可以将其禁用,以避免进行不必要的模拟。
更多详细信息,请参阅《粒子系统性能指南》。
控制元素寿命
通过将应用程序划分为简单的模块化组件(每个组件包含在单个 QML 文件中),您可以加快应用程序的启动速度,更好地控制内存使用情况,并减少应用程序中活动但不可见的元素数量。
懒惰初始化
QML 引擎会做一些棘手的事情,以确保组件的加载和初始化不会导致跳帧。然而,减少启动时间的最好方法莫过于避免做不需要做的工作,并将工作推迟到需要时再做。这可以通过使用Loader 或动态创建组件来实现。
使用加载器
加载器是一个允许动态加载和卸载组件的元素。
- 使用 Loader 的 "active "属性,初始化可以延迟到需要时再进行。
- 使用重载版本的 "setSource() "函数,可以提供初始属性值。
- 将装载程序asynchronous 属性设置为 true 也可以在组件实例化时提高流动性。
使用动态创建
开发人员可以使用 Qt.createComponent() 函数在运行时从 JavaScript 中动态创建一个组件,然后调用 createObject() 将其实例化。根据调用中指定的所有权语义,开发者可能需要手动删除创建的对象。更多信息,请参阅从 JavaScript 动态创建 QML 对象。
销毁未使用的元素
由于是不可见元素的子元素而不可见的元素(例如,选项卡部件(tab-widget)中的第二个选项卡,而第一个选项卡是显示的),在大多数情况下应被懒散地初始化,并在不再使用时被删除,以避免让它们处于活动状态的持续成本(例如,渲染、动画、属性绑定评估等)。
使用 Loader 元素加载的项目可以通过重置 Loader 的 "source"(源)或 "sourceComponent"(源组件)属性来释放,而其他项目则可以通过调用 destroy() 来显式释放。在某些情况下,可能有必要让项目处于活动状态,在这种情况下,至少应将其设置为不可见。
有关活动但不可见元素的更多信息,请参阅即将介绍的 "渲染 "部分。
渲染
QtQuick 2 中用于渲染的场景图允许以 60 FPS 的速度流畅地渲染高度动态的动画用户界面。不过,有些东西会大大降低渲染性能,开发人员应注意尽可能避免这些陷阱。
剪切
剪切功能默认为禁用,只有在需要时才可启用。
剪切是一种视觉效果,而不是优化。它会增加(而不是减少)渲染器的复杂性。如果启用了剪切,一个项目会将其自身的绘制及其子项的绘制剪切到其边界矩形中。这将阻止渲染器自由地重新排列元素的绘制顺序,从而导致次优的最佳场景图遍历。
在委托内部进行剪切尤其糟糕,应尽量避免。
过度绘制和隐形元素
如果有元素完全被其他(不透明)元素覆盖,最好将其 "可见 "属性设置为false
,否则它们将被不必要地绘制。
同样,对于不可见的元素(例如,标签部件中的第二个标签,而第一个标签显示),如果需要在启动时初始化(例如,如果实例化第二个标签的时间太长,而只有在激活标签时才能初始化),则应将其 "可见 "属性设置为false
,以避免绘制它们的成本(尽管如前所述,由于它们仍处于活动状态,因此仍会产生动画或绑定评估的成本)。
半透明与不透明
不透明内容的绘制速度通常比半透明内容快得多。原因是半透明内容需要混合,而渲染器可以更好地优化不透明内容。
有一个半透明像素的图像会被视为全透明图像,尽管它大部分是不透明的。具有透明边缘的BorderImage 也是如此。
着色器
ShaderEffect 类型可以将 GLSL 代码内嵌到Qt Quick 应用程序中,而且开销很小。不过,需要注意的是,片段程序需要为渲染形状中的每个像素运行。当部署到低端硬件且着色器覆盖大量像素时,应将片段着色器控制在几条指令内,以避免性能低下。
使用 GLSL 编写的着色器可以实现复杂的变换和视觉效果,但在使用时应小心谨慎。使用ShaderEffectSource 会导致场景在绘制前被预渲染为 FBO。这种额外的开销可能相当昂贵。
内存分配和收集
应用程序分配的内存量以及分配内存的方式是非常重要的考虑因素。在内存受限的设备上分配内存显然会出现内存不足的情况,除此之外,在堆上分配内存是一项计算成本相当高的操作,而且某些分配策略可能会导致跨页面的数据碎片增加。JavaScript 使用的是受管内存堆,堆会自动进行垃圾回收,这样做有一些好处,但也有一些重要的影响。
用 QML 编写的应用程序会同时使用 C++ 堆和自动管理的 JavaScript 堆的内存。应用程序开发人员需要了解两者的微妙之处,以最大限度地提高性能。
给 QML 应用程序开发人员的提示
本节包含的提示和建议仅为指导原则,可能不适用于所有情况。请务必使用经验指标对您的应用程序进行仔细的基准测试和分析,以便尽可能做出最佳决策。
懒散地实例化和初始化组件
如果您的应用程序包含多个视图(例如多个选项卡),但同一时间只需要一个视图,那么您可以使用懒惰实例化来最大限度地减少任何给定时间内需要分配的内存量。有关详细信息,请参阅前面的 "懒初始化 "部分。
销毁未使用的对象
如果在 JavaScript 表达式中懒散加载组件或动态创建对象,通常最好是手动destroy()
,而不是等待自动垃圾回收。更多信息,请参阅前面的 "控制元素生命周期"一节。
不要手动调用垃圾回收器
在大多数情况下,手动调用垃圾回收器并不明智,因为它会阻塞 GUI 线程相当长的一段时间。这会导致跳帧和生涩的动画,应尽量避免。
在某些情况下,手动调用垃圾收集器是可以接受的(在接下来的章节中会有更详细的解释),但在大多数情况下,调用垃圾收集器是不必要的,而且会适得其反。
避免定义多个相同的隐式类型
如果 QML 元素在 QML 中定义了自定义属性,它就会成为自己的隐式类型。这将在下一节详细解释。如果在一个组件中内联定义了多个相同的隐式类型,就会浪费一些内存。在这种情况下,通常最好明确定义一个新的组件,然后再重复使用。
定义自定义属性通常可以带来有益的性能优化(例如,减少需要绑定或重新评估的绑定数量),或者可以提高组件的模块性和可维护性。在这种情况下,我们鼓励使用自定义属性。不过,如果新类型被多次使用,则应将其拆分为自己的组件(.qml 文件),以节省内存。
重复使用现有组件
如果您正在考虑定义一个新的组件,值得仔细检查一下您的平台的组件集中是否已经存在这样一个组件。否则,你将迫使 QML 引擎为一个类型生成并存储类型数据,而这个类型本质上是另一个已存在且可能已加载的组件的重复。
使用单例类型代替 pragma 库脚本
如果使用 pragma 库脚本来存储应用程序范围内的实例数据,请考虑使用QObject 单例类型来代替。这应该会带来更好的性能,并减少 JavaScript 堆内存的使用。
QML 应用程序中的内存分配
QML 应用程序的内存使用可分为两部分:C++ 堆使用和 JavaScript 堆使用。每部分分配的内存都有一部分是不可避免的,因为它们是由 QML 引擎或 JavaScript 引擎分配的,而其余部分则取决于应用程序开发人员的决定。
C++ 堆将包含
- QML 引擎固定且不可避免的开销(执行数据结构、上下文信息等);
- 每个组件的编译数据和类型信息,包括每个类型的属性元数据,由 QML 引擎生成,取决于应用程序加载了哪些模块和组件;
- 每个对象的 C++ 数据(包括属性值)和每个元素的元对象层次结构,这取决于应用程序实例化了哪些组件;
- 由 QML 导入(库)专门分配的任何数据。
JavaScript 堆将包含
- JavaScript 引擎本身固定且不可避免的开销(包括内置 JavaScript 类型);
- 我们的 JavaScript 集成(已加载类型的构造函数、函数模板等)的固定且不可避免的开销;
- JavaScript 引擎在运行时为每种类型生成的每种类型布局信息和其他内部类型数据(参见下文关于类型的注释);
- 每个对象的 JavaScript 数据("var "属性、JavaScript 函数和信号处理器,以及未优化的绑定表达式);
- 在表达式评估过程中分配的变量。
此外,还将分配一个 JavaScript 堆供主线程使用,并可选择分配另一个 JavaScript 堆供WorkerScript 线程使用。如果应用程序不使用WorkerScript 元素,就不会产生这种开销。JavaScript 堆的大小可达几兆字节,因此为内存受限设备编写的应用程序最好避免使用WorkerScript 元素,尽管它在异步填充列表模型时非常有用。
请注意,QML 引擎和 JavaScript 引擎都会自动生成自己的缓存,缓存中包含有关观察到的类型的类型数据。应用程序加载的每个组件都是独特的(显式)类型,而在 QML 中定义了自己自定义属性的每个元素(组件实例)都是隐式类型。任何未定义自定义属性的元素(组件实例)都会被 JavaScript 和 QML 引擎视为组件明确定义的类型,而非其自身的隐式类型。
请看下面的例子:
import QtQuick Item { id: root Rectangle { id: r0 color: "red" } Rectangle { id: r1 color: "blue" width: 50 } Rectangle { id: r2 property int customProperty: 5 } Rectangle { id: r3 property string customProperty: "hello" } Rectangle { id: r4 property string customProperty: "hello" } }
在前面的示例中,矩形r0
和r1
没有任何自定义属性,因此 JavaScript 和 QML 引擎认为它们属于同一类型。也就是说,r0
和r1
都被视为明确定义的Rectangle
类型。矩形r2
、r3
和r4
都有自定义属性,因此被认为是不同的(隐式)类型。请注意,尽管r3
和r4
具有相同的属性信息,但它们各自被视为不同的类型,原因很简单,自定义属性没有在它们作为实例的组件中声明。
如果r3
和r4
都是RectangleWithString
组件的实例,并且该组件定义中包含名为customProperty
的字符串属性声明,那么r3
和r4
将被视为相同类型(即它们是RectangleWithString
类型的实例,而不是定义自己的隐式类型)。
深入考虑内存分配问题
在决定内存分配或性能权衡时,必须牢记 CPU 缓存性能、操作系统分页和 JavaScript 引擎垃圾回收的影响。应仔细衡量潜在的解决方案,以确保选择最佳解决方案。
任何一套通用指导原则都无法取代应用程序开发人员对计算机科学基本原理的扎实理解,以及对所开发平台实施细节的实际了解。此外,在做出取舍决定时,再多的理论计算也无法取代一套好的基准和分析工具。
碎片化
碎片化是一个 C++ 开发问题。如果应用程序开发人员没有定义任何 C++ 类型或插件,他们可以安全地忽略本节内容。
随着时间的推移,应用程序会分配大量内存,向内存写入数据,并在使用完部分数据后释放部分内存。这可能会导致 "释放 "的内存位于非连续块中,无法返回给操作系统供其他应用程序使用。这还会影响应用程序的缓存和访问特性,因为 "活 "数据可能会分散在物理内存的许多不同页面上。这反过来又会迫使操作系统进行交换,从而导致文件系统 I/O - 相对而言,这是一种极其缓慢的操作。
可以通过以下方法避免碎片:使用池分配器(和其他连续内存分配器);通过仔细管理对象生命周期来减少每次分配的内存量;定期清理和重建缓存;或使用具有垃圾回收功能的内存管理运行时(如 JavaScript)。
垃圾回收
JavaScript 提供垃圾回收功能。在 JavaScript 堆(而不是 C++ 堆)上分配的内存归 JavaScript 引擎所有。引擎会定期收集 JavaScript 堆上所有未引用的数据。
垃圾回收的影响
垃圾回收有利有弊。它意味着手动管理对象生命周期不再那么重要。不过,这也意味着 JavaScript 引擎可能会在应用程序开发人员无法控制的时间启动一个潜在的长期操作。除非应用程序开发人员仔细考虑 JavaScript 堆的使用情况,否则垃圾回收的频率和持续时间可能会对应用程序体验产生负面影响。
手动调用垃圾回收器
用 QML 编写的应用程序(很可能)需要在某个阶段执行垃圾回收。虽然 JavaScript 引擎会在可用空闲内存不足时自动触发垃圾回收,但有时最好还是由应用程序开发人员决定何时手动调用垃圾回收器(尽管通常不会这样)。
应用程序开发人员可能最了解应用程序何时会闲置很长时间。如果 QML 应用程序使用了大量 JavaScript 堆内存,导致在对性能特别敏感的任务(如列表滚动、动画等)期间出现定期的、破坏性的垃圾回收周期,那么应用程序开发人员最好在零活动期间手动调用垃圾回收器。闲置期是执行垃圾回收的理想时间,因为用户不会注意到在活动期间调用垃圾回收器所导致的用户体验下降(跳帧、动画生涩等)。
可以通过在 JavaScript 中调用gc()
来手动调用垃圾收集器。这将导致执行一个全面的收集周期,可能需要几百到一千多毫秒才能完成,因此应尽可能避免。
内存与性能的权衡
在某些情况下,可以用增加内存使用量来换取减少处理时间。例如,将紧循环中使用的符号查找结果缓存到 JavaScript 表达式中的临时变量中,会在评估该表达式时显著提高性能,但这需要分配一个临时变量。在某些情况下,这些权衡是明智的(例如上述情况,几乎总是明智的),但在其他情况下,为了避免增加系统的内存压力,允许处理时间稍长一些可能会更好。
在某些情况下,内存压力增加的影响可能会非常大。在某些情况下,以牺牲内存使用率来换取假定的性能提升,可能会导致页面或缓存崩溃,从而导致性能大幅下降。为了确定在特定情况下哪种解决方案最好,始终有必要仔细衡量权衡的影响。
有关缓存性能和内存时间权衡的深入信息,请参阅以下文章:
- Ulrich Drepper 的出色文章:"每个程序员都应了解的内存知识》,网址:https://people.freebsd.org/~lstewart/articles/cpumemory.pdf。
- Agner Fog 关于优化 C++ 应用程序的优秀手册,网址:http://www.agner.org/optimize/。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.