QML 性能上的注意事项和建议
1. 时序建议
作为应用程序开发人员,我们必须努力让渲染引擎实现一致的 60 帧每秒刷新率。 60 FPS 意味着每个帧之间可以进行大约 16 毫秒的处理,其中包括将绘制图元上传到图形硬件所需的处理。
实际上,这意味着应用程序开发人员应该:
- 尽可能使用异步,事件驱动的编程
- 使用工作线程进行重大处理
- 永远不要手动调整事件循环
- 在阻塞功能中,每帧不要花费超过几毫秒
如果不这样做,将导致跳帧,这对用户体验会有很大的影响。
注意:创建自己的 QEventLoop 或调用 QCoreApplication :: processEvents() 以避免在从 QML 调用的 C++ 代码块中阻塞,这是一个诱人的,但不应该被使用的模式。这是很危险的,因为当在信号处理程序或绑定中输入事件循环时,QML 引擎继续运行着其他绑定,动画,转换等。那些绑定可能会导致副作用,例如破坏包含事件循环的层次结构。
2. 分析
最重要的提示是使用 Qt Creator 附带的 QML 分析器。知道应用程序花费的时间将使我们能够清楚地知道并专注于实际存在问题的范围,而不是猜测可能存在问题的范围。有关如何使用 QML 分析工具的更多信息,请参阅 Qt Creator 手册。
确定最常运行哪些绑定,或者我们的应用程序花费最多时间的功能,将使我们能够决定是否需要优化问题,或重新设计应用程序的一些实现细节,以提高性能。尝试优化代码而不事先进行分析可能导致非常小的而不是显着的性能改进,事倍功半。
3. JavaScript 代码
大多数 QML 应用程序将以动态函数,信号处理程序和属性绑定表达式的形式使用大量的 JavaScript 代码。这通常没有什么问题。由于 QML 引擎中的一些优化,例如对绑定编译器的优化,使用 JavaScript 代码甚至可以(在某些用例中)比调用 C++ 函数更快。但是,必须注意确保不必要的处理不会被意外触发。
3.1. 绑定
QML 中有两种类型的绑定:优化和非优化的绑定。最好保持绑定表达式的简单性,因为 QML 引擎使用了一个优化的绑定表达式求值器,它可以对简单的绑定表达式进行运算,而不需要切换到一个完整的 JavaScript 执行环境。与更复杂(非优化的)绑定相比,这些优化的绑定要有效得多。绑定的优化的基本要求是,在编译时必须知道所访问的每个符号的类型信息。
为了达到最好的优化效果,我们应该在绑定表达式中避免的如下操作:
- 声明中间 JavaScript 变量
- 访问 “var” 属性
- 调用 JavaScript 函数
- 在绑定表达式中构造闭包或定义函数
- 访问直接求值域之外的属性
- 将其写入其他属性,作为副作用
当绑定知道它们所使用的对象和属性的类型时,它们的绑定速度最快。这意味着在某些情况下,绑定表达式中的非 final 属性查找可能会变慢,在这种情况下,查找的属性的类型可能已更改(例如,通过派生类型)。
直接求值域可概括为:
- 表达式作用域对象的属性(对于绑定表达式,这是属性绑定所属的对象)
- 组件中任何对象的 ID
- 组件中根项目的属性
来自其他组件的对象 id 和任何此类对象的属性以及 JavaScript 导入中定义的或包含在 JavaScript 导入中的符号不在即时运算范围内,因此访问这些对象中的任何一个的绑定都不会得到优化。
请注意,如果不能通过 QML 引擎的优化的绑定表达式运算器来优化绑定,因此必须通过完整的 JavaScript 环境进行运算,则上面列出的一些技巧将不再适用。 例如,有时将属性解析的结果缓存在一个非常复杂的绑定中的中间 JavaScript 变量中可能是有益的。接下来的部分将会介绍更多关于这类优化的信息。
3.2. 类型转换
使用 JavaScript 的一个主要代价是,在大多数情况下,访问来自 QML 类型的属性时,将创建一个包含底层 C++ 数据(或其引用)的外部资源的 JavaScript 对象。在大多数情况下,这是相当快捷的,但在其他情况下可能相当耗资源。一个很耗资源的例子就是将一个 C++ QVariantMap 属性通过 Q_PROPERTY 宏转换成 QML 中的 “variant” 属性。列表序列(Lists)也可能很耗资源,但是特定类型的序列(如int、qreal、bool、QString和QUrl 的QList 序列)应该很快捷;其他列表序列类型可能会产生高昂的转换成本(创建一个新的 JavaScript 数组,一个一个地添加新类型,从 C++ 类型实例转换为 JavaScript 值)。
某些基本属性类型(如“string”和“url”属性)之间的转换也可能很耗资源。使用最接近的匹配属性类型将避免不必要的转换。
如果我们必须向 QML 引入 QVariantMap ,使用 “var” 属性而不是 “variant” 属性可能会更好一些。一般来说,对于 QtQuick 2.0 和更新版本的每个用例,“property var” 应被视为优于 “property variant” (请注意,“property variant” 被标记为已过时),因为它允许存储真正的 JavaScript 引用(这可以减少某些表达式中需要的转换次数)。
3.3. 解析属性
属性解析需要消耗时间。在某些情况下,如果可能的话我们可以将查找的结果缓存和重用,以避免做不必要的工作。在下面的例子中,我们有一个经常运行的代码块(在这种情况下,它是一个显式循环的内容;但是它可以是一个经常运算的绑定表达式),在这个例子中,我们解析了 id 为 “rect” 的对象及其 “color” 属性多次:
// bad.qml
import QtQuick 2.12
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 2.12
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 2.12
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");
}
}
3.4. 属性绑定
如果其引用的任何属性发生更改,则属性绑定表达式将被重新运算。因此,绑定表达式应尽可能简单。
例如我们有一个循环,我们要做一些处理,但是只有处理的最终结果是我们需要的,通常更好的处理方式是添加一个临时累加器,然后对这个累加器进行处理,而不是每次都更新属性本身,以避免在累加的过程中触发绑定这个属性的地方进行可能的重新运算。
下面的例子说明了这一点:
// bad.qml
import QtQuick 2.12
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” 属性绑定重新计算了六次(然后导致依赖于 text 属性的任何其他属性绑定以及 onTextChanged 信号处理程序每次都会被重新计算,并且每次更新文本的显示结果)。 在这种情况下,这显然是不必要的,因为我们真的只关心积加的最终结果。
它可以被改写如下:
// good.qml
import QtQuick 2.3
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;
}
}
3.5. 序列建议(Sequence tips)
如前所述,一些序列类型很快(例如 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 元素复制序列分配给该引用序列的,因此通常最好是修改临时复制序列,然后在计算过程中将结果赋值给引用序列。
假设存在(并预先注册到 “Qt.example 1.0” 名称空间)下面的 C++ 类型:
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 2.12
import Qt.example 1.0
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 2.12
import Qt.example 1.0
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 2.12
import Qt.example 1.0
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 2.12
import Qt.example 1.0
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");
}
}
在上面的示例中,每次只针对中间绑定的值的变化进行重新运算,从而可以显著地提升性能。
3.6. 值类型的建议
值类型属性(font, color, vector3d 等)具有类似的 QObject 属性,并将通知语义更改为序列类型属性。因此,上面针对序列给出的提示也适用于值类型属性。 尽管它们通常不涉及值类型的问题(因为值类型的子属性的数量通常远少于序列中元素的数量),但是绑定数量的任何增加都需要重新运算,这都不必要地会对性能产生负面影响。
3.7. 其他 JavaScript 对象
不同的 JavaScrip t引擎提供不同的优化。Qt Quick 2 使用的 JavaScript 引擎针对对象实例化和属性查找进行了优化,但是它提供的优化依赖于某些条件。如果我们的应用程序不符合条件,则 JavaScript 引擎会退化到“慢”模式,从而导致性能大大降低。因此,请始终确保满足以下条件:
- 尽可能避免使用
eval()
- 不要删除对象的属性
4. 通用接口元素
4.1. 文本元素
计算文本布局可能是一个缓慢的操作。考虑尽可能使用 PlainText
格式而不是 StyledText
,因为这会减少布局引擎所需的工作量。如果我们不能使用 PlainText
(因为我们需要嵌入图像,或使用标签指定字符范围以具有与整个文本相对的某些格式(粗体,斜体等)) ,则应使用 StyledText
。
如果文本可能(也可能不)是 StyledText
时才去使用 AutoText
,因为此模式将导致解析成本。不应使用 RichText
模式,因为 StyledText
提供了几乎所有功能,而其成本却很少。
4.2. 图像
图像是任何用户界面的重要组成部分。不幸的是,由于加载它们的时间、消耗的内存量以及使用的方式,它们也是性能问题的一个主要来源。
4.2.1. 异步加载图片
图像通常相当大,所以明智的做法是确保加载图像不会阻塞 UI 线程。将 QML 的 Image
元素的 “asynchronous
” 属性设置为 true
以启用从本地文件系统异步加载图像(远程图像总是使用异步加载),这一般不会对用户界面的美观产生负面影响。
将 “asynchronous
” 属性设置为 true
的 Image
元素将在低优先级的工作线程中加载图像。
4.2.2. 显示设置 sourceSize 属性值
如果我们的应用程序将加载大型图像但将其显示在一个小尺寸的元素中,请将 “sourceSize
” 属性设置为要呈现的元素的大小,以确保将图像的小比例版本保留在内存中,而不是较大的那个。
请注意,更改 sourceSize
将会导致重新加载图像,所以不要频繁的设置这个熟悉值。
4.2.3. 避免运行时的组合
还请记住,通过为应用程序提供预先合成的图像资源(例如,为元素提供阴影效果),可以避免在运行时进行合成工作。
4.2.4. 避免使图像平滑
仅在需要时启用 image.smooth
。它在某些硬件上速度较慢,如果以其原有的尺寸显示图像,启用这个设置也不会有视觉效果。
4.2.5. 绘制
避免多次绘制同一区域。使用 Item 作为根元素而不是 Rectangle 可以避免多次绘制背景。
4.3. 使用锚点定位元素
使用锚点而不是相对于彼此绑定来定位元素的效率更高。对于下面的实例中使用相对于 rect1 的绑定来定位 rect2 的情形:
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
,width
和 height
属性,而不是使用锚点)相对较慢,尽管它有很大的灵活性。
如果布局不是动态的,则指定布局的最有效方法是通过 x
,y
,width
和 height
属性的静态初始化。项目坐标始终相对于其父坐标,因此,如果要相对于父坐标的 0, 0 坐标固定偏移量,则不应使用锚点。在下面的示例中,子 Rectangle 对象位于同一位置,但是显示的锚代码不像通过静态初始化使用固定定位的代码那样资源高效:
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
}
}
5. 模型和视图
大多数应用程序将至少具有一个向视图提供数据的模型。 为了达到最佳性能,应用程序开发人员需要了解一些语义。
5.1. 自定义 C++ 模型
通常希望用 C++ 编写自己的自定义模型以用于 QML 中的视图。尽管任何此类模型的最佳实现都将在很大程度上取决于它必须满足的用例,但一些通用准则如下:
- 尽可能异步
- 在(低优先级)工作线程中执行所有处理
- 批量后端操作,以使(潜在的缓慢)I/O 和 IPC 最小化
- 使用滑动切片窗口缓存结果,其结果是通过分析确定的
重要的是要注意,建议使用低优先级的工作线程以最大程度地减少 GUI 线程的性能风险(这可能会导致性能下降)。另外,请记住,同步和锁定机制可能是导致性能降低的重要原因,因此应注意避免不必要的锁定。
5.2. QML 的 ListModel 类型
QML 提供了一个 ListModel 类型,可用于将数据提供给 ListView。只要正确使用,大多数使用情况就足够了,而且性能相对较高。
5.2.1. 在工作线程中填充数据
ListModel 可以在 JavaScript 中的(低优先级)工作线程中进行数据的填充。开发人员必须在 WorkerScript 中显式调用 ListModel 上的 “sync()”,以使更改与主线程同步。有关更多信息,请参阅 WorkerScript 的文档。
请注意,使用 WorkerScript 元素将导致创建单独的 JavaScript 引擎线程(因为JavaScript引擎是在单独的线程中的)。 这将导致内存使用量的增加。然而,多个 WorkerScript 元素都将使用相同的工作线程,因此一旦应用程序已经使用了一个,则使用第二个或第三个 WorkerScript 元素的内存影响可以忽略不计。
5.2.2. 不要使用动态角色
QtQuick 2 中的 ListModel 元素比 QtQuick 1 中的性能要好得多。性能改进主要来自对给定模型中每个元素中的角色类型的假设 —— 如果类型不改变,则缓存性能显着提高。如果类型可以在元素之间动态变化,则此优化将变得不可能,并且模型的性能将降低一个数量级。
因此,默认情况下禁用动态类型。开发人员必须专门设置模型的 boolean 类型的“ dynamicRoles
”属性值为 true,以启用动态类型输入(并因此而导致性能下降)。如果可以重新设计应用程序以避免使用动态类型,建议不要使用动态类型。
5.3. 视图
视图委托(delegate)应该尽可能地保持简单。在委托中应刚刚有足够少的 QML 元素来显示必要的信息即可。任何不是立即需要的附加功能(例如,如果在点击时显示更多信息)不应该被立即创建(请参见后文的延迟初始化部分)。
以下列表是设计委托时要牢记的事项的一个很好的总结:
- 委托中的元素越少,创建它们的速度就越快,因此视图滚动的速度也就越快。
- 尽量减少委托中的绑定数;特别是,使用锚而不是绑定在委托中进行相对定位。
- 避免在委托中使用 ShaderEffect 元素。
- 切勿在委托中启用 clip 属性。
我们可以设置视图的 cacheBuffer
属性,以允许在可见区域之外异步创建和缓冲 delegate。对于不重要且不太可能在单个框架内创建的视图代理,建议使用 cacheBuffer
。
请记住,cacheBuffer
会在内存中保留额外的代理,因此使用 cacheBuffer
导出的值必须与额外的内存使用相平衡。开发人员应该使用基准测试来找出用例的最佳值,因为在极少数情况下,使用 cacheBuffer
引起的内存压力增加会导致滚动时的帧速率降低。
6. 视觉效果
Qt Quick 2 括一些功能,这些功能使开发人员和设计人员可以创建引人入胜的用户界面。流动性和动态过渡以及视觉效果可以在应用程序中发挥很大的作用,但是在使用 QML 中的某些功能时必须格外小心,因为它们可能会影响性能。
6.1. 动画
一般来说,动画化属性将导致引用该属性的任何绑定被重新运算。 通常,这是期望的但是在其他情况下,最好在执行动画之前禁用绑定,然后在动画完成后重新分配绑定。
避免在动画执行期间运行 JavaScript。例如,应避免为 x
属性动画的每一帧运行复杂的 JavaScript 表达式。
开发人员在使用脚本动画时应格外小心,因为它们是在主线程中运行的(因此,如果它们花费太长时间才能完成,则会导致跳帧)。
6.2. 粒子
Qt Quick Particles 模块允许将精美的粒子效果无缝集成到用户界面中。但是,每个平台都有不同的图形硬件功能,而粒子模块无法将参数限制为硬件可以正常支持的范围。我们尝试渲染的粒子越多(它们越大),图形硬件将需要更快的速度才能以 60 FPS 进行渲染。影响更多的粒子需要更快的 CPU。因此,重要的是仔细测试目标平台上的所有粒子效果,以校准可以以 60 FPS 渲染的粒子的数量和大小。
应该注意的是,在不使用粒子系统时(例如,在不可见元素上)可以禁用粒子系统,以避免进行不必要的模拟。
有关更多详细信息,请参阅 “粒子系统性能指南”。
7. 控制元素生命周期
通过将应用程序划分为简单的模块化组件(每个组件包含在单个 QML 文件中),我们可以缩短应用程序的启动时间,更好地控制内存使用量,并减少应用程序中活动但不可见的元素的数量。
7.1. 延迟初始化
QML 引擎做了一些棘手的事情,以尝试确保组件的加载和初始化不会导致跳帧。但是,没有比减少避免执行不需要的工作以及将工作延迟到必要时来减少启动时间更好的方法了。这可以通过使用 Loader 或动态(dynamically)创建组件来实现。
7.1.1. 使用 Loader
加载器(Loader
)是一个允许组件动态加载和卸载的元素。
- 使用
Loader
的 “active
” 属性,可以延迟直到需要时再初始化。 - 使用重载版本的 “
setSource()
” 函数,可以提供额外的初始属性值。 - 将加载器(
Loader
)异步(asynchronous)属性设置为true
也可以在组件实例化时提高流畅度。
7.1.2. 使用动态创建
开发人员可以使用 Qt.createComponent()
函数在运行时从 JavaScript 内部动态创建组件,然后调用 createObject()
来实例化它。根据调用中指定的所有权语义,开发人员可能必须手动删除所创建的对象。请参阅 JavaScript 中的动态创建 QML 对象,以获得更多信息。
7.2. 销毁不再使用元素
由于元素是不可见元素的子元素(例如,一个标签小部件中的第二个标签,而当前显示的时第一个标签)在大多数情况下应延迟初始化,并在不再使用时删除 ,以避免将其保持活动状态的持续成本(例如,渲染,动画,属性绑定的不断重新计算等)。
加载 Loader
元素的 item
可以通过重新设置加载器的 “source
” 或 “sourceComponent
” 属性来释放,而动态创建的项目可以通过调用 destroy()
来显式地释放。在某些情况下,可能有必要让项目处于活动状态,在这种情况下,它至少应该是不可见(visible === false)的。
有关活动但不可见的元素的更多信息,请参阅后续的“渲染”部分的内容。
8. 渲染
QtQuick 2 中用于渲染的场景图允许以 60 FPS 流畅地渲染高度动态的动画用户界面。然而,有些事情会大大降低渲染性能,开发人员应小心避免这些陷阱。
8.1 裁剪(Clipping)
默认情况下,裁剪(clip 属性)应该是禁用的,并且仅在需要时才启用。
裁剪是一种视觉效果,而不是优化。 它增加(而不是减少)渲染器的复杂性。 如果启用了裁剪,则一个项目会将其自己的画面以及其子对象的画面裁剪到其边界矩形范围内。这使渲染器无法自由地重新排列元素的绘制顺序,从而导致无法启用最佳情况场景图遍历。
在 delegate 中使用 clip 是非常糟糕的,应该不惜一切代价避免。
8.2. 过度绘制(Over-drawing)和不可见元素
如果我们的元素完全被其他(不透明)元素覆盖,则最好将其“ visible
”属性设置为 false,否则将不必要地绘制它们。
类似地,不可见的元素(例如,当前正在显示第一个选项卡的选项卡小部件中的第二个选项卡),但需要在启动时初始化(例如,如果实例化第二个选项卡花费太长时间的成本时,才能够做到选项卡被激活),虽然实例化了第二个选项卡,但是我们应该将其 “visible
” 属性设置为 false
,以避免绘制它们产生的成本(因为如前所说,他们的 visible 将会带来可能的动画或属性值绑定的成本开销)。
同样,不可见的元素(例如,标签小部件中的第二个标签,而当前正在显示第一个标签),但是需要在启动时进行初始化(例如,实例化第二个标签的成本过长而没别的办法,只能在激活选项卡时就执行其初始化),那也应将其“visible
”属性设置为 false,以避免绘制它们的成本(尽管如前所述,它们仍将产生动画或绑定运算的成本(者也注:这些也可以通过在动画或绑定中监听 visible
属性尽量去避免),因为它们仍然处于活动状态)。
8.3. 半透明vs不透明
不透明的内容通常比半透明的绘制要快得多。原因是半透明的内容需要混合,渲染器可以更好地优化不透明的内容。
具有一个半透明像素的图像被视为完全半透明,即使它大部分是不透明的。 具有透明边缘的BorderImage 也是如此。
8.4. 着色器
ShaderEffect 类型使得可以以很少的开销将 GLSL 代码内联放置在 Qt Quick 应用程序中。但是,重要的是要认识到片段程序需要为渲染形状中的每个像素运行。当部署到低端硬件并且着色器覆盖大量像素时,应该将片段着色器保存到一些指令中,以避免性能下降。
使用 GLSL 编写的着色器允许编写复杂的转换和视觉效果,但是应谨慎使用。 使用 ShaderEffectSource 会使场景在绘制之前先渲染到 FBO 中。这种额外的开销可能会非常昂贵。
9. 内存分配和收集
应用程序将分配的内存量以及分配内存的方式是非常重要的考虑因素。除了对内存受限设备上的内存不足状况的明显担忧外,在堆上分配内存是一项计算上相当昂贵的操作,某些分配策略可能会导致跨页面的数据碎片增加。 JavaScript 使用托管内存堆,该内存堆会自动进行垃圾回收,这具有一些优点,但也有一些重要的含义。
用 QML 编写的应用程序既使用 C++ 堆也使用自动管理的 JavaScript 堆中的内存。为了最大化性能,应用程序开发人员需要意识到每个细节,以便最大限度地提高性能。
9.1. 给 QML 应用程序开发人员的提示
本节中提供的技巧和建议仅供参考,可能不适用于所有情况。确保使用经验指标仔细基准测试和分析我们的应用程序,以便做出最佳决策。
9.1.1. 延迟实例化和初始化组件
如果我们的应用程序包含多个视图(例如,多个选项卡),但是在任何时候只需要一个视图,则可以使用延迟实例化来最小化在任何给定时间分配的内存量。有关详细信息,请参阅上一节“** 延迟初始化 **”部分。
9.1.2. 销毁不再使用的对象
如果我们延迟加载组件,或者在 JavaScript 表达式中动态创建对象,通常最好手动销毁它们(动态创建的对象,可以调用 destroy()
销毁),而不是等待自动垃圾收集这样做。有关更多信息,请参阅上文控制元素生命周期的章节。
9.1.3. 不要手动调用垃圾收集器
在大多数情况下,手动调用垃圾回收器是不明智的,因为它将在相当长的一段时间内阻塞 GUI 线程。这可能会导致跳帧和动画抖动,应不惜一切代价避免。
在某些情况下,可以接受手动调用垃圾收集器(这将在后面的部分中进行详细说明),但是在大多数情况下,调用垃圾收集器是不必要的且会适得其反。
9.1.4. 避免复杂的绑定
除了复杂绑定的性能降低(例如,由于必须进入 JavaScript 执行上下文以执行运算)之外,与 QML 优化后可以运算的绑定相比,它们在 C++ 堆和 JavaScript 堆上还占用更多的内存。
9.1.5. 避免定义多个相同的隐式类型
如果 QML 元素具有在 QML 中定义的自定义属性,则它将成为其自己的隐式类型。这将在后面的部分中详细说明。如果在组件中内联定义了多个相同的隐式类型,则会浪费一些内存。在那种情况下,通常最好将这些 QML 显式地定义成一个新组件,然后再使用它。
定义自定义属性通常可以是一种有益的性能优化(例如,减少所需或重新运算的绑定数),或者可以提高组件的模块化和可维护性。在这种情况下,鼓励使用自定义属性。但是,如果多次使用新类型,则应将其定义为自己的组件(.qml 文件)以节省内存。
9.1.6. 重复定义已有的组件
如果我们正在考虑定义一个新的组件,则需要仔细检查一下我们平台的组件集中是否不存在此类组件。否则,我们将迫使 QML 引擎生成和存储该类型的类型数据,而该类型数据本质上是与另一个预先存在的并且可能已经加载的组件重复。
9.1.7. 使用单例类型(singleton types)而不是编译库(pragma library)脚本
如果我们使用实用程序库(pragma library)脚本来存储应用程序范围内的实例数据,请考虑使用 QObject 单例类型singleton types)。这将带来更好的性能,并减少使用 JavaScript 的堆内存。
9.2. QML 应用程序中的内存分配
QML 应用程序的内存使用可能会分为两部分:即 C++ 堆的使用和 JavaScript 堆的使用。在每个部分中分配的一些内存是不可避免的,因为它由 QML 引擎或 JavaScript 引擎分配,而其余的则取决于应用程序开发人员做出的决定。
C++ 堆将包含:
- QML 引擎的固定和不可避免的开销(实现数据结构,上下文信息等);
- 每个组件编译的数据和类型信息,包括每个类型的属性元数据,由 QML 引擎根据应用程序加载的模块和组件来生成;
- 每个对象的 C++ 数据(包括属性值)加上每个元素的元对象层次结构,具体取决于应用程序实例化的组件;
- 任何由 QML 导入(库)专门分配的数据。
JavaScript 堆将包含:
- JavaScript 引擎本身的固定和不可避免的开销(包括内置的 JavaScript 类型);
- 我们的 JavaScript 集成的固定和不可避免的开销(用于加载类型,函数模板的构造函数);
- JavaScript 引擎在运行时针对每种类型生成的每个类型的布局信息和其他内部类型数据(有关类型,请参见下面的注释);
- 每个对象的 JavaScript 数据(“
var
” 属性,JavaScript 函数和信号处理程序以及未优化的绑定表达式); - 在表达式求值期间分配的变量。
此外,将分配一个 JavaScript 堆供主线程使用,并可选地分配另一个供 WorkerScript 线程使用 JavaScript 堆。如果应用程序不使用 WorkerScript 元素,那么就不会产生该开销。JavaScript 堆的大小可能是几兆字节,因此为内存受限的设备编写的应用程序最好避免使用 WorkerScript 元素,尽管它在异步填充 list 模型方面很有用。
请注意,QML 引擎和 JavaScript 引擎都将自动生成它们自己的有关观察到的类型的类型数据的缓存。应用程序加载的每个组件都是一个不同的(显式)类型,并且在 QML 中定义自己的自定义属性的每个元素(组件实例)都是隐式类型。JavaScript 和 QML引擎将未定义任何自定义属性的任何元素(组件的实例)视为该组件显式定义的类型,而不是其自身的隐式类型。
请考虑以下示例:
// Test.qml
import QtQuick 2.12
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 每个都具有自定义属性,因此各自被 JavaScript 和 QML 引擎认为是不同的(隐式)类型。注意,即使 r3 和 r4 具有相同的属性信息,它们也被认为是不同的类型,只是因为自定义属性未在其实例的组件中声明。
如果 r3 和 r4 都是 RectangleWithString 组件的实例,并且该组件定义包括一个名为 customProperty 的字符串属性的声明,则 r3 和 r4 将被视为相同的类型(即,它们将是 RectangleWithString 的实例类型,而不是定义自己的隐式类型)。
// RectangleWithString.qml
import QtQuick 2.12
Rectangle {
id: r2
property string customProperty: "test"
}
// Test.qml
import QtQuick 2.12
import "../common" // RectangleWithString.qml in common dir
Item {
id: root
Rectangle {
id: r0
color: "red"
}
Rectangle {
id: r1
color: "blue"
width: 50
}
Rectangle {
id: r2
property int customProperty: 5
}
RectangleWithString {
id: r3
color: "red"
}
RectangleWithString {
id: r4
color: "blue"
width: 50
}
}
9.3. 深度内存分配注意事项
每当做出有关内存分配或性能折衷的决定时,务必牢记 CPU 缓存性能,操作系统分页和 JavaScript 引擎垃圾回收的影响。应对潜在解决方案进行仔细基准测试,以确保选择最佳解决方案。
没有一套通用准则可以取代对计算机科学基本原理的扎实理解,再加上对应用程序开发人员所开发平台的实现细节的实践知识。此外,在做出权衡决策时,没有任何理论计算可替代一套良好的基准测试和分析工具。
9.3.1. 碎片化
碎片是一个 C++ 开发问题。如果应用程序开发人员没有定义任何 C++ 类型或插件,他们可能会安全地忽略此部分。
随着时间的流逝,应用程序将分配大部分内存,将数据写入该内存,然后在完成使用某些数据后释放部分内存。这可能导致“空闲”内存位于不连续的块中,无法将其返回给操作系统供其他应用程序使用。它还会影响应用程序的缓存和访问特性,因为“活动”数据可能会分散在物理内存的许多不同页面上。这反过来可能会迫使操作系统进行交换,这可能会导致文件系统 I/O —— 相对而言,这是一个极其缓慢的操作。
通过使用池分配器(和其他连续的内存分配器),减少在任何时候分配的内存量,通过仔细管理对象的生命周期、定期清理和重建缓存或利用内存管理的运行时,可以避免(像JavaScript 那样的)碎片垃圾收集。
9.3.2. 垃圾收集
JavaScript 提供垃圾回收。JavaScript 引擎拥有在 JavaScript 堆(而不是C ++堆)上分配的内存。引擎将定期收集 JavaScript 堆上所有未引用的数据。
9.3.2.1. 垃圾收集的影响
垃圾收集有优点也有缺点。这意味着手动管理对象生存期不太重要。但是,这也意味着 JavaScript 引擎可能会在应用程序开发人员无法控制的时间启动可能持续很长时间的操作。除非应用程序开发人员仔细考虑了 JavaScript 堆的使用,否则垃圾回收的频率和持续时间可能会对应用程序体验产生负面影响。
9.3.2.2. 手动调用垃圾收集器
用 QML 编写的应用程序(很可能)将要求在某个阶段执行垃圾回收。尽管当可用内存不足时,JavaScript 引擎会自动触发垃圾回收,但如果应用程序开发人员决定何时手动调用垃圾回收器,则偶尔会更好(尽管通常情况并非如此)。
应用程序开发人员可能会对什么时候长时间处于空闲状态有最好的了解。如果 QML 应用程序使用大量的 JavaScript 堆内存,那么在特别对性能敏感的任务(例如,列表滚动,动画等)中触发定期的垃圾回收是很具破坏性的,此时应用程序开发人员可以考虑手动调用零活动期间的垃圾收集器。空闲时间是执行垃圾收集的理想选择,因为用户不会注意到,而在活动发生时调用垃圾收集器会导致用户体验下降(跳帧,抖动的动画等)。
垃圾收集器可以通过在 JavaScript 内调用 gc()
来手动调用。这将导致执行一个全面的收集周期,这可能需要数百到一千多毫秒来完成,因此,如果可能的话,应避免使用。
9.3.3. 内存与性能的权衡
在某些情况下,可以权衡增加的内存使用量以减少处理时间。例如,在嵌套循环中将查找的结果缓存到 JavaScript 表达式中的临时变量,将大大改善运算该表达式时的性能,但是它却又涉及到分配临时变量。在某些情况下,这些折衷是明智的(例如,上面的情况几乎总是明智的),但在其他情况下,最好允许处理花费更长的时间,以避免增加系统的内存压力 。
在某些情况下,内存压力增加的影响可能是极端的。在某些情况下,为了获得预期的性能提升而对内存使用进行折衷会导致增加的页面异常或缓存异常,从而导致性能大幅下降。为了确定在给定情况下哪种解决方案是最佳的,始终有必要仔细的权衡。
有关缓存性能和内存时间权衡的深入信息,请参阅 Ulrich Drepper 的优秀文章“每个程序员都应该了解的内存”(可从访问 http://ftp.linux.org.ua/pub/docs/developer/general/cpumemory.pdf 截至2012年4月18日),以及有关 C++ 特定优化的信息,请参阅 Agner Fog 的“优化 C++ 应用程序的优秀手册”(可从访问http://www.agner.org/optimize/ 截至2012年4月18日)。
本文欢迎转载,但是请注明出处。