【QT】QtQml应用程序的性能考虑与建议

本文翻译自Qt官网文档:

http://doc.qt.io/qt-5/qtquick-performance.html


QtQml应用程序的性能考虑与建议


1、时间考虑

作为一名程序开发者,应该努力使渲染引擎的刷新率维持在60fps,也就是说在每帧之间大约有16ms,这段时间包括了基本图元在图形硬件上的描画。具体内容如下:

>尽可能的使用异步事件驱动来编程。

>使用工作者线程来处理重要的事情,比如说QMLWorkerScript类型就是起用了一个新的线程。

>不要手动重复事件循环。

>每帧的函数阻塞的时间不要超过几毫秒。

如果不注意上面提到的内容,就会导致跳帧,影响用户体验。

注意:QMLC++交互时,为了避免阻塞就去创建自己的QEventLoop或调用QCoreApplication::processEvents(),这虽说是一种常见的模式,但也是危险的,因为信号处理或绑定进入事件循环时,QML引擎会继续运行其它的绑定、动画、状态迁移等,这些动作就可能带来副作用,例如,破坏包含了事件循环的层级结构。


2、性能分析

最重要的建议:使用QtCreator软件提供的QML性能分析工具,以查看应用程序的时间花销,这样就可以把着重点放在实际存在的问题上,而不是那些潜在的问题,QtCreator文档提供了QML性能分析工具的用法,可参考如下网址:

http://doc.qt.io/qtcreator/creator-qml-performance-monitor.html

通过QML性能分析工具,查看高频度的绑定、时间花销较大的函数,以优化焦点问题、重写实现细节,相反不使用QML性能分析工具的话,就没有显著的性能提升效果。


3JavaScript代码

QML是对JavaScript的扩展,所以在QML应用程序中经常有大量的JavaScript代码,例如函数与信号的动态参数类型或属性绑定表达式,这些通常都不是问题所在,反而由于QML引擎的优化,使得绑定比C++函数调用效率更高,但也要注意不必要事件的偶发性。


3.1绑定

绑定在QML中有两种类型:优化的和非优化的。绑定表达式越简单越好,QML引擎发挥了优化的绑定表达式的求值特性,使得简单的绑定表达式不用转换到纯JavaScript运行环境就可以求值。优化的绑定在求值时比复杂的非优化的绑定效率更高,前提是所有用到的类型信息在编译时刻都应该是知道的。最大化地优化绑定表达式应该避免以下事情:

>声明JavaScript中间变量。

>访问“var”类型的变量。

>调用JavaScript函数。

>用绑定表达式构建闭包或定义函数。

>在即时求值范围外访问属性。

>与其它属性绑定引起的副作用。

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

>绑定表达式所在对象的属性。

>组件中的id

>组件中的根元素id

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


3.2类型转换

当访问QML类型的属性时,对应的JavaScript对象就会被创建,这个对象带有外部资源,包括基本的C++数据,这是使用JavaScript的一个主要花销,在大多数情况下这个花销很小,但有时这个花销就很大,例如用C++QvariantMap类型的数据(用到了Q_PROPERTY)赋值给QMLvariant类型的属性,尽管intqrealboolQStringQUrl参数化的QList列表花销不大,但其它类型的列表花销就很大了,这个涉及到JavaScript数组的创建、类型添加及转换。在基本类型间转换的花销也可能不小,比如说stringurl类型间的转换,应该

使用匹配最接近的类型,避免不必要的类型转换。

如果必须暴露QVariantMap类型给QML时,QML的属性类型应该是var而不是variant,通常情况下,也应该优先考虑var类型,这样就可以存储真正的JavaScript引用,而且variant类型在QtQuick2.0及以后的版本中已标记为废弃使用。


3.3属性解析

属性解析要花一点时间,在某些情况下,多次访问同一属性时,其结果被存储起来的话,就可避免重复性的不必要的工作。如下例子,有个要经常用到的代码块,在for循环中多次访问了rect对象的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");
    }
}

for循环中保存相同的rectcolor

// 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");
    }
}

一个简单的改变就可以显著提高性能,上面的代码还可以做进一步的修改,因为rect对象的color属性在for循环中没有变,所以可以把这个存储过程移到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();
        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");
    }
}


3.4属性绑定

因为属性绑定表达式中的某一个属性改变时,这个表达式就会重新求值,所以绑定表达式要力求简单。在循环中使用了绑定表达式时,绑定属性的最后结果才是重要的,这时就可以使用一个临时累加变量,而不是直接在绑定属性上累加,以免额外的重复求值过程,下面的粒子说明了这一点:

