QML 性能上的注意事項和建議

時間注意事項

做爲應用程序開發人員,咱們必須努力讓渲染引擎實現一致的 60 幀每秒刷新率。 60 FPS 意味着每一個幀之間能夠進行大約 16 毫秒的處理,其中包括將繪圖圖元上傳到圖形硬件所需的處理。javascript

實際上,這意味着應用程序開發人員應該:html

  • 儘量使用異步,事件驅動的編程
  • 使用工做線程進行重大處理
  • 永遠不要手動調整事件循環
  • 在阻塞功能中,每幀不要花費超過幾毫秒

若是不這樣作,將致使跳幀,這對用戶體驗會有很大的影響。java

** 注意 **:建立本身的 QEventLoop 或調用 QCoreApplication :: processEvents() 以免在從 QML 調用的 C++ 代碼塊中阻塞,這是一個誘人的,但不該該被使用的模式。這是很危險的,由於當在信號處理程序或綁定中輸入事件循環時,QML 引擎繼續運行着其餘綁定,動畫,轉換等。那些綁定可能會致使反作用,例如破壞包含事件循環的層次結構。linux

分析

最重要的提示是使用 Qt Creator 附帶的 QML 分析器。知道應用程序花費的時間將使咱們可以清楚地知道並專一於實際存在問題的範圍,而不是猜想可能存在問題的範圍。有關如何使用 QML 分析工具的更多信息,請參閱 Qt Creator 手冊。程序員

肯定最常運行哪些綁定,或者咱們的應用程序花費最多時間的功能,將使咱們可以決定是否須要優化問題,或從新設計應用程序的一些實現細節,以提升性能。嘗試優化代碼而不事先進行分析可能致使很是小的而不是顯着的性能改進,事倍功半。編程

JavaScript 代碼

大多數 QML 應用程序將以動態函數,信號處理程序和屬性綁定表達式的形式使用大量的 JavaScript 代碼。這一般沒有什麼問題。因爲 QML 引擎中的一些優化,例如對綁定編譯器的優化,使用 JavaScript 代碼甚至能夠(在某些用例中)比調用 C++ 函數更快。可是,必須注意確保沒必要要的處理不會被意外觸發。後端

綁定

QML 中有兩種類型的綁定:優化和非優化的綁定。最好保持綁定表達式的簡單性,由於 QML 引擎使用了一個優化的綁定表達式求值器,它能夠評估簡單的綁定表達式,而不須要切換到一個完整的 JavaScript 執行環境。與更復雜(非優化的)綁定相比,這些優化的綁定要有效得多。綁定的優化的基本要求是,在編譯時必須知道所訪問的每一個符號的類型信息。數組

** 爲了達到最好的優化效果,咱們應該在綁定表達式中避免的以下操做 **:緩存

  • 聲明中間 JavaScript 變量
  • 訪問 「var」 屬性
  • 調用 JavaScript 函數
  • 在綁定表達式中構造閉包或定義函數
  • 訪問直接求值域以外的屬性
  • 將其寫入其餘屬性,做爲反作用

綁定是最快的,當他們知道他們正在處理的對象和屬性的類型時。這意味着,在某些狀況下,綁定表達式中的非 final 屬性查找可能會比較慢,在這種狀況下,查找的屬性的類型已經被更改(例如,經過派生類型)。安全

直接求值域可歸納爲:

  • 表達式做用域對象的屬性(對於綁定表達式,這是屬性綁定所屬的對象)
  • 組件中任何對象的 ID
  • 組件中根項目的屬性

從其餘組件和任何此類對象的屬性,以及從 JavaScript 導入中定義或包含的符號,這些對象的 id 不在直接求值域內,所以訪問任何這些對象的綁定將不會被優化。

注意,若是一個綁定不能經過 QML 引擎優化的綁定表達式評估器進行優化,則必須由完整的 JavaScript 環境來評估,那麼上面列出的一些技巧將再也不適用。例如,在一個很是複雜的綁定中,在一箇中間 JavaScript 變量中緩存屬性解析的結果有時是有益的。接下來的部分將會介紹更多關於這類優化的信息。

