本页内容

性能注意事项与建议

时间方面的考虑

作为应用程序开发人员,您通常会努力让渲染引擎达到每秒 60 帧的稳定刷新率。 根据您的硬件和要求,这个数字可能会有所不同,但 60 FPS 非常常见。60 FPS 意味着每帧之间大约有 16 毫秒的时间可以进行处理,其中包括将绘制基元上传到图形硬件所需的处理。

实际上,这意味着应用程序开发人员应:

  • 尽可能采用异步、事件驱动的编程方式
  • 使用工作线程来处理耗时的任务
  • 切勿手动循环事件循环
  • 在阻塞函数中,每帧的处理时间绝不能超过几毫秒

若不遵守这些原则,将导致帧丢失,从而对用户体验产生严重影响。

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

性能分析

最重要的提示是:使用 QML ProfilerQt Creator 中包含的工具(或Qt Extension for Visual Studio Code 中的跟踪查看器)。了解应用程序中时间的消耗情况,将使您能够专注于实际存在的问题区域,而不是潜在存在的问题区域。有关更多信息,请参阅Qt Creator :QML 应用程序性能分析以及Qt Extension for Visual Studio Code :QML 代码性能分析。

确定哪些绑定被调用最频繁,或者应用程序在哪些函数上花费了最多时间,将有助于您判断是需要优化问题区域,还是重新设计应用程序的一些实现细节以提升性能。如果不进行性能分析就尝试优化代码,很可能只会带来微乎其微的性能提升,而非显著的改进。

JavaScript 代码

大多数 QML 应用程序中都会包含一些 JavaScript 代码,形式包括属性绑定表达式、函数和信号处理程序。这通常不是问题。得益于诸如 Qt Quick Compiler等先进工具的帮助,简单的函数和绑定可以运行得非常快。不过,必须小心确保不会意外触发不必要的处理。 QML Profiler 可以显示关于 JavaScript 执行及其触发原因的丰富细节。

类型转换

使用 JavaScript 的一个主要开销在于:在某些情况下,当访问 QML 类型的属性时,会创建一个 JavaScript 对象,该对象包含一个外部资源,其中存储了底层的 C++ 数据(或对其的引用)。在大多数情况下,这的开销相当小,但在某些情况下却可能相当大。 在处理大型且复杂的值类型或序列类型时需格外谨慎。每当您就地修改这些类型或将其赋值给另一个属性时,QML 引擎都必须对它们进行复制。当这成为性能瓶颈时,请考虑改用对象类型。 对象类型的列表不会出现与值类型列表相同的问题,因为对象类型的列表是通过 `QQmlListProperty` 实现的。

简单值类型之间的转换大多开销较低。不过也有例外:将字符串转换为URL可能涉及构建一个 `QUrl ` 实例,这会消耗较多资源。

解析属性

属性解析需要时间。虽然查找操作通常经过优化,在后续执行时会快得多,但如果可能的话,最好还是完全避免进行不必要的工作。

在下面的示例中,我们有一段经常执行的代码(此处是显式循环的内容;但也可以是经常被求值的绑定表达式等),其中多次解析了 ID 为“rect”的对象及其“color”属性:

// bad.qml
import QtQuick

Item {
    width: 400
    height: 200
    Rectangle {
        id: rect
        anchors.fill: parent
        color: "blue"
    }

    function printValue(which: string, value: real) {
        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");
    }
}

每次检索 `rect.color ` 时,QML 引擎都必须:

  • 在 JavaScript 堆上分配一个值类型包装器。
  • 运行 `Rectangle` 的 `color ` 属性的获取器。
  • 将生成的QColor 复制到值类型包装器中。

我们无需重复执行这 4 次操作。相反,我们可以在代码块中仅解析一次共同的基类:

// good.qml
import QtQuick

