当前位置:   article > 正文

Qt 5.12学习笔记--QML性能策略_qtqml有必要吗

qtqml有必要吗

1 简介

QML做UI设计,极大提高了效率,并且能把界面渲染效果做的特别棒,实际开发中需要注意性能问题,虽然大部分业务逻辑用C++实现,但是界面逻辑复杂时,性能需要着重考虑,尤其在硬件算力一般的情况下更需要注意。

2 时间因素

让渲染引擎实现一致的 60 帧每秒刷新率,意味着每个帧之间可以进行大约 16 毫秒的处理,其中包括将绘图图元上传到图形硬件所需的处理。这样才能避免跳帧。

  • 尽可能使用异步,事件驱动的编程
  • 使用工作者线程来处理重要的事情,比如说QML的WorkerScript类型就是起用了一个新的线程。
  • 永远不要手动调整事件循环
  • 在阻塞功能中,每帧不要花费超过几毫秒
    注意:QML与C++交互时,为了避免阻塞就去创建自己的QEventLoop或调用QCoreApplication::processEvents(),这虽说是一种常见的模式,但也是危险的,因为信号处理或绑定进入事件循环时,QML引擎会继续运行其它的绑定、动画、状态迁移等,这些动作就可能带来副作用,例如,破坏包含了事件循环的层级结构。

2 性能分析

QML 分析器工具能辅助调试,知道应用程序花费的时间,从而确定问题范围,便于问题解决。
http://doc.qt.io/qtcreator/creator-qml-performance-monitor.html

3 JavaScript 部分

大多数 QML 应用程序将以动态函数,信号处理程序和属性绑定表达式的形式使用大量的 JavaScript 代码。这通常没有什么问题。由于 QML 引擎中的一些优化,例如对绑定编译器的优化,使用 JavaScript 代码甚至可以(在某些用例中)比调用 C++ 函数更快。但是,必须注意确保不必要的处理不会被意外触发。

4 绑定

QML中有两种类型的绑定:优化绑定和非优化绑定。保持绑定表达式尽可能简单是一个好主意,因为QML引擎使用优化的绑定表达式求值程序,它可以评估简单的绑定表达式,而无需切换到完整的JavaScript执行环境。与更复杂(非优化)的绑定相比,这些优化的绑定的评估效率更高。优化绑定的基本要求是在编译时必须知道所访问的每个符号的类型信息。绑定表达式时要避免的事情,以达到最大的优化:

  • 声明中间 JavaScript 变量
  • 访问 “var” 属性
  • 调用 JavaScript 函数
  • 在绑定表达式中构造闭包或定义函数
  • 访问直接求值域之外的属性
  • 将其写入其他属性,作为副作用

在运行QML应用程序时,可能要设置QML_COMPILER_STATS环境变量以打印与绑定相关的数据,当知道对象和属性的类型时,绑定速度是最快的,也就是说在绑定表达式查值过程中某些非最终状态的属性的类型可能会有变化,这样绑定速度就变慢了。即时求值范围包括以下内容:

  • 表达式作用域对象的属性(对于绑定表达式,这是属性绑定所属的对象)
  • 组件中任何对象的 ID
  • 组件中根项目的属性
    其它组件的对象id、属性,还有通过import导入的东西,是不在即时求值范围的,在这种情况下,绑定是不被优化的。需要注意的是,如果绑定没有被QML引擎优化,就会在纯JavaScript环境中求值,这时上面的几点建议就不再适用了。有时候在非常复杂的绑定中,缓存JavaScript中间变量的属性求值结果是有益的,这个在后面的内容中会有描述。

5 类型转换

使用 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 引用(这可以减少某些表达式中需要的转换次数)。

6 解析属性

属性解析需要消耗时间。在某些情况下,如果可能的话我们可以将查找的结果缓存和重用,以避免做不必要的工作。在下面的例子中,我们有一个经常运行的代码块(在这种情况下,它是一个显式循环的内容;但是它可以是一个经常评估的绑定表达式),在这个例子中,我们解析了对象“rect”id及其“color”属性多次:
下面示例,请看for循环处的变化

// bad.qml
import QtQuick 2.3

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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
// good.qml
import QtQuick 2.3

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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
// better.qml
import QtQuick 2.3

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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

6 属性绑定

