QML應用程序的性能考慮與建議

QML 應用程序的性能考慮與建議

原文:csdn aidear_evo QtQml應用程序的性能考慮與建議

本文翻譯自Qt官網文檔:http://doc.qt.io/qt-5/qtquick-performance.html

時間考慮

作爲一名程序開發者,應該努力使渲染引擎的刷新率維持在60fps,也就是說在每幀之間大約有16ms,這段時間包括了基本圖元在圖形硬件上的描畫。具體內容如下:

  1. 儘可能的使用異步事件驅動來編程。

  2. 使用工作者線程來處理重要的事情,比如說QML的WorkerScript類型就是起用了一個新的線程。

  3. 不要手動重複事件循環。

  4. 每幀的函數阻塞的時間不要超過幾毫秒。

如果不注意上面提到的內容,就會導致跳幀,影響用戶體驗。

注意:QML 與 C++ 交互時,爲了避免阻塞就去創建自己的 QEventLoop 或調用QCoreApplication::processEvents(),這雖說是一種常見的模式,但也是危險的,因爲信號處理或綁定進入事件循環時,QML 引擎會繼續運行其它的綁定、動畫、狀態遷移等,這些動作就可能帶來副作用,例如,破壞包含了事件循環的層級結構。

性能分析

最重要的建議:使用 QtCreator 軟件提供的 QML 性能分析工具,以查看應用程序的時間花銷,這樣就可以把着重點放在實際存在的問題上,而不是那些潛在的問題,QtCreator 文檔提供了 QML 性能分析工具的用法,可參考如下網址:Profiling QML Applications

通過 QML 性能分析工具,查看高頻度的綁定、時間花銷較大的函數,以優化焦點問題、重寫實現細節,相反不使用 QML 性能分析工具的話,就沒有顯著的性能提升效果。

JavaScript代碼

QML 是對 JavaScript 的擴展,所以在 QML 應用程序中經常有大量的 JavaScript 代碼,例如函數與信號的動態參數類型或屬性綁定表達式,這些通常都不是問題所在,反而由於 QML 引擎的優化,使得綁定比 C++ 函數調用效率更高,但也要注意不必要事件的偶發性。

綁定

綁定在 QML 中有兩種類型:優化的和非優化的。綁定表達式越簡單越好,QML 引擎發揮了優化的綁定表達式的求值特性,使得簡單的綁定表達式不用轉換到純 JavaScript 運行環境就可以求值。優化的綁定在求值時比複雜的非優化的綁定效率更高,前提是所有用到的類型信息在編譯時刻都應該是知道的。最大化地優化綁定表達式應該避免以下事情:

  1. 聲明JavaScript中間變量。

  2. 訪問“var”類型的變量。

  3. 調用JavaScript函數。

  4. 用綁定表達式構建閉包或定義函數。

  5. 在即時求值範圍外訪問屬性。

  6. 與其它屬性綁定引起的副作用。

在運行 QML 應用程序時,可能要設置 QML_COMPILER_STATS 環境變量以打印與綁定相關的數據,當知道對象和屬性的類型時,綁定速度是最快的,也就是說在綁定表達式查值過程中某些非最終狀態的屬性的類型可能會有變化,這樣綁定速度就變慢了。即時求值範圍包括以下內容:

  1. 綁定表達式所在對象的屬性。

  2. 組件中的 id。

  3. 組件中的根元素 id。

其它組件的對象 id、屬性,還有通過 import 導入的東西,是不在即時求值範圍的,在這種情況下,綁定是不被優化的。需要注意的是,如果綁定沒有被 QML 引擎優化,就會在純 JavaScript 環境中求值,這時上面的幾點建議就不再適用了。有時候在非常複雜的綁定中,緩存 JavaScript 中間變量的屬性求值結果是有益的,這個在後面的內容中會有描述。

類型轉換

當訪問 QML 類型的屬性時,對應的 JavaScript 對象就會被創建,這個對象帶有外部資源,包括基本的 C++ 數據,這是使用 JavaScript 的一個主要花銷,在大多數情況下這個花銷很小,但有時這個花銷就很大,例如用 C++ 的 QvariantMap 類型的數據(用到了 Q_PROPERTY )賦值給 QML 的 variant 類型的屬性,儘管 intqrealboolQStringQUrl 參數化的 QList 列表花銷不大,但其它類型的列表花銷就很大了,這個涉及到 JavaScript 數組的創建、類型添加及轉換。在基本類型間轉換的花銷也可能不小,比如說 string 和 url 類型間的轉換,應該使用匹配最接近的類型,避免不必要的類型轉換。

