QML性能考量与建议

时间考虑

作为应用程序开发者,你必须努力使渲染引擎实现一致的60帧每秒刷新率。60帧每秒意味着每个帧之间大约有16毫秒的时间可以用来处理,这包括将绘制原语上传到图形硬件所需的处理。

在实践中,这意味着应用程序开发者应该

  • 尽可能使用异步、事件驱动的编程
  • 使用工作线程进行大量处理
  • 切勿手动旋转事件循环
  • 切勿在每帧的阻塞函数中耗时超过几个毫秒

如果不这样做,将会导致跳过帧,这对用户体验有很大的影响。

注意:一种看似诱人但绝不能使用的模式是创建自己的QEventLoop或调用QCoreApplication::processEvents()以避免在从QML调用的C++代码块中阻塞。这是危险的,因为当事件循环在信号处理程序或绑定中启动时,QML引擎会继续运行其他绑定、动画、过渡等。这些绑定可能导致副作用,例如,摧毁包含您的事件循环的层次结构。

分析

最重要的提示是:使用Qt Creator附带的自带QML分析器。了解应用程序中的时间花费在哪里,将使您能够专注于实际存在的问题区域,而不是潜在的问题区域。有关如何使用QML分析工具的更多信息,请参阅Qt Creator手册

确定运行最频繁的绑定或应用程序花费最多时间的函数,将使您决定是否需要优化问题区域,或者重新设计应用程序的一些实现细节以提高性能。在没有分析的情况下尝试优化代码可能会带来非常轻微的性能改进而不是重大的改进。

JavaScript代码

大多数QML应用程序将包含大量JavaScript代码,形式为动态函数、信号处理程序和属性绑定表达式。这通常不是问题。多亏了QML引擎的一些优化,如对绑定编译器的优化,它(在某些用例中)可以比调用C++函数更快。但是,必须小心,以确保不会意外触发不必要的过程。

类型转换

使用JavaScript的一个主要成本是,在大多数情况下,访问QML类型的属性时,会创建一个包含底层C++数据(或其引用)的外部资源的JavaScript对象。在大多数情况下,这并不昂贵,但在某些情况下可能会非常昂贵。一个成本较高的例子是把C++的QVariantMap Q_PROPERTY 赋值到一个QML的"variant"属性。列表也可能很昂贵,尽管特定类型的序列(例如 int 类型、qreal 类型、bool 类型和 QStringQUrl 的 QList)应该比较经济;其他列表类型涉及昂贵的转换成本(创建一个新的JavaScript数组,并根据每个类型逐一添加,再从C++类型实例到JavaScript值的类型转换)。

在一些基本属性类型(如 "string" 和 "url" 属性)之间转换也可能很昂贵。使用匹配的属性类型可以避免不必要的转换。

如果您必须将QVariantMap暴露给QML,请使用"var"属性而不是"variant"属性。一般而言,从 QtQuick 2.0 及更高版本开始(请注意 "property variant" 已标记为已过时),"property var" 应该被认为优于"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>、QStringListQList<QUrl>),而其他类型的运行速度会慢得多。除了尽可能使用这些类型代替较慢的类型外,还有一些与性能相关的语义需要您注意,以实现最佳性能。

首先,序列类型有两种不同的实现方式:一种是在QObjectQ_PROPERTY(我们将这种类型称为引用序列)中,另一种是从QObjectQ_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》提供几乎所有的功能,且成本仅为前者的一小部分。

图像

图像是任何用户界面的重要组成部分。不幸的是,由于加载图像所花费的时间、它们消耗的内存以及它们的使用方式,它们也是许多问题的根源。

异步加载

图像常常相当大,因此确保加载图像不会阻塞UI线程是很明智的。将QML Image元素的“asynchronous”属性设置为true以启用从本地文件系统(远程图像始终异步加载)的图像异步加载,而这不会对用户界面的美观产生负面影响。

将“asynchronous”属性设置为true的图像元素将以低优先级的工人线程加载图像。

显式源大小

如果您的应用程序加载了一张大图像,但在一个小型尺寸的元素中显示它,则将“sourceSize”属性设置为您正在渲染的元素的大小,以确保保持较小的图像版本在内存中,而不是大版本。

请注意,更改sourceSize将导致图像重新加载。

避免运行时合成

另外,请记住,您可以通过在应用程序中提供预合成的图像资源来避免在运行时进行合成工作(例如,提供带有阴影效果的元素)。

避免平滑图像