類型轉換

使用 JavaScript 的一個主要代價是,在大多數狀況下,訪問來自 QML 類型的屬性時,將建立一個包含底層 C++ 數據(或其引用)的外部資源的 JavaScript 對象。在大多數狀況下,這是至關快捷的,但在其餘狀況下可能至關耗資源。一個很耗資源的例子就是將一個 C++ QVariantMap 屬性經過 Q_PROPERTY 宏轉換成 QML 中的 「variant」 屬性。列表序列(Lists)也可能很耗資源,可是特定類型的序列(如int、qreal、bool、QStringQUrl 的QList 序列)應該很快捷;其餘列表序列類型可能會產生高昂的轉換成本(建立一個新的 JavaScript 數組,一個一個地添加新類型,從 C++ 類型實例轉換爲 JavaScript 值)。

某些基本屬性類型(如「string」和「url」屬性)之間的轉換也可能很耗資源。使用最接近的匹配屬性類型將避免沒必要要的轉換。

若是您必須向 QML 引入 QVariantMap ,使用 「var」 屬性而不是 「variant」 屬性可能會更好一些。通常來講,對於 QtQuick 2.0 和更新版本的每一個用例,「property var」 應被視爲優於 「property variant」 (請注意,「property variant」 被標記爲已過期),由於它容許存儲真正的 JavaScript 引用(這能夠減小某些表達式中須要的轉換次數)。

解析屬性

屬性解析須要消耗時間。在某些狀況下,若是可能的話咱們能夠將查找的結果緩存和重用,以免作沒必要要的工做。在下面的例子中,咱們有一個常常運行的代碼塊(在這種狀況下,它是一個顯式循環的內容;可是它能夠是一個常常評估的綁定表達式),在這個例子中,咱們解析了對象「rect」id及其「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");
    }
}

咱們能夠在代碼塊中解析共同的屬性:

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

onCompleted 處理程序中的循環使得 「text」 屬性綁定從新計算了六次(而後致使依賴於 text 屬性的任何其餘屬性綁定以及 onTextChanged 信號處理程序每次都會被從新評估,而且每次更新文本顯示)。 在這種狀況下,這顯然是沒必要要的,由於咱們真的只關心積加的最終結果。

它能夠被改寫以下:

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

序列提示(Sequence tips)

如前所述,一些序列類型很快(例如 QList<int>, QList<qreal>, QList<bool>, QList<QString>, QStringList 和 QList<QUrl>),而有些則慢一些。除了使用較快的類型以外,還有其餘一些與性能相關的語義,您須要瞭解這些語義,以達到最佳的性能。

首先,序列類型有兩種不一樣的實現方式:一個是序列是 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 對象

不一樣的 JavaScript 引擎提供不一樣的優化。Qt Quick 2 使用的 JavaScript 引擎針對對象實例化和屬性查找進行了優化,但它提供的優化依賴於某些標準。若是您的應用程序不符合標準,則 JavaScript 引擎將恢復到「慢路」模式,性能更差。 所以,始終儘可能確保符合如下條件:

  • 儘可能避免使用 eval()
  • 不要刪除對象的屬性

通用接口元素

文本元素

計算文本佈局多是一個緩慢的操做。考慮儘量使用 PlainText 格式而不是 StyledText,由於這會減小布局引擎所需的工做量。若是咱們不能使用 PlainText (由於咱們須要嵌入圖像,或者使用標記來指定字符的範圍以具備某種格式(粗體、斜體等等),而不是整個文本) 則應使用 StyledText。

若是文本可能(但可能不是)StyledText,則應該僅使用 AutoText,由於 StyledText 模式將致使解析成本。不該使用 RichText 模式,由於 StyledText 幾乎能夠提供幾乎全部功能,並消耗更少的成本。

圖片

圖像是任何用戶界面的重要組成部分。不幸的是,因爲加載它們的時間、消耗的內存和使用的方式,它們也是性能問題的一個主要來源。

異步加載圖片