如果必須暴露 QVariantMap 類型給 QML 時,QML 的屬性類型應該是 var 而不是 variant,通常情況下,也應該優先考慮 var 類型,這樣就可以存儲真正的 JavaScript 引用,而且 variant 類型在QtQuick2.0 及以後的版本中已標記爲廢棄使用。

屬性解析

屬性解析要花一點時間,在某些情況下,多次訪問同一屬性時,其結果被存儲起來的話,就可避免重複性的不必要的工作。如下例子,有個要經常用到的代碼塊,在 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 循環中保存相同的 rect 的 color

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

屬性綁定

因爲屬性綁定表達式中的某一個屬性改變時,這個表達式就會重新求值,所以綁定表達式要力求簡單。在循環中使用了綁定表達式時,綁定屬性的最後結果纔是重要的,這時就可以使用一個臨時累加變量,而不是直接在綁定屬性上累加,以免額外的重複求值過程,下面的例子說明了這一點:

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

列表屬性

前面提到了,QList<int>QList<qreal>QList<bool>QList<QString>QStringList、 
QList<QUrl>等類型在 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<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;
};

下面的例子在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");
    }
}

上面例子頻繁讀寫 QObject 的 qrealListProperty 屬性列表,性能欠佳,下面的更改效果會更好:

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

通過上面的修改,只有中間變量會重新求值,性能得到了改善。

值類型

如 fontcolorvector3d 等值類型,也有類似 QObject 的 C++ 類,對於上面提到的列表屬性的優化問題也是適用的,也要避免不必要的重複求值過程,注意性能影響。

其它JavaScript對象

不同的 JavaScript 引擎提供了不同的優化措施,QtQuick2 使用的 JavaScript 引擎用來對象實例和屬性查詢的優化,但也有一定的前提條件,如果前提條件不滿足,性能就會受到嚴重的影響,下面兩個條件是必須要保證的:

  1. 儘量避免使用 eval()

  2. 不要刪除對象的屬性。

通用的接口元素

文本元素

文本佈局解析是一個較慢的操作,爲了減少佈局引擎的工作量,儘可能的使用 PlainText 而不是StyledText。如文本中要嵌入圖片或標籤,以及文本字體加粗或傾斜等,這時就要用到 StyledText 了,但還是有文本解析上的開銷,用 AutoText即可。另外,不建議使用 RichText,因爲 StyledText 已提供了幾乎所有的文本特性。

圖片

圖片在 UI 的地位是舉足輕重的,但由於其加載時間的開銷、內存消耗及不同的使用方式,也是許多問題的主要來源。

  1. 異步加載。

    圖片資源一般是很大的,加載時要避免阻塞 UI 線程,異步加載是個很好的選擇,它的加載過程會在一個低優先級的工作線程執行。使用 QML 的 Image 元素加載本地圖片時,把它的 asynchronous 屬性設置爲 true 就是異步加載,如果加載的是遠程非本地的圖片,加載方式默認就是異步的。

  2. 顯式指定圖片尺寸。

    圖片的 sourceSize 屬性改變時,圖片會重新加載。如果在一個小的元素中加載一張大的圖片,應該設置 sourceSize 屬性與小元素的尺寸一致,保證圖片是以小尺寸而非本身大尺寸的形式緩存的。

  3. 運行時避免圖片合成。

    避免在運行時是提供預先合成的圖片資源,比如說提供帶有陰影效果的元素。

anchors 錨佈局

在元素佈局時,使用 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
    }
}

模型與視圖

許多應用程序都至少有一個給視圖提供數據的模型,在性能最優化方面,這也是一個需要注意的地方。

自定義的 C++ 模型

在C++中自定義模型供 QML 中的視圖使用,最優實現在很大程度上依賴具體的用例,使用指南如下:

  1. 儘可能的使用異步。

  2. 在低優先級的工作者線程中做所有的處理。

  3. 分批執行後端操作以降低 I/O 和 IPC。

  4. 使用滑動窗口緩存結果。

使用低優先級的工作者線程是很重要的,這樣可以減小 GUI 線程“捱餓”的風險,以免導致糟糕的性能,另外,同步與上鎖機制也會影響性能,也是推薦避免使用的。

