一、关于时间的优化
在实际开发中,我们必须努力让渲染引擎实现每秒60帧的刷新率。60 FPS意味着每帧之间大约有16毫秒的时间可以执行处理,这包括:将绘制原语数据上传到图形硬件所需的处理。
在实践中,我们应遵循以下规则:
(1)尽可能使用异步的事件驱动来编程。
(2)使用工作线程来完成重要的处理操作。
(3)不要手动自旋事件循环。
(4)在阻塞函数中,保证每帧花费的时间不超过几毫秒。
如果不遵循以上规则,极有可能导致一些帧被跳过,从而会对用户体验和显示上产生重大影响。
『注意』:当从QML调用C++代码时,尽量避免创建自己的QEventLoop或调用QCoreApplication::processEvents()函数。这是一个危险的用法,因为当在信号处理程序或绑定中进入事件循环时,QML引擎会继续运行其他绑定、动画、转换等,这些绑定会导致副作用,例如:破坏事件循环的层次结构。
二、关于JavaScript代码的优化
在绝大多数的QML应用程序中,将以动态函数、信号处理程序和属性绑定表达式的形式包含大量JavaScript代码。由于QML引擎的一些优化,比如:对绑定编译器所做的优化,它(在某些情况下)可以比调用C++函数更快。但是,在实际开发中也同样需要遵循一些规则。
(2-1)解析属性
在QML应用程序中,我们需要知道属性解析操作是需要时间的。虽然在某些情况下,解析出的结果可以缓存和重用,但如果可能的话,尽量避免或减少属性解析操作。
在下面例子中,有一个代码块多次使用rectid和color属性解析对象:
// 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");
}
}
运行上述代码,在1000次迭代循环中,花费的时间如下:
可见,需要206毫秒的时间,但是我们可以优化这段代码,在代码块中只解析一次公共属性对象:
但上面代码还可以进一步优化(因为在循环处理过程中查找的属性是不会改变的),则可以将属性解析放到for循环外面,如下代码:
// 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();
//将解析属性对象的操作放到for循环之外
var rectColor = rect.color;
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");
}
}
(2-2)属性绑定
对于QML的属性绑定,如果属性绑定表达式引用的属性被更改,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];
}
}
}
上述代码中,onCompleted处理程序中的循环会导致text属性绑定被重新计算六次(然后导致依赖文本值的其他所有的属性绑定,以及onTextChanged信号处理程序,每次都会被重新计算,并每次都要显示文本)。因此,这显然是不必要的,因为我们实际上只关心累加值的最终值。
我们可以优化代码如下:
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;
}
}
三、序列技巧
在Qt QML中,有些序列类型是比较快速的(例如:QList<int>, QList<qreal>, QList<bool>, QList<QString>, QStringList和QList<QUrl>),相对来说其他的序列类型要慢许多。所使,在实际开发中应优先使用这些类型。
首先,序列类型有两种实现:
(1)一种是序列是QObject的Q_PROPERTY(称为引用序列)
(2)另一种是序列从QObject的Q_INVOKABLE函数返回(称为复制序列)。
通过QMetaObject::property()读取和写入引用序列,因此作为QVariant读取和写入。这意味着从JavaScript中改变序列中任何元素的值将导致三个步骤的发生:
1、完整的序列将从QObject(作为一个QVariant,但然后转换为正确类型的序列)。
2、位于指定索引处的元素将在该序列中更改
3、而完整的序列将被写回QObject(作为一个QVariant)。
复制序列要简单得多,因为实际序列存储在JavaScript对象的资源数据中,所以不会发生读/修改/写入周期(相反,直接修改资源数据)。
因此,写入引用序列的元素要比写入复制序列的元素慢得多。事实上,写入一个n元素引用序列的单个元素的代价相当于将一个n元素复制序列赋值给该引用序列,所以通常最好的做法是:修改一个临时复制序列,然后在计算期间将结果赋值给一个引用序列。
假设存在以下C++类型(事先注册到“Qt.example 1.0”命名空间):
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;
};
下面例子将在一个循环中写入引用序列的元素,将导致性能降低:
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属性读写操作,将会使此代码运行非常慢。我们可以优化代码如下:
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");
}
}
其次,如果属性中的元素发生变化,则会发出属性更改信号。如果在序列属性中,存在许多绑定到特定元素的绑定,那么最好创建一个绑定到该元素的动态属性,并将该动态属性作为绑定表达式中的符号,而不是序列元素,因为只有当其值发生变化时,才会导致重新计算绑定。
这种应用场景在实际开发中,乃是罕见的:
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处的元素被修改,但是这三个绑定都会被重新计算,因为更改信号的粒度是整个属性已经更改。因此,这时候添加中间绑定有时是有好处的:
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");
}
}
在上面示例中,每次只会重新计算中间绑定,从而显著提高性能。
(3-1)值类型的优化建议
值类型属性(字体、颜色、vector3d等)具有与QObject类似的属性。因此,上面给出的序列技巧也适用于值类型。虽然对于值类型来说,影响会非常的小(因为值类型的子属性数量通常远远少于序列中的元素数量),但增加任何不必要的重新计算绑定的数量都将对性能产生影响,在实际QML开发中应该时刻注意。
(3-2)其他JavaScript对象
Qt Quick 2使用的JavaScript引擎针对对象实例化和属性查找进行了优化,但提供的优化依赖于特定的标准。如果我们的应用程序的设计不符合标准,JavaScript引擎就会退回到“慢路径”模式(即在慢性能的条件下执行),因此性能会差得多。所以,在实际使用中需确保满足以下条件:
- 尽可能避免使用eval()。
- 不删除对象的属性。
四、内存分配和回收
在实际开发中,应用程序分配的内存数量以及分配内存的方式是非常重要的性能考虑因素。内存受限设备上分配内存时,分配的内存大小是一个重要的考虑因素;在堆上分配内存是一个计算成本相当高的操作,而且某些分配策略可能会导致跨页面的数据碎片增加。JavaScript使用了一个托管内存堆,它会自动进行垃圾收集,这有一些优点,但也有一些重要的含义。
用QML编写的应用程序使用c++堆和自动管理的JavaScript堆的内存。
本小节的提示和建议只是指导方针,并不是适用于所有情况。为了做出最佳决策,一定要使用经验指标仔细地进行测试和分析。
(1)惰性地实例化和初始化组件
例如,如果应用程序由多个视图(例如,多个选项卡)组成,但在任何时候只需要一个视图,那么则可以使用延迟实例化来缩小给定时间内需要分配的内存量。
(2)销毁没有使用的对象
如果是延迟加载组件,或者在JavaScript表达式中动态创建的对象,通常最好手动调用destroy()来销毁,而不是等待自动垃圾回收来处理。
(3)不要手动调用垃圾收集器
在大多数情况下,手动调用垃圾收集器是一种极其不好的使用方法,因为该操会将会长时间阻塞GUI线程。这可能会导致跳过帧和不稳定的动画。
在某些情况下,手动调用垃圾收集器是可以接受的(在下一节中将对此进行更详细的解释),但在大多数情况下,调用垃圾收集器是不必要的,并且会产生相反的效果。
(4)避免复杂的绑定
除了降低复杂绑定的性能之外(例如,由于必须进入JavaScript执行上下文来执行计算),它们在c++堆和JavaScript堆上占用的内存也比可以通过QML优化的绑定表达式求值器计算的绑定更多。
(5)避免定义多个相同的隐式类型
如果一个QML元素有一个在QML中定义的自定义属性,它就成为它自己的隐式类型。如果在组件中内联定义多个相同的隐式类型,会浪费一些内存。在这种情况下,通常最好显式地定义一个可以重用的新组件。
定义自定义属性通常是一种有益的性能优化(例如,可以减少需要或重新评估的绑定数量),或者可以提高组件的模块化和可维护性。在这种情况下,鼓励使用自定义属性。但是,如果新类型被多次使用,则应该将其拆分为自己的组件(.QML文件)以节省内存。
(6)重用现有的组件
如果正在考虑定义一个新组件,那么有必要再三检查,确保平台的组件集中不存在这样的组件。否则,将迫使QML引擎为一个类型生成和存储类型数据,该类型实际上是另一个预先存在且可能已经加载的组件的副本。
(7)使用单例类型而不是pragma库脚本
如果使用pragma库脚本存储在应用程序范围的实例数据,优先考虑使用QObject单例类型。这会带来更好的性能,并会减少JavaScript堆内存的使用。
五、在QML应用中的内存分配
QML应用程序的内存使用可以分为两部分:C++堆和JavaScript堆。其中分配一些内存是不可避免的,因为那些是由QML引擎或JavaScript引擎分配的,而其余的内存则取决于应用程序开发人员。
C++堆内存将包括:
- QML引擎固定且不可避免的开销(与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"
}
}
在前面例子中,矩形r0和r1没有任何自定义属性,因此JavaScript和QML引擎认为它们是同一类型的。也就是说,r0和r1都被认为是显式定义的矩形类型。矩形r2、r3和r4每个都有自定义属性,并且每个都被认为是不同的(隐式)类型。注意,r3和r4都被认为是不同的类型,尽管它们具有相同的属性信息,这仅仅是因为自定义属性没有在它们作为实例的组件中声明。
如果r3和r4都是RectangleWithString组件的实例,并且该组件定义包含了一个名为customProperty的字符串属性的声明,那么r3和r4将被认为是同一类型(也就是说,它们是RectangleWithString类型的实例,而不是定义它们自己的隐式类型)。
六、深度内存分配
在决定内存分配或性能权衡时,务必牢记cpu - 缓存性能、操作系统分页和JavaScript引擎垃圾收集的影响。应该仔细评估潜在的解决方案,以确保选择最佳方案。