Item {
    width: 400
    height: 200
    Rectangle {
        id: rect
        anchors.fill: parent
        color: "blue"
    }

    function printValue(which: string, value: real) {
        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: string, value: real) {
        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”属性绑定被重新评估六次(这进而导致任何依赖于 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;
    }
}

序列使用技巧

如前所述,处理值类型的序列时必须格外谨慎。

首先,序列类型在两种截然不同的场景下表现不同:

  • 如果该序列是QObject 的Q_PROPERTY (我们称之为引用序列),
  • 如果该序列是由某个QObject 的Q_INVOKABLE 函数返回的(我们称之为“引用序列”),

每当引用序列发生变化时——无论是在您的 JavaScript 代码中,还是在原始对象上——都会通过QMetaObject 进行读写操作。作为一种优化措施,引用序列(以及引用值类型)可能会被延迟加载。 实际内容仅在首次使用时才会被检索。这意味着,若通过 JavaScript 更改序列中任何元素的值,将会导致:

  • 可能从QObject 中读取内容(如果采用延迟加载)。
  • 更改该序列中指定索引处的元素。
  • 将整个序列写回QObject 。

复制序列则简单得多,因为实际序列存储在 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");
    }
}

另一个应避免的常见模式是“读取-修改-写入”循环,即逐个读取元素、进行修改,然后将结果写回序列属性。与前例类似,这会导致每次迭代中都发生QObject 属性的读写操作:

// 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] = qrealListProperty[j] * 2;
            }
        }
        var t1 = new Date();
        console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
    }
}

相反,应手动创建序列的副本,修改副本,然后将结果赋值回属性:

// good.qml
import QtQuick
import Qt.example

SequenceTypeExample {
    id: root
    width: 200
    height: 200

    Component.onCompleted: {
        var t0 = new Date();
        for (var i = 0; i < 500; ++i) {
            let data = [...qrealListProperty];
            for (var j = 0; j < 100; ++j) {
                data[j] = data[j] * 2;
            }
            qrealListProperty = data;
        }
        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");
    }
}

在上例中,每次只有中间绑定会被重新评估,从而显著提升性能。

值类型提示

值类型属性(如 font、color、vector3d 等)在QObject 属性和变更通知语义方面与序列类型属性类似。因此,上文针对序列类型给出的建议同样适用于值类型属性。 虽然对于值类型而言,这通常不是什么大问题(因为值类型的子属性数量通常远少于序列中的元素数量),但任何不必要地增加重新评估的绑定数量都会对性能产生负面影响。

通用性能建议

源于语言设计的一般 JavaScript 性能考虑因素同样适用于 QML。最显著的是:

  • 尽可能避免使用 eval()
  • 不要删除对象的属性

常见界面元素

文本元素

计算文本布局可能是一项耗时的操作。 请尽可能使用PlainText 格式代替StyledText ,因为这可以减少布局引擎的工作量。如果无法使用PlainText (例如需要嵌入图片,或者需要使用标签指定特定字符范围进行格式设置(如加粗、斜体等),而非对整个文本进行格式设置),则应使用StyledText 。

仅当文本可能(但可能性不大)属于StyledText 时,才应使用AutoText ,因为该模式会产生解析开销。不应使用RichText 模式,因为StyledText 能以极低的开销提供其几乎所有功能。

图片

图像 是任何用户界面的重要组成部分。遗憾的是,由于其加载时间、占用的内存以及使用方式等因素,图像也是导致问题的主要来源之一。

异步加载

图像通常体积较大,因此应确保加载图像时不会阻塞 UI 线程。 将 QML Image 元素的“asynchronous”属性设置为true ,即可启用从本地文件系统异步加载图片(远程图片始终以异步方式加载),前提是这不会对用户界面的美观性产生负面影响。

将“asynchronous”属性设置为true 的Image元素,将在低优先级的工作线程中加载图片。

显式源尺寸

如果您的应用程序加载了一张大图片,但将其显示在较小尺寸的元素中,请将“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++ 模型

通常,您可能希望使用后端语言(如 C++)编写自定义模型,以便与 QML 中的视图配合使用。虽然此类模型的最佳实现方式在很大程度上取决于其需要满足的使用场景,但一些通用准则如下:

  • 尽可能采用异步处理
  • 在(低优先级的)工作线程中完成所有处理
  • 将后端操作进行批处理,以最大限度地减少(可能较慢的)I/O 和 IPC 操作

