1. 程式人生 > >QML 效能上的注意事項和建議

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

時間注意事項

作為應用程式開發人員,我們必須努力讓渲染引擎實現一致的 60 幀每秒重新整理率。 60 FPS 意味著每個幀之間可以進行大約 16 毫秒的處理,其中包括將繪圖圖元上傳到圖形硬體所需的處理。

實際上,這意味著應用程式開發人員應該:

  • 儘可能使用非同步,事件驅動的程式設計
  • 使用工作執行緒進行重大處理
  • 永遠不要手動調整事件迴圈
  • 在阻塞功能中,每幀不要花費超過幾毫秒

如果不這樣做,將導致跳幀,這對使用者體驗會有很大的影響。

注意 :建立自己的 QEventLoop 或呼叫 QCoreApplication :: processEvents() 以避免在從 QML 呼叫的 C++ 程式碼塊中阻塞,這是一個誘人的,但不應該被使用的模式。這是很危險的,因為當在訊號處理程式或繫結中輸入事件迴圈時,QML 引擎繼續執行著其他繫結,動畫,轉換等。那些繫結可能會導致副作用,例如破壞包含事件迴圈的層次結構。

分析

最重要的提示是使用 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、QStringQUrlQList 序列)應該很快捷;其他列表序列型別可能會產生高昂的轉換成本(建立一個新的 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 來顯示必要的資訊即可。 任何不是立即需要的附加功能(例如,如果在點選時顯示更多資訊)不應該被立即建立(請參見即將後文的延遲初始化部分)。

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

  • 委託中的元素越少,可以建立得越快,因此可以滾動檢視的速度越快。
  • 將委託中的繫結數保持最小; 特別地,使用錨而不是繫結以在委託中進行相對定位。
  • 切勿在委託中啟用 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 表示式的臨時變數中,在評估該表示式時將會得到顯著的效能提升,但它涉及分配一個臨時變數。在某些情況下,這些權衡是明智的(比如上面的例子,這幾乎總是明智的),但是在其他情況下,為了避免增加系統的記憶體壓力,最好允許處理稍微長一點的時間。

在某些情況下,增加的記憶體使用的影響可能是極端的。在某些情況下,將記憶體使用用於假定的效能增益,可能會導致增加的頁面或快取,從而導致效能的大幅降低。總是有必要仔細評估權衡的影響,以確定在給定情況下哪個解決方案最好。

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

相關推薦

QML 效能注意事項建議

時間注意事項 作為應用程式開發人員,我們必須努力讓渲染引擎實現一致的 60 幀每秒重新整理率。 60 FPS 意味著每個幀之間可以進行大約 16 毫秒的處理,其中包括將繪圖圖元上傳到圖形硬體所需的處理。 實際上,這意味著應用程式開發人員應該: 儘可能使用非同步,事件驅動的程式設計使用工作執行緒進行重大處理永

關於Visual Studio 2013 配置OpenCV 的一些注意事項執行問題

1.在visual studio上配置opencv的依賴項和執行庫. 1.開啟Vs,檔案->新建->專案 2. visual c++ -> Win32控制檯應用程式->確定 (劃線的內容可以根據自己習慣更改) 3.直接下一步 4.選中空專案這個選項,然

Python注意事項誤區

Python是一種解釋性、面向物件並具有動態語義的高階程式語言。它內建了高階的資料結構,結合了動態型別和動態繫結的優點,這使得它在快速應用開發中非常有吸引力,並且可作為指令碼或膠水語言來連線現有的元件或服務。Python支援模組和包,從而鼓勵了程式的模組化和程式碼重用。 關於這篇文章

Windows 的java客戶端實現傳檔案到Linux的Hadoop叢集(注意ip埠是否一致)

我這幾天一直在學大資料,處於入門階段,然後老師的視訊中教學有用windows的java客戶端上傳檔案到Linux的Hadoop叢集, 但是這邊出BUG了一直上傳不上去,執行程式後一直沒反應。。。。弄了幾天(雖然這幾天在做前端專案~~~) 然後問群裡的大佬,他們說應該是ip和埠

GridView在電視注意事項

1、android:focusable="true" <GridView android:id="@+id/gv_device" android:layout_width="match_parent" android:la

安裝Linux 18.04作為第二系統時的注意事項解決方案(第一系統是windows10 )

搞了很久才把Unbuntu 18.04安裝成功,為了防止以後再遇到這些問題,記下來。  我的筆記本時MSI的。 UEFI bios面板, 硬碟分頁方式是GPT。  如何檢查自己的電腦時UEFI還是Legacy BIOS Type win+R, input msinfo

redis的使用注意事項問題總結

redis單機版注意和問題: 1、redis使用命令列操作時,查詢結果中的中文會顯示為16進位制的字串,解決方案: 使用命令 redis-cli –raw就能正常顯示中文,如下圖所示: redis

序列化的一些注意事項建議

本文來自《改善java的151個建議》 建議11:養成良好習慣,顯示宣告UID 我們先寫一個序列化與反序列化的工具類SerilizationUtils public class SerializationUtils { private static Strin

Android 整合融雲注意事項使用教程

在這裡我使用的融雲集成的單人音視訊通話,其實套路都是一樣的,在這裡我給大家介紹一下,整合遇到的問題。 一.在myapplication初始化的時候找不到嵌入modle或者匯入依賴時的依賴包,或者是匯入了modle,modle下又有依賴,但是匯入依賴有的方法找不到,問題有

Laravel 5.5 注意事項常見問題

config/app.php 配置檔案修改 config/app.php 中的時區 timezone 配置:'timezone' => 'Asia/Shanghai',請將時區修改為你當前所在的時區。.env 環境變數配置檔案該檔案非常重要,裡面儲存著和開發環境相關的變

QML跨平臺佈局注意事項

QML跨平臺佈局注意事項 1、本部落格解決的問題:羅列出qml程式在跨平臺下,各個元件之間的佈局使用事項,僅在windows和linux上進行測試,mac和安卓暫時未進行測試。 2、問題前言:在使用qml進行誇平臺開發的時候,往往會出現很多意料之外的問題,最大的一個問題就是各個元

雷塞DMC3600運動控制注意事項解決辦法

(1)使用步長移動的方法很容易和相機的取圖相互衝突。 因此,將步長移動換為按照速度移動則不會有問題。 (2)運動完成之後返回運動到位訊號 在程式中是使用while迴圈來監聽軸是否在動來判斷有沒有到

Mybatis中SQL效能優化注意事項

Mybatis SQL效能調優 1.  Mapper層引數為Map,由Service層負責過載     Mapper由於機制的問題,不能過載,引數一般設定成Map,但這樣會使引數變得模糊,如果想要使程式碼變得清晰,可以通過service層來實現過載的目的,對外提

Mybatis的使用注意事項出錯的一些總結

為什麼會出現這種原因:1.我設定的引數型別是map型別的,結果我傳遞的時候,以為需要一個引數,就傳遞了一個字串;(我按照字串的方式進行傳遞,就穿了一個字串,實際需要map) <select id="find_count" parameterType="map

字串匹配的Sunday演算法--效能超過KMPBM演算法

第一次聽到Sunday演算法,是大餅餅說的。在他圖文並茂的解釋中,我發現這個演算法果然是一個又容易理解,效率又強過kmp和BM的演算法。 Sunday的移動次數更少! 於是試著寫了一個,果真是好東東,分享一下。 轉一些概念先: Sunday演算法是Daniel M.Sun

python 正則表示式注意事項re.match()re.search()區別

首先,正則我們一般用到re.match()和re.search() 其中re.match()是從開始進行匹配的,re.search()是從中間開始匹配. 另外關於懶惰匹配的問題,需要懶惰的地方加"?

InnoDB索引實現原理以及注意建議

一、InnoDB實現原理 雖然InnoDB也使用B+Tree作為索引結構,但具體實現方式卻與MyISAM截然不同。因為InnoDB支援聚簇索引(主鍵索引),聚簇索引就是表,所以InnoDB不用像MyISAM那樣需要獨立的行儲存。也就是說,InnoDB的資料檔案本身就是索引檔案。 聚簇索引的每一個葉子節點都包含

最全面的EventBus 3.1的使用教程及官方推薦的結合訂閱者索引processor顯著提升效能實際專案中的使用注意事項

需求場景 無論是現在的專案還是以前的專案中,都會遇見執行緒之間通訊,元件之間通訊的需求,我們知道這些需求都可以使用EventBus來處理,為了對比體現出EventBus使用的方便簡潔,我們先來回顧下在EventBus出現以前我們是怎麼處理執行緒間通訊和元件間通訊的。 1,執行緒間通訊

檔案的下載—傳的實現,注意事項

 實現WEB開發中的檔案上傳功能,需完成如下二步操作: 在WEB頁面中新增上傳輸入項,<input type=“life” name=“”>,使用時注意: 1. 必須要設定input輸入項的name屬性,否則瀏覽器將不會發送上傳檔案的資料。 2. 必須把i

關於在真實物理機器用cloudermanger或ambari搭建大資料叢集注意事項總結、經驗感悟心得(圖文詳解)

寫在前面的話   (1) 最近一段時間,因擔任我團隊實驗室的大資料環境叢集真實物理機器工作,至此,本人秉持負責、認真和細心的態度,先分別在虛擬機器上模擬搭建ambari(基於CentOS6.5版本)和cloudermanager(基於CentOS6.5或Ubuntu14.04版本)。   (2)  大