// 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];
        }
    }
}

在上面的for循环中,accumulatedValue改了六次,导致与其绑定的text属性也改了六次,

onTextChanged信号处理器也响应了六次,这些都是不必要的,accumulatedValue的最终结果才是最重要的,使用中间变量temp修改如下:

// 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;
    }
}


3.5列表属性

前面提到了,QListQListQListQListQStringList

QList等类型在C++QML间的转换是快速的,其它的列表类型则是很慢的。即使是使用这些转换快速的列表类型,也要格外注意,以获得最佳性能。

首先,列表类型的实现有两个方法,一个是QObject对象的Q_PROPERTY宏,称作列表引用,另一个是来自QObject对象的Q_INVOKABLE宏标记的函数的返回值,称作列表拷贝。列表引用的读写是通过QMetaObject::property()进行的,属性类型通过QVariant来处理。

通过JavaScript改变列表属性值时要经过三个步骤:读取列表、改变列表中特定下标的元素、写入列表。列表拷贝相对来说就简单多了,因为JavaScript对象的资源数据存储了实际的列表,直接修改资源数据即可,不用经过列表引用的读、改、写这个过程。所以,列表拷贝比列表引用速度更快,事实上对单一的元素列表而言,两者速度是差不多的。通常情况下,要修改临时列表拷贝,然后把这个结果赋值给列表引用。假设定义了下面例子中的C++类且注册到了Qt.example包中,版本是1.0

class SequenceTypeExample : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY (QList qrealListProperty READ qrealListProperty WRITE setQrealListProperty NOTIFY qrealListPropertyChanged)

public:
    SequenceTypeExample() : QQuickItem() { m_list << 1.1 << 2.2 << 3.3; }
    ~SequenceTypeExample() {}

    QList qrealListProperty() const { return m_list; }
    void setQrealListProperty(const QList &list) { m_list = list; emit qrealListPropertyChanged(); }

signals:
    void qrealListPropertyChanged();

private:
    QList m_list;
};

下面的例子在for循环中给列表引用赋值,性能较差:

// 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");
    }
}

上面例子频繁读写QObjectqrealListProperty属性列表,性能欠佳,下面的更改效果会更好:

// 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的元素,但绑定到了列表中下标为1的属性也会重新求值,所以引入一个中间变量效果更好:

// 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");
    }
}

通过上面的修改,只有中间变量会重新求值,性能得到了改善。


3.6值类型

fontcolorvector3d等值类型,也有类似QObjectC++类,对于上面提到的列表属性的优化问题也是适用的,也要避免不必要的重复求值过程,注意性能影响。


3.7其它JavaScript对象

不同的JavaScript引擎提供了不同的优化措施,QtQuick2使用的JavaScript引擎用来对象实例和属性查询的优化,但也有一定的前提条件,如果前提条件不满足,性能就会受到严重的影响,下面两个条件是必须要保证的:

>尽量避免使用eval()

>不要删除对象的属性。


4、通用的接口元素


4.1文本元素

文本布局解析是一个较慢的操作,为了减少布局引擎的工作量,尽可能的使用PlainText而不是StyledText。如文本中要嵌入图片或标签,以及文本字体加粗或倾斜等,这时就要用到StyledText了,但还是有文本解析上的开销,用AutoText即可。另外,不建议使用RichText,因为StyledText已提供了几乎所有的文本特性。


4.2图片

图片在UI的地位是举足轻重的,但由于其加载时间的开销、内存消耗及不同的使用方式,也是许多问题的主要来源。

异步加载——

图片资源一般是很大的,加载时要避免阻塞UI线程,异步加载是个很好的选择,它的加载过程会在一个低优先级的工作线程执行。使用QMLImage元素加载本地图片时,把它的asynchronous属性设置为true就是异步加载,如果加载的是远程非本地的图片,加载方式默认就是异步的。

显式指定图片尺寸——

图片的sourceSize属性改变时,图片会重新加载。如果在一个小的元素中加载一张大的图片,应该设置sourceSize属性与小元素的尺寸一致,保证图片是以小尺寸而非本身大尺寸的形式缓存的。

运行时避免图片合成——

避免在运行时是提供预先合成的图片资源,比如说提供带有阴影效果的元素。


4.3anchors锚布局

在元素布局时,使用anchors锚布局比属性绑定效率更高。下面例子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
}

如下例子使用anchors锚布局效果更好:

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
}