仅在需要时启用image.smooth。在某些硬件上,它较慢,并且如果以自然大小显示图像时,它没有任何视觉效果。

绘图

避免多次绘制相同区域。使用Item作为根元素而不是Rectangle以避免多次绘制背景。

使用锚点定位元素

相对于使用绑定,使用锚点定位元素彼此之间的位置更加高效。考虑以下使用绑定将 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、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
    }
}

模型和视图

大多数应用程序都至少有一个模型向视图提供数据。应用开发者需要了解一些语义,以实现最大性能。

自定义 C++ 模型

常常希望在 QML 中使用自己的自定义模型与视图一起使用。虽然任何此类模型的最佳实现将严重取决于它必须履行的用例,但以下是一些一般性指南

  • 尽可能异步
  • 在一个(低优先级)的工作线程中执行所有处理
  • 批量处理后端操作,以最小化(可能缓慢的)I/O 和 IPC

请注意,建议使用低优先级的工作线程,以最大程度地减少使 GUI 线程饥饿的风险(这可能导致性能感觉更差)。此外,请记住,同步和锁定机制可能是导致性能缓慢的一个重要原因,因此应尽量避免不必要的锁定。

ListModel QML 类型

QML 提供了一个 ListModel 类型,可用于向 ListView 提供数据。只要使用得当,它就足够用于大多数用例,并且相对高效。

在工作线程中填充

在 JavaScript 中,可以在一个(低优先级)的工作线程中填充 ListModel 元素。开发人员必须在 ListModel 上显式调用 "sync()",以便从 WorkerScript 中同步到主线程。有关更多信息,请参阅 WorkerScript 文档。

请注意,使用 WorkerScript 元素将导致创建一个单独的 JavaScript 引擎(因为 JavaScript 引擎是按线程划分的)。这将导致内存使用增加。然而,所有多个 WorkerScript 元素都将使用同一个工作线程,因此在使用一个应用程序已经使用了工作线程之后,使用第二个或第三个 WorkerScript 元素的内存影响是可以忽略不计的。

不要使用动态角色

ListModel 元素中,与 QtQuick 2 相比,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

Loader 是一个允许动态加载和卸载组件的元素。

  • 使用 Loader 的 "active" 属性,初始化可以推迟到需要时。
  • 使用重载的 "setSource()" 函数版本的初始属性值。
  • 将 Loader 的 asynchronous 属性设置为 true 也可能在组件实例化时提高流畅度。

使用动态创建

开发者可以使用 Qt.createComponent() 函数在运行时从 JavaScript 动态创建一个组件,然后调用 createObject() 来实例化它。根据调用中指定的所有者语义,开发者可能需要手动删除创建的对象。有关更多信息,请参阅从 JavaScript 创建动态 QML 对象

销毁未使用元素

由于它们是不可见元素的子元素而不可见(例如,在一个标签控件中的第二个标签,当第一个标签显示时),大多数情况下,它们应被懒加载初始化,并在不再使用时删除,以避免留下它们活动的持续成本(例如,渲染、动画、属性绑定评估等)。

通过将 Loader 的 "source" 或 "sourceComponent" 属性重置,可以释放使用 Loader 元素加载的项目,而其他项目可以通过调用 destroy() 在它们上显式释放。在某些情况下,可能需要保持项目活跃,在这种情况下,至少应将其设置为不可见。

有关活动但不可见元素的信息,请参阅即将到来的渲染部分。

渲染

QtQuick 2 中用于渲染的场景图允许高度动态、动画化的用户界面以 60 FPS 的流畅度渲染。然而,某些事情可能会显著降低渲染性能,开发者应尽可能避免这些陷阱。

裁剪

默认情况下裁剪是禁用的,只有在需要时才应启用。

裁剪是一种视觉效果,NOT是优化。它会增加(而不是减少)渲染器的复杂度。如果启用裁剪,一个项目将剪裁其自己的绘制,以及其子项目的绘制,到其边界矩形。这阻止了渲染器自由重新排列元素的绘制顺序,导致次优的最佳场景图遍历。

委托内的裁剪特别差,应不惜一切代价避免。

过度绘制和不可见元素

如果您有被其他(不透明的)元素完全覆盖的元素,最好将它们的"visible"属性设置为false,否则它们将无谓地被绘制。

同样,尽管如前所述,它们仍然会因其激活状态而担负任何动画或绑定评估的成本,但这些不可见的元素(例如,第一个标签页显示时,第二个标签页中的标签)需要在启动时间初始化(例如,如果实例化第二个标签页的成本太高,只能在其激活时完成),应该将它们的"visible"属性设置为false,以避免绘制它们的成本。