圖像一般至關大,因此明智的作法是確保加載圖像不會阻塞 UI 線程。將 QML Image 元素的 「asynchronous」 屬性設置爲true以啓用從本地文件系統異步加載圖像(遠程圖像老是異步加載),這不會對用戶界面的美觀產生負面影響。

將 「asynchronous」 屬性設置爲 true 的 Image 元素將在低優先級的工做線程中加載圖像。

顯示設置 SourceSize 屬性值

若是咱們的應用程序將加載大型圖像但將其顯示在一個小尺寸的元素中,請將 「sourceSize」 屬性設置爲要渲染的元素的大小,以確保圖像的較小縮放版本保存在內存中,而不是較大的那個。

請注意,更改 sourceSize 將致使從新加載圖像。

避免運行時的組合

還要記住,您能夠經過在應用程序中提供預先組合的圖像資源(例如,提供具備陰影效果的元素)來避免在運行時執行構圖工做。

使用錨點定位元素

使用錨點而不是相對於彼此綁定來定位元素的效率更高。考慮使用綁定來定位 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
}

使用錨點能夠更有效地實現:

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、寬度和高度屬性。Item 座標老是相對於它們的父類,因此若是咱們想要從 Item 父母的 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++ 模型

在 C++ 中編寫咱們本身的自定義模型,以便在 QML 中的視圖中使用。儘管任何此類模型的最佳實現都將嚴重依賴於它必須實現的用例,但一些通常的指導方針以下:

  • 儘量異步
  • 在(低優先級)工做線程中執行全部處理
  • 批量後端操做,以使(潛在的緩慢)I/O 和 IPC 最小化
  • 使用滑動切片窗口緩存結果,其參數經過分析來肯定

值得注意的是,建議使用低優先級的工做線程來最小化 GUI 線程的性能風險(這可能會致使更糟糕的性能)。另外,請記住,同步和鎖定機制多是性能降低的重要緣由,所以應注意避免沒必要要的鎖定。

QML 的 ListModel 類型

QML 提供了一個 ListModel 類型,可用於將數據提供給 ListView。只要正確使用,大多數使用狀況就足夠了,並且性能相對較高。

在工做線程中填充數據

ListModel 能夠在 JavaScript 中的(低優先級)工做線程中進行數據的填充。開發人員必須在WorkerScript 中顯式調用 ListModel 上的 「sync()」,以使更改與主線程同步。有關更多信息,請參閱 WorkerScript 的文檔。

請注意,使用 WorkerScript 元素將致使建立單獨的 JavaScript 引擎線程(由於JavaScript引擎是在單獨的線程中的)。 這將致使內存使用量的增長。然而,多個 WorkerScript 元素都將使用相同的工做線程,所以一旦應用程序已經使用了一個,則使用第二個或第三個 WorkerScript 元素的內存影響能夠忽略不計。

不要使用動態角色

QtQuick 2 中的 ListModel 元素比 QtQuick 1 中的性能要好得多。性能改進主要來自對給定模型中每一個元素中的角色類型的假設 —— 若是類型不改變,則緩存性能顯着提升。若是類型能夠從元素到元素動態變化,則這種優化變得不可能,而且模型的性能將會更差一個數量級。

所以,動態類型在缺省狀況下是禁用的;開發人員必須主動設置模型的boolean 類型 「dynamicRoles」 屬性值爲 true,以啓用動態類型(並忍受伴隨的性能降低)。若是能夠從新設計應用程序以免使用,咱們建議不要使用動態類型。

視圖

視圖委託(delegate)應該儘量地保持簡單。在委託中應剛剛有足夠的 QML 來顯示必要的信息便可。 任何不是當即須要的附加功能(例如,若是在點擊時顯示更多信息)不該該被當即建立(請參見即將後文的延遲初始化部分)。

如下列表是設計委託時要牢記的事項的一個很好的總結:

  • 委託中的元素越少,能夠建立得越快,所以能夠滾動視圖的速度越快。
  • 將委託中的綁定數保持最小; 特別地,使用錨而不是綁定以在委託中進行相對定位。
  • 避免在委託中使用 ShaderEffect 元素。
  • 切勿在委託中啓用 clip 屬性。