位置绑定虽说灵活,但相对anchors锚布局来说效率还是不高的。如果布局非动态的话,使用静态初始化是最好的方式,坐标是相对于父元素的,子元素相对于父元素的偏移量想要是固定值的话,就不应该使用anchors锚布局。下面的例子中有两个子Rectangle,位置和大小一样,但布局方式不同,anchors锚布局这时就没有静态初始化效率高了。

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


5、模型与视图

许多应用程序都至少有一个给视图提供数据的模型,在性能最优化方面,这也是一个需要注意的地方。


5.1自定义的C++模型

C++中自定义模型供QML中的视图使用,最优实现在很大程度上依赖具体的用例,使用指南如下:

>尽可能的使用异步。

>在低优先级的工作者线程中做所有的处理。

>分批执行后端操作以降低I/OIPC

>使用滑动窗口缓存结果。

使用低优先级的工作者线程是很重要的,这样可以减小GUI线程“挨饿”的风险,以免导致糟糕的性能,另外,同步与上锁机制也会影响性能,也是推荐避免使用的。


5.2ListModel这个QML类型

QML提供的ListModel类型给ListView提供数据,只要运用正确的话,大多数用例也会有相对较佳的性能。


5.2.1使用工作者线程填充

ListModel元素可在低优先级的工作者线程用JavaScript形式来填充,在WorkScript中作了改变时,要调用sync()以同步这些变化到主线程。WorkScript用法可参考Qt文档。使用WorkScript元素时,会调用各自的JavaScript引擎,增加内存使用,然而多个WorkScript元素用的是同一个工作者线程,在内存的影响上就可以忽略不计了。


5.2.2不要使用动态角色

ListModel元素在QtQuick2中的性能要好于QtQuick1,这主要是基于给定模型中每个角色的类型,类型不变时,缓存性能的提升是非常明显的,类型动态变化时,性能提升就会受到影响,因此dynamicRoles默认是不可用的,手动设置为true时就要承担随之而来的性能退化了,如果可能的话重新设计应用程序而不要使用动态类型。


5.3视图

视图代理越简单越好,使用QML展示必要的信息,任何额外的功能都不是即时的,在真正用到它时才会体现出来,设计视图代理时要注意以下内容:

>代理中的元素越少,代理的创建越快,视图的滚动越快。

>代理中的绑定尽可能少,位置布局时用anchors替代绑定或相对位置。

>代理中的元素避免使用ShaderEffect

>代理中不要使用clip剪切效果。

设置cacheBuffer属性,允许异步创建和可视化区域外的缓存,对于那些重要的不可能在单帧下创建的代理,cacheBuffer便是推荐设置的。cacheBuffer会使用一部分内存,这是开发者要权衡的事情,避免视图滚动时帧刷新率的降低。


6、可视化特效

QtQuick2提供了一些特性,供开发设计者创建例外地诱人的UI,如流畅的动态变换和一些可视化的特性,在QML中使用这些特性的同时,也要注意性能方面的影响。


6.1动画

通常说来,一个属性播放动画时会引起与之绑定的表达式重新求值,这是想要的结果,但有些情况下,期望绑定优先于属性动画,所以在属性动画结束后重新设置这个绑定。在动画中要避免使用JavaScript,如x属性动画的每帧要避免复杂的JavaScript表达式。开发者也要注意脚本动画的使用,因为脚本动画是在主线程执行的,时间过长的话有可能导致跳帧。


6.2粒子

QtQuick.Particles模块使得漂亮的粒子效果可以UI中得以实现,然而每个平台的图形硬件性能都不同,粒子当然也就不能无限的设置其参数了,以免超出硬件支持的范围。试图描画的粒子越多,所需的图形硬件以60FPS来描画就要求越高,渲染更多的粒子也要求更快的CPU,因此在目标平台上测试粒子效果是非常重要的,校正以60FPS渲染粒子的数量和尺寸。用不到粒子系统时不要使用粒子,如不可视元素,减少不必要的仿真。关于粒子系统的性能指南可参考如下Qt官方网址:

http://doc.qt.io/qt-5/qtquick-particles-performance.html


7、元素生命周期的控制

把一个应用程序拆分成简单的、模块化的组件,每个组件包含有一个单一的QML文件,这样就可以获得快速的启动时,拥有更好的内存管理,减少程序中已激活却不可见的元素。


7.1实例化延迟

QML引擎做了一些微妙的事情来保证组件加载和实例化不引起跳帧,然而除了避免做不必要的工作和工作延迟外,却没有更好的办法来减少启动时,QMLLoader便可以动态的创建组件。


7.1.1使用Loader

LoaderQML中的一个元素,可以动态的加载和卸载一个组件:

>使用active属性,可以延迟实例化。