半透明和不透明

不透明内容通常比半透明内容绘制得快得多。原因是半透明内容需要混合,而渲染器可以更好地优化不透明内容。

包含一个半透明像素的图像被视为完全半透明,即使它大部分是不透明的。对于透明边缘的< a href="qml-qtquick-borderimage.html" translate="no">BorderImage也是同样的情况。

着色器

ShaderEffect 类型使得在 Qt Quick 应用程序中内联放置 GLSL 代码成为可能,同时开销非常小。然而,重要的是要认识到碎程序需要为渲染形状中的每个像素运行。当部署到低端硬件且着色器覆盖大量像素时,应将片段着色器保持为几个指令,以避免性能下降。

用 GLSL 编写的着色器可以编写复杂的转换和视觉效果,然而它们应谨慎使用。使用 ShaderEffectSource 会将场景预渲染到 FBO 中,然后再进行绘制。这种额外的开销可能相当高昂。

内存分配和回收

应用程序将分配的内存量以及内存分配的方式是非常重要的考虑因素。除了显然的关于内存受限设备上内存不足条件的担忧之外,在堆上分配内存是一种相对昂贵的计算操作,某些分配策略可能导致页面间数据碎片化增加。JavaScript 使用一个自动垃圾回收的管理内存堆,这有一些优点,但也一些重要的影响。

用 QML 编写的应用程序使用 C++ 堆和自动管理的 JavaScript 堆的内存。应用开发者需要意识到各自的微妙之处,以便最大限度地提高性能。

为 QML 应用开发者提供的一些建议

本节包含的提示和建议仅作为指导,可能不适用于所有情况。请务必使用经验指标仔细基准测试和分析您的应用程序,以确保做出最佳决策。

惰性创建和初始化组件

如果您的应用程序包含多个视图(例如,多个标签),但任何给定时间只需要一个,您可以使用惰性创建来最小化在任何给定时间需要分配的内存量。有关更多信息,请参阅先前的关于 惰性初始化 的部分。

销毁未使用的对象

如果您对组件进行惰性加载,或在使用 JavaScript 表达式时动态创建对象,手动调用 destroy() 通常比等待自动垃圾回收以这样做更好。有关更多信息,请参阅先前的关于 控制元素生命周期 的部分。

不要手动调用垃圾回收器

在大多数情况下,手动调用垃圾回收器并不明智,因为它将在一段时间内阻塞 GUI 线程。这可能导致跳过帧和抖动的动画,应避免这种情况。

在某些情况下,手动调用垃圾回收器是可以接受的(将在下一部分中详细介绍),但大多数情况下,调用垃圾回收器是不必要的且适得其反。

避免定义多个相同的隐式类型

如果一个 QML 元素在 QML 中定义了一个自定义属性,它就成为了自己的隐式类型。这将在下一部分中详细介绍。如果在组件中内联定义了多个相同的隐式类型,将浪费一些内存。在这种情况下,通常更好的是显式定义一个新组件,然后可以重用它。

定义自定义属性通常是一种有益的性能优化,例如减少所需的或重新评估的绑定数量,或提高组件的模块化和可维护性。在这些情况下,鼓励使用自定义属性。然而,如果该类型被多次使用,则应将其拆分为自己的组件(.qml 文件),以节省内存。

重用现有组件

如果您正在考虑定义一个新的组件,请务必再次检查该组件是否已在您的平台组件集中存在。否则,您将迫使 QML 引擎为实际上是一个预存在并可能已经加载的组件的类型生成和存储类型数据。

使用单例类型而不是指令库脚本

如果您使用指令库脚本来存储应用程序范围内的实例数据,请考虑使用一个 QObject 单例类型。这应会带来更好的性能,并将减少使用的 JavaScript 堆内存。

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元素,尽管它在异步填充列表模型方面非常有用。

注意,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"
    }
}

在上面的示例中,矩形 r0r1 没有任何自定义属性,因此JavaScript和QML引擎认为它们都是同一类型。也就是说,r0r1 都被认为是显式定义的 Rectangle 类型。矩形 r2r3r4 每个都有自己的自定义属性,每个都被认为是不同的(隐式)类型。注意,r3r4 被认为是不同的类型,尽管它们的属性信息相同,仅仅是因为它们实例的组件中没有声明自定义属性。