ListModel 這個 QML 類型

QML提供的 ListModel 類型給 ListView 提供數據,只要運用正確的話,大多數用例也會有相對較佳的性能。

使用工作者線程填充

ListModel 元素在 QtQuick2 中的性能要好於 QtQuick1,這主要是基於給定模型中每個角色的類型,類型不變時,緩存性能的提升是非常明顯的,類型動態變化時,性能提升就會受到影響,因此 dynamicRoles 默認是不可用的,手動設置爲 true 時就要承擔隨之而來的性能退化了,如果可能的話重新設計應用程序而不要使用動態類型。

視圖

視圖代理越簡單越好,使用 QML 展示必要的信息,任何額外的功能都不是即時的,在真正用到它時纔會體現出來,設計視圖代理時要注意以下內容:

  1. 代理中的元素越少,代理的創建越快,視圖的滾動越快。

  2. 代理中的綁定儘可能少,位置佈局時用anchors替代綁定或相對位置。

  3. 代理中的元素避免使用 ShaderEffect

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

設置 cacheBuffer 屬性,允許異步創建和可視化區域外的緩存,對於那些重要的不可能在單幀下創建的代理,cacheBuffer 便是推薦設置的。cacheBuffer 會使用一部分內存,這是開發者要權衡的事情,避免視圖滾動時幀刷新率的降低。

可視化特效

QtQuick2 提供了一些特性,供開發設計者創建例外地誘人的 UI,如流暢的動態變換和一些可視化的特性,在 QML 中使用這些特性的同時,也要注意性能方面的影響。

動畫

通常說來,一個屬性播放動畫時會引起與之綁定的表達式重新求值,這是想要的結果,但有些情況下,期望綁定優先於屬性動畫,所以在屬性動畫結束後重新設置這個綁定。在動畫中要避免使用 JavaScript,如 x 屬性動畫的每幀要避免複雜的 JavaScript 表達式。開發者也要注意腳本動畫的使用,因爲腳本動畫是在主線程執行的,時間過長的話有可能導致跳幀。

粒子

QtQuick.Particles 模塊使得漂亮的粒子效果可以 UI 中得以實現,然而每個平臺的圖形硬件性能都不同,粒子當然也就不能無限的設置其參數了,以免超出硬件支持的範圍。試圖描畫的粒子越多,所需的圖形硬件以 60 FPS 來描畫就要求越高,渲染更多的粒子也要求更快的 CPU,因此在目標平臺上測試粒子效果是非常重要的,校正以60FPS渲染粒子的數量和尺寸。用不到粒子系統時不要使用粒子,如不可視元素,減少不必要的仿真。關於粒子系統的性能指南可參考如下Qt官方網址:Particle System Performance Guide

元素生命週期的控制

把一個應用程序拆分成簡單的、模塊化的組件,每個組件包含有一個單一的 QML 文件,這樣就可以獲得快速的啓動時,擁有更好的內存管理,減少程序中已激活卻不可見的元素

實例化延遲

QML 引擎做了一些微妙的事情來保證組件加載和實例化不引起跳幀,然而除了避免做不必要的工作和工作延遲外,卻沒有更好的辦法來減少啓動時,QML 的 Loader 便可以動態的創建組件。

使用Loader

Loader是QML中的一個元素,可以動態的加載和卸載一個組件:

  1. 使用 active 屬性,可以延遲實例化。

  2. 使用 setSource() 函數,提供初始屬性值。

  3. asynchronouse 爲 ture 時,在組件實例化時可提高流暢性。

使用動態創建

開發者可使用 Qt.createComponnet() 在運行時動態的創建組件,然後調用 createObject() 來完成實例化,還需要的就是在適當的時候手動刪除創建的對象,具體可參考如下網址:Dynamic QML Object Creation from JavaScript

銷燬不用的元素

有些不可見元素,如 Tab 插件的第一個插件顯示時後面的就不可見,因爲它們是不可見元素的孩子,所以在大多數情況下要延遲實例化,而且在不用的時候還要銷燬,以避免不必要的開銷,如渲染、動畫、綁定等。使用 Loader 加載元素時,可以重置 source 或 sourceComponnet 屬性爲空來釋放這個元素,其它的元素可調用 destroy() 來銷燬。在某些情況下,這些元素還是要保留激活狀態,但在最後要設置爲不可見。下面的渲染部分也會談到這一點。