>使用setSource()函数,提供初始属性值。

>asynchronouseture时,在组件实例化时可提高流畅性。


7.1.2使用动态创建

开发者可使用Qt.createComponnet()在运行时动态的创建组件,然后调用createObject()来完成实例化,还需要的就是在适当的时候手动删除创建的对象,具体可参考如下网址:

http://doc.qt.io/qt-5/qtqml-javascript-dynamicobjectcreation.html


7.2销毁不用的元素

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


8、渲染

QtQuick2中,用作渲染的场景图可在60FPS下高度动态的渲染UI,但有些东西会极大的降低渲染性能,这些缺点便是开发者需要注意的地方。


8.1剪切

剪切默认是不可用的,在需要的时候再去设置它。剪切效果是可视的,它并不是一项优化措施,而是根据边界剪切掉自己和孩子的多余部分,打破了自由的渲染各个元素的顺序,使最优变成了次优,特别是在视图代表中要不惜一切代价避免使用剪切功能。


8.2可视元素与覆盖

如果某些元素被其它不透明的元素完全覆盖时,最好的办法是设置这些元素的visible属性为false,让它们不可见,否则它们的描画就是多余的。同样,在不可见元素需要在启动时初始化的情况下,也要设置其visible属性为false


8.3透明与不透明

不透明的内容通常比透明的内容描画起来要快点,这是因为透明还需混调,不透明在优化方面反而更好。一张图片即使是不透明的,但在某个像素透明时是被当作全透明来处理的,这个对有透明边框的BorderImage也是适用的。


8.4着色

QMLShaderEffect类型可以在QtQuick应用程序中嵌入GLSL代码,重要的是,着色范围内的每一个像素都要执行片源着色程序,如果在低端硬件上着色大量的像素时就会导致性能变差。用GLSL写的着色程序支持复杂的变换和可视化的特性,使用ShaderEffectSource可以在场景描画之前预先着色到FBO中,FBO即帧缓存对象。


9、内存分配与回收

在应用程序中内存分配的总量及分配方式是非常重要的,抛去设备的内存限制不说,在堆上分配内存也是一笔昂贵的花销,这导致了整页数据的碎片化,JavaScript对内存的处理则是自动回收,这是它的好处。一个QML应用程序的内存要用到C++的堆和JavaScript的自动内存管理,在性能优化上,这两者的微妙之处需要格外注意。


9.1对于QML应用程序开发者的建议

这些建议只是一个简单的指南,不一定在所有的场合下都适用,但为了做最好的决定,一定要通过一个富有经验的基准来分析。


9.1.1延时实例化

如果在一个应用程序中有多个视图,但在某一时刻只会用到其中一个,那么其它的视图就可以使用延时实例化来降低内存消耗。


9.1.2销毁不用的对象

如果延时实例化了一个组件,或者用JavaScript表达式动态的创建了一个对象,较好的做法是调用destroy()来销毁,而不是等待垃圾自动回收机制来处理它。


9.1.3不要手动触发垃圾回收器

在大多数情况下,手动触发垃圾回收器是不明智的,不仅没必要反而还有副作用,因为这会阻塞GUI线程一段时间,导致跳帧和动画的不连贯,这个需要极力避免。


9.1.4避免复杂绑定

复杂的绑定会降低性能,同时还会占用更多的内存。


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

如果在QML中自定义了一个元素的属性,该属性就成了这个元素的隐式类型,这方面的内容会在下面作详细介绍。如果定义了多个隐式而又相同的类型,有些内存就浪费了,较好的做法是显式地定义一个新的组件来做同样的事情,这个组件还可复用。自定义属性可以优化性能,比如说减少了绑定和表达式重新求值的次数,而且还提高了组件的模块化特性和可维护性,然而这个属性多次使用时,就要放到自己的组件中。


9.1.6复用存在的组件

如果正在考虑定义一个新的组件时,一定要仔细确认这个组件是不存在的,否则就应该从另一个存在的对象中作个拷贝。


9.1.7使用单例代替pragma

如果使用pragma库存储应用程序的实例数据时,考虑使用QObject单例是个更好的选择,因为后者性能更好,也会使用更少的JavaScript堆。


9.2QML应用程序中的内存分配

QML应用程序中的内存用法可以分为两个部分:C++JavaScript。这两者中的有些内存分配是不可避免的,可能由QML引擎或JavaScript引擎分配,然后其它的内存分配主要取决于开发者。

C++堆包括:

>固定的不可避免的QML引擎分配的内存。

>每一个组件编译时的数据和类型信息。

