赞
踩
QML做UI设计,极大提高了效率,并且能把界面渲染效果做的特别棒,实际开发中需要注意性能问题,虽然大部分业务逻辑用C++实现,但是界面逻辑复杂时,性能需要着重考虑,尤其在硬件算力一般的情况下更需要注意。
让渲染引擎实现一致的 60 帧每秒刷新率,意味着每个帧之间可以进行大约 16 毫秒的处理,其中包括将绘图图元上传到图形硬件所需的处理。这样才能避免跳帧。
QML 分析器工具能辅助调试,知道应用程序花费的时间,从而确定问题范围,便于问题解决。
http://doc.qt.io/qtcreator/creator-qml-performance-monitor.html
大多数 QML 应用程序将以动态函数,信号处理程序和属性绑定表达式的形式使用大量的 JavaScript 代码。这通常没有什么问题。由于 QML 引擎中的一些优化,例如对绑定编译器的优化,使用 JavaScript 代码甚至可以(在某些用例中)比调用 C++ 函数更快。但是,必须注意确保不必要的处理不会被意外触发。
QML中有两种类型的绑定:优化绑定和非优化绑定。保持绑定表达式尽可能简单是一个好主意,因为QML引擎使用优化的绑定表达式求值程序,它可以评估简单的绑定表达式,而无需切换到完整的JavaScript执行环境。与更复杂(非优化)的绑定相比,这些优化的绑定的评估效率更高。优化绑定的基本要求是在编译时必须知道所访问的每个符号的类型信息。绑定表达式时要避免的事情,以达到最大的优化:
在运行QML应用程序时,可能要设置QML_COMPILER_STATS环境变量以打印与绑定相关的数据,当知道对象和属性的类型时,绑定速度是最快的,也就是说在绑定表达式查值过程中某些非最终状态的属性的类型可能会有变化,这样绑定速度就变慢了。即时求值范围包括以下内容:
使用 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 引用(这可以减少某些表达式中需要的转换次数)。
属性解析需要消耗时间。在某些情况下,如果可能的话我们可以将查找的结果缓存和重用,以避免做不必要的工作。在下面的例子中,我们有一个经常运行的代码块(在这种情况下,它是一个显式循环的内容;但是它可以是一个经常评估的绑定表达式),在这个例子中,我们解析了对象“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"); } }
// 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"); } }
// 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"); } }
如果其引用的任何属性发生更改,则属性绑定表达式将被重新评估。因此,绑定表达式应尽可能简单。
例如我们有一个循环,我们要做一些处理,但是只有处理的最终结果是我们需要的,通常更好的处理方式是添加一个临时累加器,然后对这个累加器进行处理,而不是逐步更新属性本身,以避免在累加的过程中触发绑定这个属性的地方重新运算。
/ 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]; } } }
// 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; } }
如前所述,一些序列类型很快(例如 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; };
以下示例在嵌套循环中写入参考序列的元素,导致性能不佳:
// 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"); } }
由 “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"); } }
其次,如果其中的任何元素发生变化,则会发出该属性的更改信号。如果对序列属性中的特定元素有很多绑定,最好创建绑定到该元素的动态属性,并将该动态属性用作绑定表达式中的符号而不是序列元素,因为它将只有当其值发生变化时才会重新评估绑定。
// 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"); } }
请注意,尽管在循环中仅修改索引 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"); } }
在上面的示例中,每次只针对中间绑定的值的变化进行重新评估,从而可以显著地提升性能。
值类型属性(font, color, vector3d 等)具有类似的 QObject 属性,并将通知语义更改为序列类型属性。因此,上面针对序列给出的提示也适用于值类型属性。虽然它们对于值类型的问题通常不那么严重(因为值类型的子属性的数量通常比序列中的元素数量少得多),但是重新评估的绑定数量的增加将会对性能产生负面影响。
不同的 JavaScript 引擎提供不同的优化。Qt Quick 2 使用的 JavaScript 引擎针对对象实例化和属性查找进行了优化,但它提供的优化依赖于某些标准。如果您的应用程序不符合标准,则 JavaScript 引擎将恢复到“慢路”模式,性能更差。 因此,始终尽量确保符合以下条件:
计算文本布局可能是一个缓慢的操作。考虑使用PlainText格式,而不是StyledText尽可能使用格式,因为这会减少布局引擎所需的工作量。如果您不能使用PlainText(因为您需要嵌入图像,或者使用标签来指定字符范围以具有与整个文本相对的某些格式(粗体,斜体等)),则应该使用StyledText。
您仅应AutoText在文本可能是(但可能不是)的情况下使用,StyledText因为此模式将产生解析消耗。RichText不应使用该模式,因为该模式StyledText几乎提供了所有功能,而其成本却很少。
图像是任何用户界面的重要组成部分。不幸的是,由于它们的加载时间,消耗的内存量以及使用方式,它们也是造成问题的主要原因。
在元素布局的时候尽量使用锚布局的方式,不要使用直接绑定固定位置,如设置 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
}
可以替代成如下方式:
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中的视图。尽管任何此类模型的最佳实现都将在很大程度上取决于它必须满足的用例,但一些通用准则如下:
QML提供了一个ListModel类型,可用于将数据提供给ListView。只要正确使用,它就足以满足大多数用例并具有相对较高的性能。
视图代表应保持尽可能简单。委托中只有足够的QML以显示必要的信息。不需要立即创建的任何其他立即不需要的功能(例如,如果单击时显示更多信息)(请参阅后面的延迟初始化部分)。
以下列表很好地总结了设计委托时要记住的事项:
您可以设置cacheBuffer视图的属性以允许异步创建和缓冲可见区域之外的委托。cacheBuffer对于非平凡且不太可能在单个框架中创建的视图委托,建议使用a 。
请记住,a cacheBuffer会将其他代表保留在内存中。因此,cacheBuffer必须将通过利用导出的值与额外的内存使用进行平衡。开发人员应使用基准测试来找到其用例的最佳价值,因为cacheBuffer在某些情况下,由于使用罐子而导致的内存压力增加,导致滚动时帧速率降低。
Qt Quick 2包括一些功能,这些功能使开发人员和设计人员可以创建非常吸引人的用户界面。流动性和动态过渡以及视觉效果可以在应用程序中发挥很大的作用,但是在使用QML中的某些功能时必须格外小心,因为它们可能会影响性能。
通常,对属性设置动画将导致重新引用引用该属性的所有绑定。通常,这是所需要的,但是在其他情况下,最好在执行动画之前禁用绑定,然后在动画完成后重新分配绑定。
避免在动画期间运行JavaScript。例如,应避免为x属性动画的每一帧运行复杂的JavaScript表达式。
开发人员在使用脚本动画时应格外小心,因为它们是在主线程中运行的(因此,如果它们花费太长时间才能完成,则可能导致帧被跳过)。
在Qt的快速粒子模块允许美丽粒子效果被无缝地集成到用户界面。但是,每个平台都有不同的图形硬件功能,Particles模块无法将参数限制为硬件可以正常支持的范围。您尝试渲染的粒子越多(它们越大),以60 FPS进行渲染所需的图形硬件就会越快。影响更多的粒子需要更快的CPU。因此,重要的是仔细测试目标平台上的所有粒子效果,以校准可以以60 FPS渲染的粒子的数量和大小。
应该注意的是,在不使用粒子系统时(例如,在不可见元素上)可以禁用粒子系统,以避免进行不必要的模拟。
有关更多详细信息,请参见《粒子系统性能指南》。
http://doc.qt.io/qt-5/qtquick-particles-performance.html
通过将应用程序划分为简单的模块化组件(每个组件包含在一个QML文件中),您可以缩短应用程序的启动时间,更好地控制内存使用,并减少应用程序中活动但不可见的元素的数量。
QML引擎做了一些棘手的事情,试图确保组件的加载和初始化不会导致帧被跳过。但是,没有比减少避免执行不需要的工作以及将工作延迟到必要时更好的方法来减少启动时间。这可以通过使用Loader或动态创建组件来实现。
加载程序是允许动态加载和卸载组件的元素。
开发人员可以使用Qt.createComponent()函数在运行时从JavaScript内部动态创建组件,然后调用createObject()实例化它。根据调用中指定的所有权语义,开发人员可能必须手动删除创建的对象。有关更多信息,请参见从JavaScript动态创建QML对象。
http://doc.qt.io/qt-5/qtqml-javascript-dynamicobjectcreation.html
有些不可见元素,如Tab插件的第一个插件显示时后面的就不可见,因为它们是不可见元素的孩子,所以在大多数情况下要延迟实例化,而且在不用的时候还要销毁,以避免不必要的开销,如渲染、动画、绑定等。使用Loader加载元素时,可以重置source或sourceComponnet属性为空来释放这个元素,其它的元素可调用destroy()来销毁。在某些情况下,这些元素还是要保留激活状态,但在最后要设置为不可见。下面的渲染部分也会谈到这一点。
QtQuick 2中用于渲染的场景图允许以60 FPS流畅地渲染高度动态的动画用户界面。有些事情会大大降低渲染性能,但是,开发人员应小心避免这些陷阱。
默认情况下,裁剪是禁用的,并且仅在需要时才启用。
裁剪是一种视觉效果,而不是优化。它增加(而不是减少)渲染器的复杂性。如果启用了剪切,则一个项目会将其自己的画以及其子对象的画剪切到其边界矩形。这使渲染器无法自由地对元素的绘制顺序进行重新排序,从而导致次佳的最佳情况场景图遍历。
委托内部的剪辑特别糟糕,应不惜一切代价避免。
如果您的元素完全被其他(不透明)元素覆盖,则最好将其“可见”属性设置为false,否则将不必要地绘制它们。
同样,不可见的元素(例如,标签小部件中的第二个标签,而第一个标签则显示),但是需要在启动时进行初始化(例如,实例化第二个标签的成本过长而无法只能在激活选项卡时才能执行此操作),应将其“可见”属性设置为false,以避免绘制它们的成本(尽管如前所述,它们仍将招致任何动画或绑定评估的成本,因为他们仍然活跃)。
不透明的内容通常比半透明的绘制要快得多。原因是半透明内容需要混合,渲染器可以更好地优化不透明内容。
具有一个半透明像素的图像被视为完全半透明,即使它大部分是不透明的。具有透明边缘的BorderImage也是如此。
使用ShaderEffect类型,可以以很少的开销将GLSL代码内联放置在Qt Quick应用程序中。但是,重要的是要认识到片段程序需要为渲染形状中的每个像素运行。当部署到低端硬件并且着色器覆盖大量像素时,应该将片段着色器遵循一些指令,以免性能变差。
用GLSL编写的着色器允许编写复杂的转换和视觉效果,但是应谨慎使用。使用ShaderEffectSource会使场景在绘制之前先渲染到FBO中。这种额外的开销可能会非常昂贵。
应用程序将分配的内存量以及分配内存的方式是非常重要的考虑因素。除了明显担心内存受限的设备上的内存不足情况外,在堆上分配内存是一项计算量很大的操作,某些分配策略可能会导致跨页的数据碎片增多。JavaScript使用托管内存堆,该内存堆会自动进行垃圾回收,这具有一些优点,但也有一些重要的含义。
用QML编写的应用程序使用C ++堆和自动管理的JavaScript堆中的内存。为了最大化性能,应用程序开发人员需要意识到每个细节。
本节中包含的提示和建议仅是指导原则,不一定适用于所有情况。请确保使用经验指标仔细对应用程序进行基准测试和分析,以便做出最佳决策。
QML应用程序的内存使用情况可以分为两部分:C ++堆使用情况和JavaScript堆使用情况。每个内存中分配的某些内存将不可避免,因为它是由QML引擎或JavaScript引擎分配的,而其余的则取决于应用程序开发人员的决定。
C ++堆将包含:
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" } }
在前面的示例中,矩形r0和r1没有任何自定义属性,因此JavaScript和QML引擎将它们都视为相同类型。即,r0并且r1都被认为是显式定义的Rectangle类型。矩形和都有自定义属性r2,r3并且r4每个矩形均被视为不同(隐式)类型。请注意,r3并r4分别被认为是不同类型的,即使他们有相同的属性信息,仅仅是因为自定义属性并没有在他们的情况下,该组件声明。
如果r3和r4都是RectangleWithString组件的实例,并且该组件定义包括一个名为的字符串属性的声明customProperty,则r3和r4会被认为是相同类型的(也就是说,它们将是该RectangleWithString类型的实例,而不是定义自己的实例隐式类型)。
无论何时做出有关内存分配或性能折衷的决策,务必牢记CPU缓存性能,操作系统分页和JavaScript引擎垃圾回收的影响。应对潜在解决方案进行仔细基准测试,以确保选择最佳解决方案。
没有一套通用准则可以取代对计算机科学基本原理的扎实理解,再加上对应用程序开发人员正在为其开发平台的实现细节的实践知识。此外,在做出权衡决策时,没有任何理论计算能够替代一套好的基准和分析工具。
有关缓存性能和内存时间权衡的深入信息,请参阅以下文章:
乌尔里希·德雷珀(Ulrich Drepper)的精彩文章:“每个程序员应该了解的内存知识”,网址:https : //people.freebsd.org/~lstewart/articles/cpumemory.pdf。
Agner Fog关于优化C ++应用程序的优秀手册,网址为:http
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。