咱們能夠設置視圖的 cacheBuffer 屬性,以容許在可見區域以外異步建立和緩衝代理。對於不重要且不太可能在單個框架內建立的視圖代理,建議使用 cacheBuffer。請注意,cacheBuffer 會在內存中保留額外的代理,所以使用 cacheBuffer 導出的值必須與額外的內存使用相平衡。開發人員應該使用基準測試來找出用例的最佳值,由於在極少數狀況下,使用 cacheBuffer 引發的內存壓力增長會致使滾動時的幀速率下降。

視覺效果

Qt Quick 2 包括幾個功能,容許開發人員和設計人員建立很是吸引人的用戶界面。流動性和動態轉換以及視覺效果能夠在應用程序中發揮很大的做用,可是在使用QML中的一些特性時,必定要注意它們的性能影響。

動畫

通常來講,動畫化屬性將致使引用該屬性的任何綁定被從新評估。 一般,這是指望的,但在其餘狀況下,最好在執行動畫以前禁用綁定,而後在動畫完成後從新分配綁定。

避免在動畫過程當中運行 JavaScript。例如,應避免爲 x 屬性動畫的每一個幀運行復雜的 JavaScript 表達式。

開發人員應該特別注意使用腳本動畫,由於它們在主線程中運行(所以若是花費的時間太長,可能會致使跳幀)。

粒子

Qt Quick Particles 模塊容許將美麗的粒子效果無縫集成到用戶界面中。然而,每一個平臺都具備不一樣的圖形硬件功能,而粒子模塊沒法將參數限制在硬件能夠正常支持的範圍內。您嘗試渲染的粒子越多(它們越大),圖形硬件須要以60 FPS呈現的速度越快。影響更多的粒子須要更快的CPU。所以,重要的是仔細測試目標平臺上的全部粒子效應,以校準能夠以60 FPS渲染的粒子的數量和大小。

應當注意的是,在不使用時(例如,在不可見元素上)能夠禁用粒子系統,以免進行沒必要要的模擬。

有關更多詳細信息,請參閱 「粒子系統性能指南」。

控制元素生命週期

經過將應用程序分爲簡單的模塊化組件,每一個組件都包含在單個 QML 文件中,咱們能夠實現更快的應用程序啓動時間,更好地控制內存使用狀況,並減小應用程序中活動但不可見元素的數量。

延遲初始化

QML引擎作了一些棘手的事情,以確保組件的加載和初始化不會致使跳幀。然而,除了不去作你不須要作的工做,而且把工做推遲到有必要的時候進行,沒有更好的方法來減小啓動時間。這能夠經過使用 Loader 或動態(dynamically)建立組件來實現。

使用 Loader

加載器(Loader)是一個容許組件動態加載和卸載的元素。

  • 使用 Loader 的 「active」 屬性,能夠延遲初始化,直到須要。
  • 使用重載版本的 「setSource()」 函數,能夠提供初始屬性值。
  • 將加載器(Loader)異步(asynchronous)屬性設置爲 true 也能夠在組件實例化時提升流動性。

使用動態建立

開發人員可使用 Qt.createComponent() 函數在 JavaScript 中在運行時動態建立一個組件,而後調用 createObject() 來實例化它。根據調用中指定的全部權語義,開發人員可能必須手動刪除所建立的對象。請參閱 JavaScript 中的動態建立 QML 對象,以得到更多信息。

銷燬再也不使用元素

因爲它們是不可見元素的子元素(例如,標籤窗口小部件中的第二個選項卡,當前正在顯示第一個選項卡),所以在大多數狀況下應該被延遲初始化,並在再也不使用時被刪除 ,以免使其活動的持續成本(例如,渲染,動畫,財產綁定評估等)。

加載加載器元素的項能夠經過從新設置加載器的 「source」 或 「sourceComponent」 屬性來釋放,而其餘項目能夠經過調用 destroy() 來顯式地釋放。在某些狀況下,可能有必要讓項目處於活動狀態,在這種狀況下,它至少應該是不可見(invisible)的。

有關活動但不可見的元素的更多信息,請參閱後續的「渲染」部分的內容。