渲染

在 QtQuick2 中,用作渲染的場景圖可在 60 FPS 下高度動態的渲染 UI,但有些東西會極大的降低渲染性能,這些缺點便是開發者需要注意的地方。

剪切

剪切默認是不可用的,在需要的時候再去設置它。剪切效果是可視的,它並不是一項優化措施,而是根據邊界剪切掉自己和孩子的多餘部分,打破了自由的渲染各個元素的順序,使最優變成了次優,特別是在視圖代表中要不惜一切代價避免使用剪切功能

可視元素與覆蓋

如果某些元素被其它不透明的元素完全覆蓋時,最好的辦法是設置這些元素的 visible 屬性爲 false,讓它們不可見,否則它們的描畫就是多餘的。同樣,在不可見元素需要在啓動時初始化的情況下,也要設置其 visible 屬性爲 false

透明與不透明

不透明的內容通常比透明的內容描畫起來要快點,這是因爲透明還需混調,不透明在優化方面反而更好。一張圖片即使是不透明的,但在某個像素透明時是被當作全透明來處理的,這個對有透明邊框的BorderImage 也是適用的。

着色

QML 的 ShaderEffect 類型可以在 QtQuick 應用程序中嵌入 GLSL 代碼,重要的是,着色範圍內的每一個像素都要執行片源着色程序,如果在低端硬件上着色大量的像素時就會導致性能變差。用 GLSL 寫的着色程序支持複雜的變換和可視化的特性,使用 ShaderEffectSource 可以在場景描畫之前預先着色到 FBO 中,FBO 即幀緩存對象。

內存分配與回收

在應用程序中內存分配的總量及分配方式是非常重要的,拋去設備的內存限制不說,在堆上分配內存也是一筆昂貴的花銷,這導致了整頁數據的碎片化,JavaScript 對內存的處理則是自動回收,這是它的好處。一個 QML 應用程序的內存要用到 C++ 的堆和 JavaScript 的自動內存管理,在性能優化上,這兩者的微妙之處需要格外注意。

對於QML應用程序開發者的建議

這些建議只是一個簡單的指南,不一定在所有的場合下都適用,但爲了做最好的決定,一定要通過一個富有經驗的基準來分析

延時實例化

如果在一個應用程序中有多個視圖,但在某一時刻只會用到其中一個,那麼其它的視圖就可以使用延時實例化來降低內存消耗。

銷燬不用的對象

如果延時實例化了一個組件,或者用 JavaScript 表達式動態的創建了一個對象,較好的做法是調用 destroy() 來銷燬,而不是等待垃圾自動回收機制來處理它。

不要手動觸發垃圾回收器

在大多數情況下,手動觸發垃圾回收器是不明智的,不僅沒必要反而還有副作用,因爲這會阻塞 GUI 線程一段時間,導致跳幀和動畫的不連貫,這個需要極力避免。

避免複雜綁定

複雜的綁定會降低性能,同時還會佔用更多的內存。

避免定義多個隱式而又相同的類型

如果在 QML 中自定義了一個元素的屬性,該屬性就成了這個元素的隱式類型,這方面的內容會在下面作詳細介紹。如果定義了多個隱式而又相同的類型,有些內存就浪費了,較好的做法是顯式地定義一個新的組件來做同樣的事情,這個組件還可複用。自定義屬性可以優化性能,比如說減少了綁定和表達式重新求值的次數,而且還提高了組件的模塊化特性和可維護性,然而這個屬性多次使用時,就要放到自己的組件中。

複用存在的組件

如果正在考慮定義一個新的組件時,一定要仔細確認這個組件是不存在的,否則就應該從另一個存在的對象中作個拷貝。

使用單例代替 pragma 庫

如果使用 pragma 庫存儲應用程序的實例數據時,考慮使用 QObject 單例是個更好的選擇,因爲後者性能更好,也會使用更少的 JavaScript 堆。

在 QML 應用程序中的內存分配

在 QML 應用程序中的內存用法可以分爲兩個部分:C++ 和 JavaScript。這兩者中的有些內存分配是不可避免的,可能由 QML 引擎或 JavaScript 引擎分配,然後其它的內存分配主要取決於開發者。

C++堆包括:

  1. 固定的不可避免的 QML 引擎分配的內存。

  2. 每一個組件編譯時的數據和類型信息。

  3. 每一個 C++ 對象的數據,以及每一個元對象的元素。

  4. QML 的 import 中的數據。