>每一个C++对象的数据,以及每一个元对象的元素。

>QMLimport中的数据。

Javascript堆包括:

>固定的不可避免的JavaScript引擎分配的内存。

>固定的不可避免的JavaScript集成。

>JavaSciprt引擎运行时的类型信息。

>JavaScript的对象数据,如var类型、函数、信号等。

>表达式求值分配的变量。


进一步来说,JavaScript堆分配有一种情况是在主线程的,还有一种可选的情况是在WorkerScript线程的。如果在应用程序中没有使用WorkerScript元素的话,也就没有上面提到的工作者线程。JavaScript堆分配可能是几百万字节的,对于内存受限的设备来说,虽然WorkerScript元素有助于异步模型填充,但也要避免使用WorkerScript

QML引擎和JavaScript引擎都会自动地产生缓存数据,每一个应用程序加载的组件是一个不同的、明确的类型,在QML中每一个元素自定义的属性是隐式类型,任何没有自定义属性的元素都被JavaScriptQML引擎当作是组件定义的显式类型,而不是它们自己的隐式类型,看下面的例子:

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

在上面的例子中,r0r1对象都没有自定义属性,JavaScriptQML引擎认为它们是同一个类型,即显式的Rectangle类型。r2r3r4都有一个自定义属性,它们就被隐式地认为是不同的类型,r3r4虽有相同的自定义属性,但也是不同的类型。如果r3r4RectangleWithString组件的实例,这个组件定义了customProperty字符串属性,那么r3r4就被认为是同一个类型。


9.3深度考虑内存分配

无论何时决定考虑内存分配和性能权衡性能时,注意CPU缓存、操作系统分页和JavaScript引擎垃圾回收都是重要的,对比分析一些潜在的方法后选一个最佳方案。下面的计算机科学原则是应用程序开发者在实现细节中的实践经验结合起来的,对于这些原则的理解是非常重要的,另外,权衡性能时的对比分析是不能由理论计算代替的。


9.3.1碎片

碎片是C++开发的一个问题,如果应用程序开发者没有定义任何C++类型或插件的话,这倒是安全的,可以忽略这部分内容。久而久之,应用程序将分派很大一部分内存并写入数据,随后某些内存结束了数据使用后又会被释放掉,但是这些释放的内存不是在一个连续块里,是不能被操作系统的其它应用程序再次使用的,而且在缓存和特性访问上也有影响,因为这些数据可能占据了物理内存的不同分页,反过来又影响了操作系统的性能。碎片是可以避免的,使用池分配器或其它连续的内存分配器,减少内存总量,在任何时候分配内存时,小心的管理对象生命周期,周期性地清洁、重建缓存,或者使用类似JavaScript的运行时内存管理进行垃圾回收。


9.3.2垃圾回收

JavaScript提供了垃圾回收器,内存在JavaScript引擎管理的JavaScript堆上分配,JavaScript引擎周期性地在JavaScript堆上收集所有的未引用的数据,如果有了碎片的话,就会压缩这个堆,把现有的全部数据移到连续的内存区域,这样释放掉的内存就又可以被操作系统的其它应用程序使用了。


9.3.2.1垃圾回收的意义

垃圾回收是吧双刃剑,好处是解决了碎片问题,也就是说手动管理对象的生命周期不再那么重要了,但是不足之处在于它的潜在的长时间持续的操作,这个可能由JavaScript引擎在某个时刻就开始了,却在应用程序开发者的控制之外。如果JavaScript堆运用不当的话,垃圾回收的频率和持续时间对程序会有负面影响。


9.3.2.2手动触发垃圾回收器

QML应用程序在某些阶段需要垃圾回收,当可用内存不足时,JavaScript引擎就会自动触发垃圾回收,有时候程序开发者决定何时触发垃圾回收器也是可行的。程序开发者很可能已经理解了程序在何时将会有一段很长的空闲时间,在QML程序中使用了很多的JavaScript堆内存,在特定的性能易受影响的任务中会引起周期性的垃圾回收,所以开发者可以在不考虑用户体验受到影响时手动触发垃圾回收,要调用gc()函数,这将带来深层次的垃圾回收及压缩周期,可能是几百毫秒,也可能超过几千毫秒,如果可能的哈,还是不要手动触发垃圾回收器。


9.3.3内存与性能

在某些情况下,需要牺牲内存的消耗以换取性能的提升,如前面例子中提到的增加JavaScript临时变量以缓存查询结果的情形。有时候,增加内存消耗带来的影响也是很严重的,这时候就要权衡内存与性能的影响了。



你可能感兴趣的:(图形图像)