渲染

用於在 QtQuick 2 中渲染的場景圖容許高度動態的動畫用戶界面以 60 FPS 流暢呈現。然而,有一些東西可能極大地下降渲染性能,而開發人員應該當心地避免這些陷阱。

剪裁

默認狀況下禁用了剪裁,只在須要時才啓用。

剪裁是一種視覺效果,而不是一種優化。它增長(而不是減小)渲染器的複雜性。若是啓用了剪裁,一個項目將會把它本身的繪畫,以及它的孩子的繪畫,剪切到它的邊界矩形中。這將使渲染器沒法自由地從新排列元素的繪製順序,從而致使次優的最佳狀況場景圖遍歷。

在委託中進行剪切是很是糟糕的,應該不惜一切代價避免。

超繪和不可見元素

若是咱們有其餘(不透明)元素徹底覆蓋的元素,最好將其 「visible」 屬性設置爲false,不然將被無謂地繪製。

相似地,不可見的元素(例如,當前正在顯示第一個選項卡的選項卡小部件中的第二個選項卡),但須要在啓動時初始化(例如,若是實例化第二個選項卡的成本花費的時間太長時,纔可以作到選項卡被激活),應該將其「visible」屬性設置爲false,以免繪製它們的成本(雖然如前所說,他們仍將承擔任何動畫或綁定的成本評估,由於他們仍然活躍)。

半透明vs不透明

不透明的內容一般比半透明的要快得多。緣由在於半透明的內容須要混合,並且渲染器能夠更好地優化不透明的內容。

具備一個半透明像素的圖像被視爲徹底半透明的,儘管它大可能是不透明的。 對於具備透明邊框的 BorderImage 也是如此。

着色器

ShaderEffect 類型使咱們能夠在開銷很小的狀況下將 GLSL 代碼內聯到 Qt Quick 應用程序中。然而,重要的是要意識到片斷程序須要爲渲染形狀中的每一個像素運行。當部署到低端硬件時,着色器覆蓋了大量的像素,應該將片斷着色器保存到一些指令中,以免性能降低。

用 GLSL 編寫的着色器容許編寫複雜的轉換和視覺效果,但應謹慎使用。使用 ShaderEffectSource 會將場景預渲染到 FBO 中,而後繪製。 這種額外的開銷可能至關昂貴。

內存分配和收集

應用程序分配的內存數量和內存分配的方式是很是重要的考慮事項。除了對內存受限設備的內存不足的明顯擔心外,在堆上分配內存是一個至關昂貴的操做,並且某些分配策略可能致使跨頁面的數據碎片化。JavaScript使用一個託管的內存堆,它自動地收集垃圾,這提供了一些優點,但也有一些重要的含義。

用 QML 編寫的應用程序能夠即便用 C++ 堆也使用自動管理的 JavaScript 堆中的內存。 應用程序開發人員須要瞭解每一個應用程序的細微之處,以便最大限度地提升性能。

給 QML 應用程序開發人員的提示

本節中提供的技巧和建議僅供參考,可能不適用於全部狀況。確保使用經驗指標仔細地對咱們的應用進行基準和分析,以便作出最佳決策。

延遲實例化和初始化組件

若是咱們的應用程序包含多個視圖(例如,多個選項卡),可是在任什麼時候候只須要一個視圖,則可使用延遲實例化來最小化在任何給定時間分配的內存量。有關詳細信息,請參閱上一節「** 延遲初始化 **」部分。

銷燬再也不使用的對象

若是咱們延遲實例化組件,或者在 JavaScript 表達式中動態建立對象,最好是手動 destroy() 而不是等待自動垃圾收集來完成。有關更多信息,請參閱上文控制元素生命週期的章節。

不要手動調用垃圾收集器

在大多數狀況下,手動調用垃圾收集器是不明智的,由於它會在至關長的一段時間內阻塞 GUI 線程。這可能致使跳幀和不穩定的動畫,這些都應該不惜一切代價避免。

有些狀況下,手動調用垃圾收集器是能夠接受的(這在接下來的部分中會更詳細地解釋),可是在大多數狀況下,調用垃圾收集器是沒必要要的,而且會產生相反的效果。