需要注意的是,建议使用低优先级的工作线程,以最大限度地降低GUI线程饥饿的风险(这可能会导致感知性能变差)。此外,请记住,同步和锁定机制可能是导致性能变慢的重要原因,因此应谨慎避免不必要的锁定。

ListModel QML 类型

Qt Qml Models提供了一种ListModel 类型,可用于向ListView 提供数据。它适用于快速原型设计,但不适合处理大量数据。必要时请使用合适的QAbstractItemModel 。

在工作线程中填充数据

ListModel 在 JavaScript 中,可以在(低优先级)工作线程中向元素赋值。开发者必须在WorkerScript 内部显式调用ListModel 上的sync() 方法,才能将更改同步到主线程。有关更多信息,请参阅WorkerScript 文档。

请注意,使用WorkerScript 元素将导致创建一个独立的 JavaScript 引擎(因为 JavaScript 引擎是按线程分配的)。这将导致内存使用量增加。 不过,多个 `WorkerScript ` 元素将共用同一个工作线程,因此一旦应用程序已使用一个 `WorkerScript ` 元素,使用第二个或第三个该元素对内存的影响可以忽略不计。但另一方面,额外的工作线程脚本不会并行运行。

请勿使用动态角色

出于优化目的,ListModel 元素假设给定模型中每个元素内的角色类型是稳定的。如果类型会在不同元素之间动态变化,模型的性能将大幅下降。

因此,动态类型默认处于禁用状态;开发者必须明确将模型的dynamicRoles 布尔属性设置为true,才能启用动态类型(并因此承受相应的性能下降)。我们建议您除非绝对必要,否则不要使用动态类型。

视图

视图委托应尽可能保持简单。委托中的 QML 代码应仅包含显示必要信息所需的最小内容。任何非立即需要的功能(例如,点击后显示更多信息),都应等到需要时再创建(参见下文关于懒加载的部分)。

以下列表很好地总结了设计委托时需要注意的事项:

  • 委托中包含的元素越少,创建速度就越快,从而使视图滚动得更快。
  • 将委托中的绑定数量控制在最低限度;特别是,在委托内部进行相对定位时,应使用锚点而非绑定。
  • 避免在委托中使用ShaderEffect 元素。
  • 切勿在委托上启用裁剪功能。

您可以设置视图的cacheBuffer 属性,以允许在可见区域之外异步创建和缓冲委托。对于非简单的视图委托,且不太可能在单帧内创建的,建议使用cacheBuffer 。

请注意,cacheBuffer 会将额外的委托保留在内存中。因此,使用cacheBuffer 所带来的收益必须与额外的内存占用相权衡。开发者应通过基准测试来为自己的用例找到最佳值,因为使用cacheBuffer 造成的内存压力增加,在极少数情况下可能会导致滚动时的帧率下降。

若要进一步提升性能,请考虑在视图中启用项复用功能。更多信息请参阅Reusing Items for ListView 和Reusing Items for TableView and TreeView 。

视觉效果

Qt Quick 包含多项功能,可帮助开发人员和设计师创建极具吸引力的用户界面。流畅的动态过渡以及视觉效果可在应用程序中发挥显著作用,但在使用 QML 中的某些功能时需谨慎,因为它们可能会对性能产生影响。

动画

通常情况下,对某个属性进行动画处理会导致引用该属性的所有绑定被重新评估。虽然这通常是预期行为,但在某些情况下,最好在执行动画之前先禁用该绑定,待动画完成后再重新设置绑定。

请避免在动画过程中运行 JavaScript。例如,应避免在 x 属性动画的每一帧中运行复杂的 JavaScript 表达式。

开发人员在使用脚本动画时应格外谨慎,因为这些动画是在主线程中运行的(因此,如果执行时间过长,可能会导致帧被跳过)。

粒子

该 Qt Quick Particles 模块允许将精美的粒子特效无缝集成到用户界面中。然而,每个平台的图形硬件性能各不相同,而“粒子”模块无法将参数限制在您的硬件能够流畅支持的范围内。 您尝试渲染的粒子越多(且粒子越大),图形硬件就需要越快,才能以 60 FPS 的帧率进行渲染。 处理更多粒子则需要更快的 CPU。因此,务必在目标平台上仔细测试所有粒子特效,以确定在 60 FPS 下可渲染的粒子数量和大小。