如果其引用的任何属性发生更改,则属性绑定表达式将被重新评估。因此,绑定表达式应尽可能简单。
例如我们有一个循环,我们要做一些处理,但是只有处理的最终结果是我们需要的,通常更好的处理方式是添加一个临时累加器,然后对这个累加器进行处理,而不是逐步更新属性本身,以避免在累加的过程中触发绑定这个属性的地方重新运算。

/ bad.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 ];
        for (var i = 0; i < someData.length; ++i) {
            accumulatedValue = accumulatedValue + someData[i];
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
// 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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

7 序列提示(Sequence tips)

如前所述,一些序列类型很快(例如 QList, QList, QList, QList, QStringList 和 QList),而有些则慢一些。除了使用较快的类型之外,还有其他一些与性能相关的语义,您需要了解这些语义,以达到最佳的性能。
首先,序列类型有两种不同的实现方式:一个是序列是 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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

以下示例在嵌套循环中写入参考序列的元素,导致性能不佳:

// bad.qml
import QtQuick 2.3
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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

由 “qrealListProperty [j] = j” 表达式引起的内部循环中的 QObject 属性读取和写入使得该代码非常不理想。相反,一些功能上相同但速度更快的做法是:

// good.qml
import QtQuick 2.3
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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

其次,如果其中的任何元素发生变化,则会发出该属性的更改信号。如果对序列属性中的特定元素有很多绑定,最好创建绑定到该元素的动态属性,并将该动态属性用作绑定表达式中的符号而不是序列元素,因为它将只有当其值发生变化时才会重新评估绑定。

// bad.qml
import QtQuick 2.3
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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

请注意,尽管在循环中仅修改索引 2 中的元素,但是由于更改信号的粒度是整个属性已更改,所以三个绑定将全部重新计算。因此,添加中间绑定有时是有益的:

// good.qml
import QtQuick 2.3
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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在上面的示例中,每次只针对中间绑定的值的变化进行重新评估,从而可以显著地提升性能。

8 值类型的建议

值类型属性(font, color, vector3d 等)具有类似的 QObject 属性,并将通知语义更改为序列类型属性。因此,上面针对序列给出的提示也适用于值类型属性。虽然它们对于值类型的问题通常不那么严重(因为值类型的子属性的数量通常比序列中的元素数量少得多),但是重新评估的绑定数量的增加将会对性能产生负面影响。

9 其他 JavaScript 对象

不同的 JavaScript 引擎提供不同的优化。Qt Quick 2 使用的 JavaScript 引擎针对对象实例化和属性查找进行了优化,但它提供的优化依赖于某些标准。如果您的应用程序不符合标准,则 JavaScript 引擎将恢复到“慢路”模式,性能更差。 因此,始终尽量确保符合以下条件:

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

10 通用接口元素

10.1 文本元素

计算文本布局可能是一个缓慢的操作。考虑使用PlainText格式,而不是StyledText尽可能使用格式,因为这会减少布局引擎所需的工作量。如果您不能使用PlainText(因为您需要嵌入图像,或者使用标签来指定字符范围以具有与整个文本相对的某些格式(粗体,斜体等)),则应该使用StyledText。
您仅应AutoText在文本可能是(但可能不是)的情况下使用,StyledText因为此模式将产生解析消耗。RichText不应使用该模式,因为该模式StyledText几乎提供了所有功能,而其成本却很少。

10.2 图片

图像是任何用户界面的重要组成部分。不幸的是,由于它们的加载时间,消耗的内存量以及使用方式,它们也是造成问题的主要原因。

  • 异步加载
    图片通常很大,因此明智的做法是确保加载图片不会阻塞UI线程。设置QML Image元素的**“ a​​synchronous”属性true**以启用从本地文件系统异步加载图像(远程图像始终异步加载),这样不会导致界面卡顿。
    设置为“异步”属性的图像元素true将在低优先级工作线程中加载图像。
  • 显式源大小
    如果应用程序加载大图像但是将其显示在一个小尺寸元素中,可以将**“sourceSize”属性设置为要渲染的元素的大小**,以确保图像的较小比例版本保留在内存中,而不是原图片尺寸大小,这样可以节省内存消耗。
    请注意,更改sourceSize将导致重新加载图像。
  • 避免运行时合成
    还要记住,通过为应用程序提供预先合成的图像资源(例如,为元素提供阴影效果),可以避免在运行时进行合成工作。
  • 避免使图像平滑
    image.smooth仅在需要时启用。在某些硬件上速度较慢,如果以自然尺寸显示图像,则不会产生视觉效果。
  • 绘画
    避免多次绘制同一区域。使用Item作为根元素而不是Rectangle可以避免多次绘制背景。

10.3 使用锚点定位元素

在元素布局的时候尽量使用锚布局的方式,不要使用直接绑定固定位置,如设置 x 和 y 值。

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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以替代成如下方式:

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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

使用绑定位置的方式(通过将绑定表达式分配给可视对象的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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

11 模型和视图

大多数应用程序将至少具有一个向视图提供数据的模型。为了达到最佳性能,应用程序开发人员需要了解一些语义。

11.1自定义 C++ 模型

通常希望用C ++编写自己的自定义模型以用于QML中的视图。尽管任何此类模型的最佳实现都将在很大程度上取决于它必须满足的用例,但一些通用准则如下:

  • 尽可能异步
  • 在(低优先级)工作线程中执行所有处理
  • 批处理后端操作,以使(可能很慢)I / O和IPC最小化
  • 使用滑动切片窗口来缓存结果,其结果是通过分析确定的
    重要的是要注意,建议使用低优先级的工作线程,以最大程度地减少使GUI线程饿死的风险(这可能会导致性能下降)。另外,请记住,同步和锁定机制可能是导致性能降低的重要原因,因此应注意避免不必要的锁定。

11.2 QML 的 ListModel 类型

QML提供了一个ListModel类型,可用于将数据提供给ListView。只要正确使用,它就足以满足大多数用例并具有相对较高的性能。

  • 填充在工作线程中
    可以在JavaScript中的(低优先级)工作线程中填充ListModel元素。开发人员必须在WorkerScript中显式调用ListModel上的“ sync()”,以使更改同步到主线程。有关更多信息,请参见WorkerScript文档。
    请注意,使用WorkerScript元素将导致创建单独的JavaScript引擎(因为JavaScript引擎是基于线程的)。这将导致内存使用量增加。但是,多个WorkerScript元素都将使用同一个工作线程,因此,一旦应用程序已经使用第二个或第三个WorkerScript元素,则对内存的影响可以忽略不计。
  • 不要使用动态角色
    QtQuick 2中的ListModel元素比QtQuick 1中的性能要好得多。性能提高主要来自对给定模型中每个元素内角色类型的假设-如果类型不变,则缓存性能将大大提高。如果类型可以在元素之间动态变化,则此优化将变得不可能,并且模型的性能将降低一个数量级。
    因此,默认情况下将禁用动态类型输入。开发人员必须专门设置模型的boolean“ dynamicRoles”属性,以启用动态类型输入(并因此而导致性能下降)。如果可以重新设计应用程序以避免使用动态类型,建议不要使用动态类型。

11.3 观看次数

视图代表应保持尽可能简单。委托中只有足够的QML以显示必要的信息。不需要立即创建的任何其他立即不需要的功能(例如,如果单击时显示更多信息)(请参阅后面的延迟初始化部分)。
以下列表很好地总结了设计委托时要记住的事项:

  • 委托中的元素越少,创建它们的速度就越快,因此视图滚动的速度也就越快。
  • 尽量减少委托中的绑定数;特别是,在代理中使用锚而不是绑定进行相对定位。
  • 避免在委托中使用ShaderEffect元素。
  • 切勿在代理上启用剪辑。

您可以设置cacheBuffer视图的属性以允许异步创建和缓冲可见区域之外的委托。cacheBuffer对于非平凡且不太可能在单个框架中创建的视图委托,建议使用a 。
请记住,a cacheBuffer会将其他代表保留在内存中。因此,cacheBuffer必须将通过利用导出的值与额外的内存使用进行平衡。开发人员应使用基准测试来找到其用例的最佳价值,因为cacheBuffer在某些情况下,由于使用罐子而导致的内存压力增加,导致滚动时帧速率降低。

12 视觉效果

Qt Quick 2包括一些功能,这些功能使开发人员和设计人员可以创建非常吸引人的用户界面。流动性和动态过渡以及视觉效果可以在应用程序中发挥很大的作用,但是在使用QML中的某些功能时必须格外小心,因为它们可能会影响性能。

12.1 动画制作

通常,对属性设置动画将导致重新引用引用该属性的所有绑定。通常,这是所需要的,但是在其他情况下,最好在执行动画之前禁用绑定,然后在动画完成后重新分配绑定。
避免在动画期间运行JavaScript。例如,应避免为x属性动画的每一帧运行复杂的JavaScript表达式。
开发人员在使用脚本动画时应格外小心,因为它们是在主线程中运行的(因此,如果它们花费太长时间才能完成,则可能导致帧被跳过)。

12.2 粒子

在Qt的快速粒子模块允许美丽粒子效果被无缝地集成到用户界面。但是,每个平台都有不同的图形硬件功能,Particles模块无法将参数限制为硬件可以正常支持的范围。您尝试渲染的粒子越多(它们越大),以60 FPS进行渲染所需的图形硬件就会越快。影响更多的粒子需要更快的CPU。因此,重要的是仔细测试目标平台上的所有粒子效果,以校准可以以60 FPS渲染的粒子的数量和大小。
应该注意的是,在不使用粒子系统时(例如,在不可见元素上)可以禁用粒子系统,以避免进行不必要的模拟。
有关更多详细信息,请参见《粒子系统性能指南》。
http://doc.qt.io/qt-5/qtquick-particles-performance.html

13 控制元素寿命

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

13.1 延迟初始化

QML引擎做了一些棘手的事情,试图确保组件的加载和初始化不会导致帧被跳过。但是,没有比减少避免执行不需要的工作以及将工作延迟到必要时更好的方法来减少启动时间。这可以通过使用Loader或动态创建组件来实现。

13.1.1 使用加载程序

加载程序是允许动态加载和卸载组件的元素。

  • 使用装载程序的“活动”属性,可以将初始化延迟到需要时。
  • 使用“ setSource()”函数的重载版本,可以提供初始属性值。
  • 将Loader 异步属性设置为true还可在实例化组件时提高流动性。

13.1.2 使用动态创建

开发人员可以使用Qt.createComponent()函数在运行时从JavaScript内部动态创建组件,然后调用createObject()实例化它。根据调用中指定的所有权语义,开发人员可能必须手动删除创建的对象。有关更多信息,请参见从JavaScript动态创建QML对象。
http://doc.qt.io/qt-5/qtqml-javascript-dynamicobjectcreation.html

13.2 销毁未使用的元素

有些不可见元素,如Tab插件的第一个插件显示时后面的就不可见,因为它们是不可见元素的孩子,所以在大多数情况下要延迟实例化,而且在不用的时候还要销毁,以避免不必要的开销,如渲染、动画、绑定等。使用Loader加载元素时,可以重置source或sourceComponnet属性为空来释放这个元素,其它的元素可调用destroy()来销毁。在某些情况下,这些元素还是要保留激活状态,但在最后要设置为不可见。下面的渲染部分也会谈到这一点。

14 渲染图

QtQuick 2中用于渲染的场景图允许以60 FPS流畅地渲染高度动态的动画用户界面。有些事情会大大降低渲染性能,但是,开发人员应小心避免这些陷阱。

14.1 剪裁

默认情况下,裁剪是禁用的,并且仅在需要时才启用。
裁剪是一种视觉效果,而不是优化。它增加(而不是减少)渲染器的复杂性。如果启用了剪切,则一个项目会将其自己的画以及其子对象的画剪切到其边界矩形。这使渲染器无法自由地对元素的绘制顺序进行重新排序,从而导致次佳的最佳情况场景图遍历。
委托内部的剪辑特别糟糕,应不惜一切代价避免。

14.2 透支和看不见的元素

如果您的元素完全被其他(不透明)元素覆盖,则最好将其“可见”属性设置为false,否则将不必要地绘制它们。
同样,不可见的元素(例如,标签小部件中的第二个标签,而第一个标签则显示),但是需要在启动时进行初始化(例如,实例化第二个标签的成本过长而无法只能在激活选项卡时才能执行此操作),应将其“可见”属性设置为false,以避免绘制它们的成本(尽管如前所述,它们仍将招致任何动画或绑定评估的成本,因为他们仍然活跃)。

14.3 半透明vs不透明

不透明的内容通常比半透明的绘制要快得多。原因是半透明内容需要混合,渲染器可以更好地优化不透明内容。
具有一个半透明像素的图像被视为完全半透明,即使它大部分是不透明的。具有透明边缘的BorderImage也是如此。

14.4 着色器

使用ShaderEffect类型,可以以很少的开销将GLSL代码内联放置在Qt Quick应用程序中。但是,重要的是要认识到片段程序需要为渲染形状中的每个像素运行。当部署到低端硬件并且着色器覆盖大量像素时,应该将片段着色器遵循一些指令,以免性能变差。
用GLSL编写的着色器允许编写复杂的转换和视觉效果,但是应谨慎使用。使用ShaderEffectSource会使场景在绘制之前先渲染到FBO中。这种额外的开销可能会非常昂贵。

15 内存分配和收集

应用程序将分配的内存量以及分配内存的方式是非常重要的考虑因素。除了明显担心内存受限的设备上的内存不足情况外,在堆上分配内存是一项计算量很大的操作,某些分配策略可能会导致跨页的数据碎片增多。JavaScript使用托管内存堆,该内存堆会自动进行垃圾回收,这具有一些优点,但也有一些重要的含义。
用QML编写的应用程序使用C ++堆和自动管理的JavaScript堆中的内存。为了最大化性能,应用程序开发人员需要意识到每个细节。

15.1 给QML应用程序开发人员的提示

本节中包含的提示和建议仅是指导原则,不一定适用于所有情况。请确保使用经验指标仔细对应用程序进行基准测试和分析,以便做出最佳决策。

  • 延迟实例化和初始化组件
    如果您的应用程序包含多个视图(例如,多个选项卡),但在任何一次仅需要一个视图,则可以使用延迟实例化来最小化在任何给定时间需要分配的内存量。有关更多信息,请参见上一节中的“ 延迟初始化 ”。
  • 销毁未使用的对象
    如果您延迟加载组件,或者在JavaScript表达式中动态创建对象,则通常最好destroy()手动进行操作,而不是等待自动垃圾收集这样做。有关更多信息,请参见前面的“ 控制元素寿命”一节。
  • 不要手动调用垃圾收集器
    在大多数情况下,手动调用垃圾回收器是不明智的,因为它将在相当长的一段时间内阻塞GUI线程。这可能会导致跳帧和抖动的动画,应不惜一切代价避免。
    在某些情况下,可以接受手动调用垃圾收集器(这将在后面的部分中进行详细说明),但是在大多数情况下,调用垃圾收集器是不必要的且会适得其反。
  • 避免复杂的绑定
    除了复杂绑定的性能降低(例如,由于必须进入JavaScript执行上下文以执行评估)以外,与QML优化后可以评估的绑定相比,它们在C ++堆和JavaScript堆上还占用更多的内存。绑定表达评估者。
  • 避免定义多个相同的隐式类型
    如果QML元素具有在QML中定义的自定义属性,则它将成为其自己的隐式类型。在后面的部分中将对此进行详细说明。如果在组件中内联定义了多个相同的隐式类型,则会浪费一些内存。在那种情况下,通常最好显式定义一个新组件,然后再使用它。
    定义定制属性通常可以是有益的性能优化(例如,减少所需或重新评估的绑定数),或者可以提高组件的模块化和可维护性。在这种情况下,鼓励使用自定义属性。但是,如果多次使用新类型,则应将其拆分为自己的组件(.qml文件)以节省内存。
  • 重用现有组件
    如果您正在考虑定义一个新组件,则需要仔细检查一下该组件在您平台的组件集中是否还不存在。否则,您将强制QML引擎生成并存储该类型的类型数据,该类型数据实质上是另一个预先存在且可能已经加载的组件的副本。
  • 使用单例类型而不是杂注库脚本
    如果您使用实用程序库脚本来存储应用程序范围的实例数据,请考虑改用QObject单例类型。这将导致更好的性能,并且将减少使用的JavaScript堆内存。

15.2 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 2.3  
  
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"  
    }  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

在前面的示例中,矩形r0和r1没有任何自定义属性,因此JavaScript和QML引擎将它们都视为相同类型。即,r0并且r1都被认为是显式定义的Rectangle类型。矩形和都有自定义属性r2,r3并且r4每个矩形均被视为不同(隐式)类型。请注意,r3并r4分别被认为是不同类型的,即使他们有相同的属性信息,仅仅是因为自定义属性并没有在他们的情况下,该组件声明。
如果r3和r4都是RectangleWithString组件的实例,并且该组件定义包括一个名为的字符串属性的声明customProperty,则r3和r4会被认为是相同类型的(也就是说,它们将是该RectangleWithString类型的实例,而不是定义自己的实例隐式类型)。

15.3 深度内存分配注意事项

无论何时做出有关内存分配或性能折衷的决策,务必牢记CPU缓存性能,操作系统分页和JavaScript引擎垃圾回收的影响。应对潜在解决方案进行仔细基准测试,以确保选择最佳解决方案。
没有一套通用准则可以取代对计算机科学基本原理的扎实理解,再加上对应用程序开发人员正在为其开发平台的实现细节的实践知识。此外,在做出权衡决策时,没有任何理论计算能够替代一套好的基准和分析工具。

  • 碎片化
    碎片是C ++开发的问题。如果应用程序开发人员未定义任何C ++类型或插件,则他们可以放心地忽略此部分。
    随着时间的流逝,应用程序将分配大部分内存,将数据写入该内存,并在完成使用某些数据后释放部分内存。这可能导致“空闲”内存位于不连续的块中,这些内存无法返回给操作系统供其他应用程序使用。它还会影响应用程序的缓存和访问特性,因为“活动”数据可能会分散在物理内存的许多不同页面上。反过来,这可能会迫使操作系统进行交换,这可能会导致文件系统I / O-相对而言,这是极其缓慢的操作。
    通过使用池分配器(和其他连续的内存分配器),通过仔细管理对象的生命周期,定期清理和重建缓存或利用内存管理的运行时减少在任何时间分配的内存量,可以避免碎片化垃圾收集(例如JavaScript)。
  • 垃圾收集
    JavaScript提供垃圾回收。JavaScript引擎拥有在JavaScript堆(而不是C ++堆)上分配的内存。引擎将定期收集JavaScript堆上所有未引用的数据。
    (1)垃圾收集的含义
    垃圾收集有优点也有缺点。这意味着手动管理对象生存期的重要性降低。但是,这也意味着JavaScript引擎可能会在应用程序开发人员无法控制的时间启动可能持续很长时间的操作。除非应用程序开发人员仔细考虑了JavaScript堆的使用情况,否则垃圾回收的频率和持续时间可能会对应用程序体验产生负面影响。
    (2)手动调用垃圾收集器
    用QML编写的应用程序(很可能)将要求在某个阶段执行垃圾回收。尽管当可用内存不足时,JavaScript引擎会自动触发垃圾回收,但如果应用程序开发人员决定何时手动调用垃圾回收器,则偶尔会更好(尽管通常情况并非如此)。
    应用程序开发人员可能会对应用程序何时长时间处于空闲状态有最好的了解。如果QML应用程序使用大量JavaScript堆内存,从而在对性能特别敏感的任务(例如,列表滚动,动画等)中导致定期的破坏性垃圾回收周期,则应用程序开发人员可以很好地手动调用零活动期间的垃圾收集器。空闲时段是执行垃圾收集的理想选择,因为用户不会注意到在活动进行时调用垃圾收集器会导致用户体验下降(跳帧,生涩的动画等)。
    可以通过gc()在JavaScript中进行调用来手动调用垃圾收集器。这将导致执行一个全面的收集周期,这可能需要数百到一千多毫秒来完成,因此,如果可能的话,应避免这样做。
  • 内存与性能的权衡
    在某些情况下,可以权衡增加的内存使用量以减少处理时间。例如,在紧密循环中使用符号查找的结果缓存到JavaScript表达式中的临时变量时,将大大提高评估该表达式时的性能,但它涉及分配临时变量。在某些情况下,这些折衷是明智的(例如上面的情况,这几乎总是明智的),但在其他情况下,最好允许处理时间稍长一些,以避免增加系统的内存压力。
    在某些情况下,内存压力增加的影响可能是极端的。在某些情况下,为了获得预期的性能提升而折衷使用内存可能会导致页面抖动或缓存抖动增加,从而导致性能大幅下降。为了确定在给定情况下哪种解决方案是最佳的,始终有必要仔细权衡基准的影响。

有关缓存性能和内存时间权衡的深入信息,请参阅以下文章:
乌尔里希·德雷珀(Ulrich Drepper)的精彩文章:“每个程序员应该了解的内存知识”,网址:https : //people.freebsd.org/~lstewart/articles/cpumemory.pdf。
Agner Fog关于优化C ++应用程序的优秀手册,网址为:http

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/117423?site
推荐阅读
相关标签