避免複雜的綁定

除了複雜綁定的性能下降(例如,因爲必須輸入 JavaScrip t執行上下文來執行評估),它們還在 C++ 堆和 JavaScript 堆上佔用了比綁定能夠由 QML 優化的綁定表達式進行評估的求值器更多的內存。

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

若是 QML 元素有在 QML 中定義的自定義屬性,那麼它就變成了它本身的隱式類型。這將在接下來的部分中更詳細地解釋。若是在一個組件內定義了多個相同的隱式類型,那麼一些內存就會被浪費掉。在這種狀況下,最好是顯式地定義一個新組件,而後能夠重用該組件。

定義自定義屬性一般能夠是有益的性能優化(例如,減小所需或從新評估的綁定數量),或者能夠提升組件的模塊性和可維護性。 在這些狀況下,鼓勵使用自定義屬性。 可是,若是新類型被屢次使用,則應將其定義爲本身的組件(.qml文件),以節省內存。

複用現有組件

若是您正在考慮定義新的組件,那麼值得加倍檢查的是,咱們的平臺的組件集中是否不存在此類組件。不然,咱們將迫使 QML 引擎生成和存儲類型數據,而類型數據實質上與另外一個預先存在的而且可能已經加載的組件重複。

使用單例類型(singleton types)而不是編譯庫(pragma library)腳本

若是您正在使用一個編譯庫(pragma library)腳原本存儲應用程序範圍的實例數據,那麼能夠考慮使用 QObject 單例類型(singleton types)。這將帶來更好的性能,並將帶來使用的 JavaScript 堆內存減小。

QML 應用程序中的內存分配

QML 應用程序的內存使用可能會分爲兩部分:即 C++ 堆的使用和 JavaScript 堆的使用。在每一個部分中分配的一些內存是不可避免的,由於它由 QML 引擎或 JavaScript 引擎分配,而其他的則取決於應用程序開發人員作出的決定。

C++ 堆將包含:

  • QML 引擎的固定和不可避免的開銷(實現數據結構,上下文信息等)
  • 每一個組件編譯的數據和類型信息,包括每一個類型的屬性元數據,它由 QML 引擎生成,取決於應用程序導入哪些模塊以及應用程序加載哪些組件
  • 每一個對象 C++ 數據(包括屬性值)加上每一個元素元對象層次結構,這取決於應用程序實例化的組件
  • 任何由 QML 導入(庫)專門分配的數據

JavaScript 堆將包含:

  • JavaScript 引擎自己的固定和不可避免的開銷(包括內置的 JavaScript 類型)
  • 咱們的 JavaScript 集成的固定和不可避免的開銷(用於加載類型,功能模板等的構造函數)
  • 在運行時,由 JavaScript 引擎在運行時生成的每種類型的佈局信息和其餘內部類型數據(請參閱下面的關於類型的說明)
  • 每一個對象的 JavaScript 數據(「var」 屬性,JavaScript 函數和信號處理程序以及未優化的綁定表達式)
  • 表達式評估期間分配的變量

此外,在主線程中將分配一個用於使用的 JavaScript 堆,並可選地分配一個用於 WorkerScript 線程的其餘 JavaScript 堆。若是應用程序不使用 WorkerScript 元素,那麼就不會產生該開銷。JavaScript 堆的大小多是幾兆字節,所以爲內存受限的設備編寫的應用程序最好避免使用 WorkerScript 元素,儘管它在異步填充 list 模型方面頗有用。

請注意,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 引擎垃圾收集的影響。潛在的解決方案應該仔細地進行基準測試,以確保選出最好的解決方案。

沒有一套通用的指導方針能夠取代對計算機科學的基本原理的深刻理解,結合應用程序開發人員正在開發的平臺的實現細節的實際知識。此外,在作出權衡決策時,沒有多少理論計算能夠替代一組良好的基準和分析工具。

碎片

碎片是一個 C++ 開發問題。若是應用程序開發人員沒有定義任何 C++ 類型或插件,他們可能會安全地忽略此部分。