需要注意的是,当粒子系统未被使用时(例如,在不可见的元素上),可以将其禁用,以避免进行不必要的模拟。

有关更深入的信息,请参阅《粒子系统性能指南》。

控制元素生命周期

通过将应用程序划分为简单的模块化组件(每个组件都包含在一个单独的 QML 文件中),您可以缩短应用程序的启动时间,更好地控制内存使用情况,并减少应用程序中处于活动状态但不可见的元素数量。

延迟初始化

QML 引擎会采取一些巧妙的措施,以确保组件的加载和初始化不会导致帧被跳过。然而,要缩短启动时间,最有效的方法莫过于避免执行不必要的工作,并将工作推迟到真正需要的时候再进行。这可以通过使用 `Loader` 来实现。

使用 Loader

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

  • 通过使用 Loader 的“active”属性,可以将初始化推迟到需要时再进行。
  • 通过使用“setSource()”函数的重载版本,可以提供初始属性值。
  • 将 Loader 的asynchronous 属性设置为 true,还可以在组件实例化期间提高流畅度。

销毁未使用元素

那些因作为不可见元素的子元素而不可见的元素 (例如,在标签控件中,当第一个标签页显示时,第二个标签页)在大多数情况下应采用延迟初始化,并在不再使用时将其删除,以避免因保持其活动状态而产生的持续开销(例如,渲染、动画、属性绑定评估等)。

通过 Loader 元素加载的项目可通过重置 Loader 的“source”或“sourceComponent”属性来释放,而其他项目则可通过调用其 destroy() 方法显式释放。在某些情况下,可能需要保持项目处于活动状态,此时至少应将其设为不可见。

有关处于活动状态但不可见的元素的更多信息,请参阅后续关于渲染的部分。

渲染

用于渲染的场景图 Qt Quick 中用于渲染的场景图,能够以每秒60帧(FPS)的流畅速度渲染高度动态的动画用户界面。然而,某些因素会大幅降低渲染性能,开发者应格外谨慎,尽可能避免这些陷阱。

裁剪

剪裁功能默认处于禁用状态,仅应在必要时启用。

裁剪是一种视觉效果,而非优化手段。它会增加(而非减少)渲染器的复杂度。 如果启用了裁剪,则某个对象会将其自身的绘制以及其子节点的绘制裁剪至其边界矩形内。这会阻止渲染器自由地重新排列元素的绘制顺序,导致场景图的遍历效率在最佳情况下也无法达到最优。

在委托(delegate)内部进行裁剪尤其有害,应不惜一切代价加以避免。

过度绘制与不可见元素

如果某些元素被其他(不透明)元素完全覆盖,最好将其“visible”属性设置为false ,否则这些元素会被不必要地渲染出来。

同样,对于那些不可见(例如,在标签控件中显示第一个标签页时,第二个标签页处于隐藏状态),但需要在启动时进行初始化的元素(例如, 如果实例化第二个标签页的开销过大,以至于无法仅在标签页被激活时才进行实例化),应将其“visible”属性设置为false ,以避免渲染它们所产生的开销(尽管如前所述,由于它们仍然处于活动状态,因此仍会产生动画或绑定评估的开销)。

半透明与不透明

不透明内容的渲染通常比半透明内容快得多。原因在于半透明内容需要混合处理,而渲染器对不透明内容的优化效果通常更好。

即使一张图像大部分区域是不透明的,只要其中包含一个半透明像素,就会被视为完全半透明。对于具有透明边缘的BorderImage ,情况也是如此。

着色器

ShaderEffect 类型使得在Qt Quick 应用程序中以极低的开销内联GLSL代码成为可能。但需要注意的是,片段程序需要针对渲染形状中的每个像素运行。 当部署到低端硬件且着色器覆盖大量像素时,应将片段着色器控制在几条指令之内,以避免性能下降。

使用 GLSL 编写的着色器可以实现复杂的变换和视觉效果,但应谨慎使用。使用ShaderEffectSource 会导致场景在绘制之前先被预渲染到 FBO 中。这种额外的开销可能相当高。