如果 r3r4 都是 RectangleWithString 组件的实例,并且该组件定义中包含了名为 customProperty 的字符串属性的声明,那么 r3r4 被认为是同一类型(也就是说,它们是 RectangleWithString 类型的实例,而不是定义了自己的隐式类型)。

内存分配深度考虑

在做出关于内存分配或性能权衡的决定时,重要的是要保持对CPU缓存性能、操作系统分页以及JavaScript引擎垃圾回收影响的认识。应仔细基准测试潜在解决方案,以确保选择最佳方案。

没有任何一般性指南可以取代对计算机科学基本原理的深刻理解,以及应用开发者所开发平台实现细节的实践知识。此外,当做出权衡决策时,任何数量的理论计算都不能替代良好的基准和分析工具。

碎片化

碎片化是C++开发中存在的问题。如果应用开发者没有定义任何C++类型或插件,他们可以安全地忽略本节。

随着时间的推移,应用将分配大量内存,向该内存写入数据,一旦完成对某些数据的使用,将释放其中的一部分。这可能会导致“空闲”内存位于非连续的块中,无法返回给操作系统供其他应用使用。它还会影响应用缓存和访问特性,因为活跃的数据可能分散在许多不同的物理内存页面上。这反过来可能会迫使操作系统进行交换,从而导致文件系统I/O - 相对而言,这是一个极其缓慢的操作。

可以通过使用池分配器(以及其他连续内存分配器)、通过精心管理对象的生命周期以减少在任何一次分配的内存量、通过定期清洗和重建缓存,或者通过使用具有垃圾回收功能的内存管理运行时(如JavaScript)来避免碎片化。

垃圾回收

JavaScript提供了垃圾回收。分配在JavaScript堆(与C++堆相对)上的内存由JavaScript引擎拥有。引擎将定期收集JavaScript堆上所有未引用的数据。

垃圾回收的影响

垃圾回收既有优点也有缺点。这意味着手动管理对象生命期不再那么重要。然而,这也意味着JavaScript引擎可能在应用开发者无法控制的时间启动一个可能持续很长时间的操作。除非应用开发者仔细考虑JavaScript堆的使用情况,否则垃圾回收的频率和持续时间可能会对应用程序体验产生负面影响。

手动触发垃圾回收

用QML编写的应用程序(很可能)在某个阶段需要执行垃圾回收。虽然当可用空闲内存量低时,垃圾回收将由JavaScript引擎自动触发,但有时如果应用开发者决定手动触发垃圾回收可能是更好的(尽管通常并非如此)。

应用程序开发者可能最了解应用程序何时会长时间空闲。如果一个QML应用程序使用了大量的JavaScript堆内存,导致在特别注意性能的任务(例如,列表滚动、动画等)期间出现周期性且破坏性的垃圾回收周期,那么在零活动期间手动调用垃圾回收器可能会有所帮助。空闲时间是执行垃圾回收的理想选择,因为用户不会注意到任何由于在活动发生期间调用垃圾回收器而导致的用户体验下降(跳帧、动画顿挫等)。

可以通过在JavaScript中调用gc()来手动调用垃圾回收器。这将导致执行全面的收集周期,这可能会占用几百到一千多个毫秒才能完成,因此应该尽可能地避免。

内存与性能权衡

在某些情况下,可以通过增加内存使用来换取减少处理时间。例如,将用于在密集循环中查找符号的值缓存在JavaScript表达式中的一个临时变量中,将大大提高评估该表达式时的性能,但这也需要分配一个临时变量。在某些情况下,这些权衡是合理的(如上面的情况几乎总是合理的),但在其他情况下,可能最好允许处理时间稍微长一点,以避免增加系统内存压力。

在某些情况下,增加内存压力的影响可能会非常严重。在某些情况下,为了换取假定的性能提升而牺牲内存使用可能会导致页面碎转或缓存碎转增加,从而使性能大幅下降。总是有必要仔细基准测试权衡的影响,以确定在特定情况下哪种解决方案最好。

有关缓存性能和内存-时间权衡的详细信息,请参阅以下文章

© 2024 Qt公司有限公司。此处包含的文档贡献是各自所有者的版权。此处提供的文档是根据由自由软件基金会出版的《GNU自由文档许可证》第1.3版在以下条款下授权的:http://www.gnu.org/licenses/fdl.html。Qt及其相关标志是Qt公司有限公司在芬兰以及/或其他国家的注册商标。所有其他商标均为其各自所有者的财产。