Javascript堆包括:

  1. 強固定的不可避免的 JavaScript 引擎分配的內存。

  2. 固定的不可避免的 JavaScript 集成。

  3. 在 JavaSciprt 引擎運行時的類型信息。

  4. JavaScript 的對象數據,如 var 類型、函數、信號等。

  5. 表達式求值分配的變量。

進一步來說,JavaScript 堆分配有一種情況是在主線程的,還有一種可選的情況是在 WorkerScript 線程的。如果在應用程序中沒有使用 WorkerScript 元素的話,也就沒有上面提到的工作者線程。JavaScript 堆分配可能是幾百萬字節的,對於內存受限的設備來說,雖然 WorkerScript 元素有助於異步模型填充,但也要避免使用 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 引擎認爲它們是同一個類型,即顯式的 Rectangle 類型。r2、r3 和 r4 都有一個自定義屬性,它們就被隱式地認爲是不同的類型,r3 和 r4 雖有相同的自定義屬性,但也是不同的類型。如果 r3 和 r4 是 RectangleWithString 組件的實例,這個組件定義了 customProperty 字符串屬性,那麼 r3 和 r4 就被認爲是同一個類型。

深度考慮內存分配

無論何時決定考慮內存分配和性能權衡性能時,注意 CPU 緩存、操作系統分頁和 JavaScript 引擎垃圾回收都是重要的,對比分析一些潛在的方法後選一個最佳方案。下面的計算機科學原則是應用程序開發者在實現細節中的實踐經驗結合起來的,對於這些原則的理解是非常重要的,另外,權衡性能時的對比分析是不能由理論計算代替的。

碎片

碎片是 C++ 開發的一個問題,如果應用程序開發者沒有定義任何 C++ 類型或插件的話,這倒是安全的,可以忽略這部分內容。久而久之,應用程序將分派很大一部分內存並寫入數據,隨後某些內存結束了數據使用後又會被釋放掉,但是這些釋放的內存不是在一個連續塊裏,是不能被操作系統的其它應用程序再次使用的,而且在緩存和特性訪問上也有影響,因爲這些數據可能佔據了物理內存的不同分頁,反過來又影響了操作系統的性能。碎片是可以避免的,使用池分配器或其它連續的內存分配器,減少內存總量,在任何時候分配內存時,小心的管理對象生命週期,週期性地清潔、重建緩存,或者使用類似 JavaScript 的運行時內存管理進行垃圾回收。

垃圾回收

JavaScript 提供了垃圾回收器,內存在 JavaScript 引擎管理的 JavaScript 堆上分配,JavaScript 引擎週期性地在 JavaScript 堆上收集所有的未引用的數據,如果有了碎片的話,就會壓縮這個堆,把現有的全部數據移到連續的內存區域,這樣釋放掉的內存就又可以被操作系統的其它應用程序使用了。

垃圾回收的意義

垃圾回收是把雙刃劍,好處是解決了碎片問題,也就是說手動管理對象的生命週期不再那麼重要了,但是不足之處在於它的潛在的長時間持續的操作,這個可能由 JavaScript 引擎在某個時刻就開始了,卻在應用程序開發者的控制之外。如果 JavaScript 堆運用不當的話,垃圾回收的頻率和持續時間對程序會有負面影響。

手動觸發垃圾回收器

QML 應用程序在某些階段需要垃圾回收,當可用內存不足時,JavaScript 引擎就會自動觸發垃圾回收,有時候程序開發者決定何時觸發垃圾回收器也是可行的。程序開發者很可能已經理解了程序在何時將會有一段很長的空閒時間,在 QML 程序中使用了很多的 JavaScript 堆內存,在特定的性能易受影響的任務中會引起週期性的垃圾回收,所以開發者可以在不考慮用戶體驗受到影響時手動觸發垃圾回收,要調用 gc() 函數,這將帶來深層次的垃圾回收及壓縮週期,可能是幾百毫秒,也可能超過幾千毫秒,如果可能的話,還是不要手動觸發垃圾回收器。

內存與性能

在某些情況下,需要犧牲內存的消耗以換取性能的提升,如前面例子中提到的增加 JavaScript 臨時變量以緩存查詢結果的情形。有時候,增加內存消耗帶來的影響也是很嚴重的,這時候就要權衡內存與性能的影響了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章