内存分配与回收

应用程序将分配的内存量及其分配方式是需要重点考虑的因素。 除了在内存受限设备上可能出现的内存不足问题外,在堆上分配内存是一项计算开销相当大的操作,而且某些分配策略可能会导致跨页面的数据碎片化加剧。 JavaScript 使用的是会自动进行垃圾回收的托管内存堆,这虽然具有一些优势,但也带来了一些重要的影响。

用 QML 编写的应用程序同时使用 C++ 堆和自动管理的 JavaScript 堆中的内存。应用程序开发人员需要了解两者的细微差别,以最大限度地提高性能。

QML 应用程序开发者的提示

本节中的提示和建议仅供参考,可能并不适用于所有情况。请务必使用实证指标对应用程序进行仔细的基准测试和分析,以便做出最佳决策。

延迟实例化和初始化组件

如果您的应用程序由多个视图组成(例如,多个标签页),但任何时候都只需其中一个,您可以使用延迟实例化来最大限度地减少在任何给定时刻需要分配的内存量。有关更多信息,请参阅前一节“延迟初始化”。

销毁未使用的对象

如果您采用懒加载组件,或在 JavaScript 表达式中动态创建对象,通常最好手动调用 `destroy() ` 方法,而不是等待自动垃圾回收来处理。有关更多信息,请参阅前一节“控制元素的生命周期”。

不要手动调用垃圾回收器

在大多数情况下,手动调用垃圾回收器并非明智之举,因为这会阻塞 GUI 线程相当长的一段时间。这可能会导致帧跳过和动画卡顿,应不惜一切代价加以避免。

在某些情况下,手动调用垃圾回收器是可以接受的(下文将对此进行更详细的说明),但在大多数情况下,调用垃圾回收器既没有必要,反而会适得其反。

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

如果某个 QML 元素在 QML 中定义了自定义属性,它就会成为一种独立的隐式类型。后续章节将对此进行更详细的说明。如果在Component 中定义了多个相同的隐式类型,将会造成内存浪费。 在这种情况下,通常最好显式定义一个新的组件以便重复使用。此时,建议考虑使用component 关键字来定义一个内联组件。

定义自定义属性通常有助于性能优化(例如,减少所需或需重新评估的绑定数量),或者可以提高组件的模块化和可维护性。在这些情况下,建议使用自定义属性。 但是,如果新类型被多次使用,应将其拆分为独立的组件(内联组件或 .qml 文件),以节省内存。

复用现有组件

如果您正在考虑定义一个新组件,请务必仔细确认该组件是否已在您所用平台的组件集 中存在。否则,您将迫使 QML 引擎为一种类型生成并存储类型数据,而该类型实质上是另一个已存在且可能已被加载的组件的重复。

使用单例类型代替 pragma 库脚本

如果您正在使用 pragma 库脚本来存储全局实例数据,请考虑改用QObject 单例类型。这将带来更好的性能,并减少 JavaScript 堆内存的占用。

QML 应用程序中的内存分配

QML 应用程序的内存使用量可分为两部分:原生堆内存和 JavaScript 堆内存。其中一部分内存分配是不可避免的,因为是由 QML 引擎或 JavaScript 引擎自动分配的,而其余部分则取决于应用程序开发者的决策。

本机堆将包含:

  • 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"
    }
}

在上例中,矩形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 堆(而非本机堆)上分配的内存由 JavaScript 引擎管理。引擎会定期回收 JavaScript 堆上所有未被引用的数据。

垃圾回收的影响

垃圾回收既有优点也有缺点。这意味着手动管理对象生命周期的重要性降低了。但这也意味着,JavaScript 引擎可能会在应用程序开发者无法控制的时间点,启动一项可能持续较长时间的操作。 除非应用程序开发人员仔细考虑 JavaScript 堆的使用情况,否则垃圾回收的频率和持续时间可能会对应用程序体验产生负面影响。自 Qt 6.8 起,垃圾回收器采用了增量回收机制,这意味着回收操作时间会更短,但可能导致更多中断。

手动调用垃圾回收器