隨着時間的推移,應用程序將分配大量內存,將數據寫入到該內存中,並在使用完某些數據以後,將部份內存釋放出來。這可能致使「空閒」內存位於非連續的塊中,這些塊不能返回給其餘應用程序使用的操做系統。它還對應用程序的緩存和訪問特性產生了影響,由於「活着」的數據可能分佈在許多不一樣的物理內存頁上。這反過來可能會迫使操做系統進行交換,這可能致使文件系統 I/O —— 相對而言,這是一個極其緩慢的操做。

能夠經過使用池分配器(和其餘連續的內存分配程序)來避免碎片化,能夠經過減小任什麼時候間分配的內存量,經過仔細管理對象的生命週期、按期清理和重建緩存、或者利用內存管理運行時使用垃圾收集(好比JavaScript)來減小內存的數量。

垃圾收集

JavaScript 提供垃圾回收。在 JavaScript 堆上分配的內存(而不是 C++ 堆)由 JavaScript 引擎所擁有。引擎將按期收集 JavaScript 堆上的全部未引用的數據。

垃圾收集的影響

垃圾收集有其優勢和缺點。這意味着手動管理對象的生命週期不那麼重要。 可是,這也意味着JavaScript引擎可能會在應用程序開發人員控制以外啓動一個潛在的持久性操做。除非應用程序開發人員仔細考慮 JavaScript 堆使用狀況,不然垃圾收集的頻率和持續時間可能會對應用程序體驗產生負面影響。

手動調用垃圾收集器

在 QML 中編寫的應用程序(極可能)須要在某個階段執行垃圾收集。當可用的空閒內存數量較低時,JavaScript 引擎會自動觸發垃圾收集,但若是應用程序開發人員決定什麼時候手動調用垃圾收集器(一般狀況並不是如此),則偶爾會更好一些。

應用程序開發人員可能最瞭解應用程序什麼時候空閒一段至關長的時間。若是 QML 應用程序使用了大量的 JavaScript 堆內存,那麼在特別對性能敏感的任務(例如列表滾動,動畫等)期間會致使按期的和破壞性的垃圾收集週期,那麼應用程序開發人員可能會很好地手動調用垃圾收集器在零活動期間。 空閒週期是執行垃圾收集的理想選擇,由於用戶不會注意到在活動發生時調用垃圾回收器會致使用戶體驗(跳幀,抖動動畫等)的任何降級。

垃圾收集器能夠經過在 JavaScript 內調用 gc() 來手動調用。這將致使執行一個完整的收集週期,它可能從幾百毫秒到超過 1000 毫秒的時間完成,所以,若是可能的話,應該儘可能避免。

內存與性能的權衡

在某些狀況下,爲了減小處理時間而增長內存使用量是可行的。例如,將一個符號查找的結果緩存到一個 JavaScript 表達式的臨時變量中,在評估該表達式時將會獲得顯著的性能提高,但它涉及分配一個臨時變量。在某些狀況下,這些權衡是明智的(好比上面的例子,這幾乎老是明智的),可是在其餘狀況下,爲了不增長系統的內存壓力,最好容許處理稍微長一點的時間。

在某些狀況下,增長的內存使用的影響多是極端的。在某些狀況下,將內存使用用於假定的性能增益,可能會致使增長的頁面或緩存,從而致使性能的大幅下降。老是有必要仔細評估權衡的影響,以肯定在給定狀況下哪一個解決方案最好。

有關緩存性能和內存時間權衡的深刻信息,請參閱Ulrich Drepper的優秀文章「每一個程序員都應該瞭解的內存」(可從訪問 http://ftp.linux.org.ua/pub/docs/developer/general/cpumemory.pdf 截至2012年4月18日),以及有關 C++ 特定優化的信息,請參閱 Agner Fog 的「優化 C++ 應用程序的優秀手冊」(可從訪問http://www.agner.org/optimize/ 截至2012年4月18日)。

本文歡迎轉載,可是請註明出處。

本文參考連接:http://doc.qt.io/qt-5/qtquick-performance.html#tips-for-qml-application-developers

相關文章
相關標籤/搜索