用 QML 编写的应用程序(极有可能)在某个阶段需要进行垃圾回收。虽然垃圾回收会由 JavaScript 引擎根据其自身的计划自动触发,但有时由应用程序开发者决定何时手动调用垃圾回收器会更好(尽管通常并非如此)。

应用程序开发者通常最清楚应用程序何时会处于较长时间的空闲状态。 如果某个 QML 应用程序占用了大量 JavaScript 堆内存,导致在特别注重性能的任务(例如列表滚动、动画等)执行期间频繁出现干扰性的垃圾回收周期,那么应用程序开发人员最好在无活动期间手动调用垃圾回收器。 空闲期是执行垃圾回收的理想时机,因为用户不会注意到因在活动期间调用垃圾回收器而导致的用户体验下降(如跳帧、动画卡顿等)。

可以通过在 JavaScript 中调用 `gc() ` 来手动触发垃圾回收器。这将执行一次完整的、非增量式的回收周期,完成时间可能从几百毫秒到上千毫秒不等,因此应尽可能避免。

内存与性能的权衡

在某些情况下,可以通过增加内存占用来换取更短的处理时间。例如,将紧循环中使用的符号查找结果缓存到 JavaScript 表达式中的临时变量中,会在评估该表达式时带来显著的性能提升,但这涉及临时变量的分配。 在某些情况下,这种权衡是合理的(例如上述情况,几乎总是合理的),但在其他情况下,为了避免增加系统的内存压力,让处理时间稍长一些可能更为妥当。

在某些情况下,内存压力增加的影响可能非常严重。在某些情境下,为了获得预期的性能提升而牺牲内存使用,可能会导致页面翻转或缓存翻转加剧,从而造成性能的大幅下降。必须始终仔细测试这些权衡取舍的影响,以确定在特定情况下哪种解决方案最为合适。

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

快速启动与启动优化

基于为Qt Quick 应用程序进行快速启动优化的实际经验,请考虑以下最佳实践:

  • 从一开始就设计应用程序,使其能够快速启动。思考您希望用户首先看到的内容。
  • 使用 QML Profiler 来识别启动过程中的瓶颈。
  • 使用链式加载。仅运行与 CPU 核心数相等的loaders 实例(例如:两核 CPU 时,同时运行两个加载器)。
  • 第一个loader 不应采用异步加载,以便立即显示部分内容。随后再触发异步加载器。
  • 仅在必要时连接后端服务。
  • 创建可在需要时导入的QML 模块。通过使用延迟加载的模块和类型,您可以根据应用需求动态提供非关键服务。
  • 使用 optipng 等工具优化 PNG/JPG 图像。
  • 通过减少顶点数量并移除不可见部分来优化您的 3D 模型。
  • 使用 glTF 优化 3D 模型的加载。
  • 限制使用剪切(clip)和不透明度(opacity)功能,因为这些可能会影响性能。
  • 测量 GPU 限制,并在设计 UI 时将其纳入考量。更多信息请参阅Frame Captures and Performance Profiling 。
  • 使用 Qt Quick Compiler 预编译 QML 文件。
  • 调查您的架构是否支持静态链接。
  • 尽量使用声明式绑定,而不是命令式信号处理程序。
  • 保持属性绑定简单。总体而言,应让 QML 代码保持简单、有趣且易于阅读。这样自然能获得良好的性能。
  • 如果创建时间是个问题,请用图像或着色器替换复杂的控件。

切勿:

  • 过度依赖 QML。即使使用 QML,也无需将所有内容都放在 QML 中实现。
  • 在 main.cpp 中初始化所有内容。
  • 创建包含所有必需接口的大型单例。
  • 为ListView 或其他视图创建复杂的委托。
  • 除非绝对必要,否则不要使用 clip。
  • 切勿陷入过度使用加载器的常见陷阱。Loader 非常适合对应用程序页面等较大内容进行延迟加载,但对于加载简单内容会引入过多开销。它并非能让一切都变快的“黑魔法”,而只是一个带有额外 QML 上下文的组件。

这些做法有助于实现亚秒级的启动速度和流畅的用户体验,特别是在嵌入式设备上。

© 2026 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.