Leistungsüberlegungen und Vorschläge
Überlegungen zum Timing
Als Anwendungsentwickler streben Sie in der Regel an, dass die Rendering-Engine eine konstante Aktualisierungsrate von 60 Bildern pro Sekunde erreicht. Abhängig von Ihrer Hardware und Ihren Anforderungen kann diese Zahl unterschiedlich sein, aber 60 FPS sind sehr üblich. 60 FPS bedeutet, dass zwischen jedem Bild etwa 16 Millisekunden liegen, in denen die Verarbeitung stattfinden kann, einschließlich der Verarbeitung, die zum Hochladen der Zeichenprimitive in die Grafikhardware erforderlich ist.
In der Praxis bedeutet dies, dass der Anwendungsentwickler:
- wann immer möglich asynchrone, ereignisgesteuerte Programmierung verwenden
- Worker-Threads für wichtige Verarbeitungsvorgänge verwenden
- die Ereignisschleife niemals manuell drehen
- nie mehr als ein paar Millisekunden pro Frame in blockierenden Funktionen verbringen
Andernfalls kommt es zu übersprungenen Frames, was sich drastisch auf das Benutzererlebnis auswirkt.
Hinweis: Ein verlockendes Muster, das jedoch niemals verwendet werden sollte, ist die Erstellung eines eigenen QEventLoop oder der Aufruf von QCoreApplication::processEvents(), um das Blockieren innerhalb eines von QML aufgerufenen C++-Codeblocks zu vermeiden. Dies ist gefährlich, denn wenn eine Ereignisschleife in einem Signalhandler oder einer Bindung eingegeben wird, fährt die QML-Engine fort, andere Bindungen, Animationen, Übergänge usw. auszuführen. Diese Bindungen können dann Seiteneffekte verursachen, die z. B. die Hierarchie, die Ihre Ereignisschleife enthält, zerstören.
Profiling
Der wichtigste Tipp ist: Verwenden Sie die QML Profiler die in Qt Creator enthalten ist. Wenn Sie wissen, wie viel Zeit in einer Anwendung verbracht wird, können Sie sich auf tatsächlich vorhandene Problembereiche konzentrieren, anstatt auf solche, die potenziell vorhanden sind. Siehe Qt Creator: Profiling QML Applications für weitere Informationen.
Wenn Sie feststellen, welche Bindungen am häufigsten ausgeführt werden oder in welchen Funktionen Ihre Anwendung die meiste Zeit verbringt, können Sie entscheiden, ob Sie die Problembereiche optimieren oder einige Implementierungsdetails Ihrer Anwendung neu gestalten müssen, um die Leistung zu verbessern. Der Versuch, den Code ohne Profiling zu optimieren, wird wahrscheinlich eher zu sehr geringen als zu erheblichen Leistungsverbesserungen führen.
JavaScript-Code
Die meisten QML-Anwendungen enthalten JavaScript-Code in Form von Property-Binding-Ausdrücken, Funktionen und Signal-Handlern. Dies stellt im Allgemeinen kein Problem dar. Dank fortschrittlicher Werkzeuge wie dem Qt Quick Compiler können einfache Funktionen und Bindungen sehr schnell sein. Es muss jedoch darauf geachtet werden, dass nicht versehentlich eine unnötige Verarbeitung ausgelöst wird. Der QML Profiler kann zahlreiche Details über die Ausführung von JavaScript und deren Auslöser anzeigen.
Typumwandlung
Ein großer Nachteil der Verwendung von JavaScript besteht darin, dass in einigen Fällen beim Zugriff auf eine Eigenschaft eines QML-Typs ein JavaScript-Objekt mit einer externen Ressource erstellt wird, die die zugrunde liegenden C++-Daten (oder einen Verweis darauf) enthält. In den meisten Fällen ist dies recht kostengünstig, in anderen Fällen kann es jedoch recht teuer werden. Vorsicht ist geboten, wenn es um große und komplizierte Wertetypen oder Sequenztypen geht. Diese müssen von der QML-Engine jedes Mal kopiert werden, wenn Sie sie an Ort und Stelle ändern oder sie einer anderen Eigenschaft zuweisen. Wenn dies zu einem Engpass wird, sollten Sie stattdessen Objekttypen verwenden. Listen von Objekttypen haben nicht das gleiche Problem wie Listen von Werttypen, da Listen von Objekttypen mit QQmlListProperty implementiert sind.
Die meisten Konvertierungen zwischen einfachen Werttypen sind billig. Es gibt jedoch Ausnahmen. Das Erstellen einer url aus einer Zeichenkette kann das Konstruieren einer QUrl Instanz beinhalten, was kostspielig ist.
Auflösen von Eigenschaften
Die Auflösung von Eigenschaften braucht Zeit. Obwohl Lookups in der Regel so optimiert werden, dass sie bei nachfolgenden Ausführungen viel schneller ablaufen, ist es immer am besten, unnötige Arbeit ganz zu vermeiden, wenn dies möglich ist.
Im folgenden Beispiel haben wir einen Codeblock, der häufig ausgeführt wird (in diesem Fall handelt es sich um den Inhalt einer expliziten Schleife, aber es könnte z. B. auch ein häufig ausgewerteter Bindungsausdruck sein) und in dem wir das Objekt mit der ID "rect" und seiner Eigenschaft "color" mehrfach auflösen:
// bad.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which: string, value: real) { 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"); } }
Jedes Mal, wenn rect.color abgerufen wird, muss die QML-Engine:
- Einen Value Type Wrapper auf dem JavaScript Heap zuweisen.
- Den Getter der Eigenschaft
colorvon Rectangle ausführen. - Kopieren Sie die resultierende QColor in den Value Type Wrapper.
Wir müssen dies nicht 4 Mal tun. Stattdessen können wir die gemeinsame Basis nur einmal in dem Block auflösen:
// good.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which: string, value: real) { 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"); } }
Allein diese einfache Änderung führt zu einer erheblichen Leistungsverbesserung. Beachten Sie, dass der obige Code noch weiter verbessert werden kann (da sich die nachgeschlagene Eigenschaft während der Schleifenverarbeitung nie ändert), indem die Eigenschaftsauflösung wie folgt aus der Schleife herausgenommen wird:
// better.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which: string, value: real) { 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"); } }
Eigenschaftsbindungen
Ein Ausdruck für eine Eigenschaftsbindung wird neu ausgewertet, wenn eine der Eigenschaften, auf die er verweist, geändert wird. Aus diesem Grund sollten Bindungsausdrücke so einfach wie möglich gehalten werden.
Wenn Sie eine Schleife haben, in der Sie einige Verarbeitungen vornehmen, aber nur das Endergebnis der Verarbeitung wichtig ist, ist es oft besser, einen temporären Akkumulator zu aktualisieren, den Sie anschließend der zu aktualisierenden Eigenschaft zuweisen, anstatt die Eigenschaft selbst inkrementell zu aktualisieren, um zu vermeiden, dass die Bindungsausdrücke während der Zwischenphasen der Akkumulation neu bewertet werden.
Das folgende konstruierte Beispiel veranschaulicht diesen Punkt:
// bad.qml import QtQuick 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]; } } }
Die Schleife im onCompleted-Handler führt dazu, dass die Eigenschaftsbindung "text" sechsmal neu bewertet wird (was dazu führt, dass alle anderen Eigenschaftsbindungen, die sich auf den Textwert stützen, sowie der onTextChanged-Signalhandler jedes Mal neu bewertet werden und den Text jedes Mal für die Anzeige ausgeben). Dies ist in diesem Fall eindeutig unnötig, da wir uns wirklich nur für den Endwert der Akkumulation interessieren.
Es könnte wie folgt umgeschrieben werden:
// good.qml import QtQuick 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; } }
Sequenztipps
Wie bereits erwähnt, müssen Sequenzen von Wertetypen mit Vorsicht behandelt werden.
Erstens zeigen Sequenztypen in zwei verschiedenen Szenarien ein unterschiedliches Verhalten:
- wenn die Sequenz eine Q_PROPERTY einer QObject ist (wir nennen dies eine Referenzsequenz),
- wenn die Sequenz von einer Q_INVOKABLE Funktion einer QObject zurückgegeben wird (wir nennen dies eine Kopiersequenz).
Eine Referenzsequenz wird über die QMetaObject gelesen und geschrieben, sobald sie sich ändert, entweder in Ihrem JavaScript-Code oder auf dem Originalobjekt. Zur Optimierung können Referenzsequenzen (ebenso wie Referenzwerttypen) verzögert geladen werden. Der tatsächliche Inhalt wird dann erst bei der ersten Verwendung abgerufen. Das bedeutet, dass eine Änderung des Wertes eines beliebigen Elements in der Sequenz von JavaScript aus dazu führt, dass:
- Möglicherweise wird der Inhalt von der QObject gelesen (wenn er verzögert geladen wird).
- Ändern des Elements am angegebenen Index in dieser Sequenz.
- Zurückschreiben der gesamten Sequenz in die QObject.
Eine Kopiersequenz ist wesentlich einfacher, da die eigentliche Sequenz in den Ressourcendaten des JavaScript-Objekts gespeichert ist, so dass kein Lese-Änderungs-Schreib-Zyklus stattfindet (stattdessen werden die Ressourcendaten direkt geändert).
Daher sind Schreibvorgänge auf Elemente einer Referenzsequenz viel langsamer als Schreibvorgänge auf Elemente einer Kopiersequenz. Tatsächlich ist das Schreiben in ein einzelnes Element einer N-Element-Referenzsequenz von den Kosten her gleichbedeutend mit der Zuweisung einer N-Element-Kopiersequenz zu dieser Referenzsequenz, so dass es in der Regel besser ist, eine temporäre Kopiersequenz zu modifizieren und das Ergebnis dann während der Berechnung einer Referenzsequenz zuzuweisen.
Nehmen wir an, dass der folgende C++-Typ existiert (und zuvor im Namensraum "Qt.example" registriert wurde):
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; };
Das folgende Beispiel schreibt in einer engen Schleife in Elemente einer Referenzsequenz, was zu einer schlechten Leistung führt:
// bad.qml import QtQuick import Qt.example 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"); } }
Die QObject Eigenschaft Lesen und Schreiben in der inneren Schleife, verursacht durch den "qrealListProperty[j] = j" Ausdruck, macht diesen Code sehr suboptimal. Stattdessen wäre etwas funktional Äquivalentes, aber viel schneller:
// good.qml import QtQuick import Qt.example 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"); } }
Ein weiteres häufiges Muster, das vermieden werden sollte, sind Lese-Änderungs-Schreib-Schleifen, in denen jedes Element gelesen, geändert und in die Sequenzeigenschaft zurückgeschrieben wird. Ähnlich wie im vorherigen Beispiel führt dies dazu, dass bei jeder Iteration die Eigenschaft QObject gelesen und geschrieben wird:
// bad.qml import QtQuick import Qt.example 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] = qrealListProperty[j] * 2; } } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
Erstellen Sie stattdessen eine manuelle Kopie der Sequenz, ändern Sie die Kopie und weisen Sie das Ergebnis dann der Eigenschaft zurück zu:
// good.qml import QtQuick import Qt.example SequenceTypeExample { id: root width: 200 height: 200 Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 500; ++i) { let data = [...qrealListProperty]; for (var j = 0; j < 100; ++j) { data[j] = data[j] * 2; } qrealListProperty = data; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
Zweitens wird ein Änderungssignal für die Eigenschaft ausgegeben, wenn sich ein Element in ihr ändert. Wenn Sie viele Bindungen an ein bestimmtes Element in einer Sequenzeigenschaft haben, ist es besser, eine dynamische Eigenschaft zu erstellen, die an dieses Element gebunden ist, und diese dynamische Eigenschaft als Symbol in den Bindungsausdrücken anstelle des Sequenzelements zu verwenden, da sie nur dann eine Neuauswertung der Bindungen verursacht, wenn sich ihr Wert ändert.
Dies ist ein ungewöhnlicher Anwendungsfall, auf den die meisten Kunden nie stoßen sollten, aber es lohnt sich, dies zu wissen, falls Sie so etwas tun:
// bad.qml import QtQuick import Qt.example 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"); } }
Obwohl nur das Element bei Index 2 in der Schleife geändert wird, werden alle drei Bindungen neu bewertet, da die Granularität des Änderungssignals darin besteht, dass die gesamte Eigenschaft geändert wurde. Daher kann das Hinzufügen einer Zwischenbindung manchmal von Vorteil sein:
// good.qml import QtQuick import Qt.example 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"); } }
Im obigen Beispiel wird nur die Zwischenbindung jedes Mal neu ausgewertet, was zu einer erheblichen Leistungssteigerung führt.
Tipps zum Wert-Typ
Eigenschaftenvom Typ Wert (Schrift, Farbe, vector3d usw.) haben eine ähnliche QObject Eigenschaft und eine ähnliche Semantik wie Eigenschaften vom Typ Sequenz. Daher gelten die oben für Sequenzen gegebenen Tipps auch für wertartige Eigenschaften. Während sie bei Werttypen in der Regel weniger problematisch sind (da die Anzahl der Untereigenschaften eines Werttyps in der Regel weitaus geringer ist als die Anzahl der Elemente in einer Sequenz), wirkt sich jede Zunahme der Anzahl von Bindungen, die unnötigerweise neu ausgewertet werden, negativ auf die Leistung aus.
Allgemeine Tipps zur Leistung
Allgemeine JavaScript-Leistungsüberlegungen, die sich aus dem Sprachdesign ergeben, gelten auch für QML. Besonders hervorzuheben:
- Vermeiden Sie nach Möglichkeit die Verwendung von eval()
- Löschen Sie keine Eigenschaften von Objekten
Gemeinsame Elemente der Schnittstelle
Text-Elemente
Die Berechnung von Textlayouts kann ein langsamer Vorgang sein. Verwenden Sie nach Möglichkeit das Format PlainText anstelle von StyledText, da dies den Arbeitsaufwand für die Layout-Engine verringert. Wenn Sie PlainText nicht verwenden können (weil Sie Bilder einbetten oder Tags verwenden müssen, um Zeichenbereiche für bestimmte Formatierungen (fett, kursiv usw.) und nicht den gesamten Text anzugeben), sollten Sie StyledText verwenden.
Sie sollten AutoText nur verwenden, wenn der Text StyledText sein könnte (aber wahrscheinlich nicht ist), da dieser Modus Parsing-Kosten verursacht. Der Modus RichText sollte nicht verwendet werden, da StyledText fast alle seine Funktionen zu einem Bruchteil seiner Kosten bietet.
Bilder
Bilder sind ein wesentlicher Bestandteil jeder Benutzeroberfläche. Leider sind sie aber auch eine große Problemquelle, da sie viel Zeit zum Laden benötigen, viel Speicherplatz verbrauchen und auf unterschiedliche Weise verwendet werden.
Asynchrones Laden
Bilder sind oft recht groß, und es ist daher ratsam, dafür zu sorgen, dass das Laden eines Bildes den UI-Thread nicht blockiert. Setzen Sie die Eigenschaft "asynchron" des QML-Image-Elements auf true, um das asynchrone Laden von Bildern aus dem lokalen Dateisystem zu ermöglichen (entfernte Bilder werden immer asynchron geladen), wenn dies keine negativen Auswirkungen auf die Ästhetik der Benutzeroberfläche hat.
Bei Bildelementen mit der Eigenschaft "asynchron" auf true werden Bilder in einem Worker-Thread mit niedriger Priorität geladen.
Explizite Quellgröße
Wenn Ihre Anwendung ein großes Bild lädt, es aber in einem kleinen Element anzeigt, setzen Sie die Eigenschaft "sourceSize" auf die Größe des gerenderten Elements, um sicherzustellen, dass die kleiner skalierte Version des Bildes im Speicher bleibt und nicht die große.
Beachten Sie, dass eine Änderung der "sourceSize"-Eigenschaft dazu führt, dass das Bild neu geladen werden muss.
Laufzeitkomposition vermeiden
Denken Sie auch daran, dass Sie die Kompositionsarbeit zur Laufzeit vermeiden können, indem Sie die vorkomponierte Bildressource mit Ihrer Anwendung bereitstellen (z. B. indem Sie Elemente mit Schatteneffekten bereitstellen).
Glättung von Bildern vermeiden
Aktivieren Sie image.smooth nur bei Bedarf. Sie ist auf manchen Geräten langsamer und hat keinen visuellen Effekt, wenn das Bild in seiner natürlichen Größe angezeigt wird.
Malen
Vermeiden Sie es, denselben Bereich mehrmals zu malen. Verwenden Sie Item als Wurzelelement und nicht Rectangle, um zu vermeiden, dass der Hintergrund mehrmals gezeichnet wird.
Elemente mit Ankern positionieren
Es ist effizienter, Anker anstelle von Bindungen zu verwenden, um Elemente relativ zueinander zu positionieren. Betrachten Sie diese Verwendung von Bindungen, um rect2 relativ zu rect1 zu positionieren:
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
}Dies wird effizienter mit Ankern erreicht:
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
}Die Positionierung mit Bindungen (durch Zuweisung von Bindungsausdrücken zu den x-, y-, Breiten- und Höheneigenschaften von visuellen Objekten, statt durch die Verwendung von Ankern) ist relativ langsam, ermöglicht aber maximale Flexibilität.
Wenn das Layout nicht dynamisch ist, ist die statische Initialisierung der x-, y-, Breiten- und Höheneigenschaften der performanteste Weg, das Layout festzulegen. Elementkoordinaten sind immer relativ zu ihrem Elternteil, wenn Sie also einen festen Abstand zur 0,0-Koordinate des Elternteils haben möchten, sollten Sie keine Anker verwenden. Im folgenden Beispiel befinden sich die untergeordneten Rechteckobjekte an der gleichen Stelle, aber der gezeigte Ankercode ist nicht so ressourceneffizient wie der Code, der eine feste Positionierung über statische Initialisierung verwendet:
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
}
}Modelle und Ansichten
Die meisten Anwendungen haben mindestens ein Modell, das Daten an eine Ansicht weiterleitet. Es gibt einige semantische Aspekte, die Anwendungsentwickler beachten müssen, um eine maximale Leistung zu erzielen.
Benutzerdefinierte C++-Modelle
Oft ist es wünschenswert, ein eigenes benutzerdefiniertes Modell in C++ für die Verwendung mit einer Ansicht in QML zu schreiben. Während die optimale Implementierung eines solchen Modells stark vom Anwendungsfall abhängt, den es erfüllen muss, sind einige allgemeine Richtlinien wie folgt:
- Seien Sie so asynchron wie möglich
- Führen Sie die gesamte Verarbeitung in einem Worker-Thread (mit niedriger Priorität) durch.
- Stapelverarbeitung von Backend-Operationen, um (potenziell langsame) E/A und IPC zu minimieren
Es ist wichtig zu beachten, dass die Verwendung eines Worker-Threads mit niedriger Priorität empfohlen wird, um das Risiko zu minimieren, den GUI-Thread auszuhungern (was zu einer schlechteren wahrgenommenen Leistung führen könnte). Denken Sie auch daran, dass Synchronisierungs- und Sperrmechanismen eine wesentliche Ursache für eine langsame Leistung sein können, so dass unnötiges Sperren vermieden werden sollte.
ListModel QML-Typ
Qt Qml Models stellt einen ListModel Typ zur Verfügung, der zur Einspeisung von Daten in eine ListView verwendet werden kann. Er ist nützlich für schnelles Prototyping, aber nicht geeignet für größere Datenmengen. Verwenden Sie bei Bedarf ein eigenes QAbstractItemModel.
Auffüllen innerhalb eines Worker-Threads
ListModel Elemente können in einem Worker-Thread (mit niedriger Priorität) in JavaScript aufgefüllt werden. Der Entwickler muss explizit sync() auf ListModel von WorkerScript aus aufrufen, damit die Änderungen mit dem Hauptthread synchronisiert werden. Weitere Informationen finden Sie in der Dokumentation WorkerScript.
Bitte beachten Sie, dass die Verwendung eines WorkerScript Elements dazu führt, dass eine separate JavaScript-Engine erstellt wird (da die JavaScript-Engine pro Thread arbeitet). Dies führt zu einer erhöhten Speichernutzung. Mehrere WorkerScript Elemente verwenden jedoch alle denselben Worker-Thread, so dass die Speicherauswirkungen der Verwendung eines zweiten oder dritten WorkerScript Elements vernachlässigbar sind, wenn eine Anwendung bereits eines verwendet. Auf der anderen Seite laufen die zusätzlichen Arbeitsskripte nicht parallel.
Verwenden Sie keine dynamischen Rollen
Das Element ListModel geht davon aus, dass die Rollentypen innerhalb jedes Elements in einem bestimmten Modell zu Optimierungszwecken stabil sind. Wenn sich der Typ von Element zu Element dynamisch ändern kann, wird die Leistung des Modells wesentlich schlechter sein.
Daher ist die dynamische Typisierung standardmäßig deaktiviert; der Entwickler muss die boolesche Eigenschaft dynamicRoles des Modells speziell einstellen, um die dynamische Typisierung zu aktivieren (und die damit verbundene Leistungsverschlechterung in Kauf nehmen). Wir empfehlen, die dynamische Typisierung nur dann zu verwenden, wenn es unbedingt notwendig ist.
Ansichten
View Delegates sollten so einfach wie möglich gehalten werden. Es sollte nur so viel QML im Delegaten enthalten sein, dass die notwendigen Informationen angezeigt werden. Jede zusätzliche Funktionalität, die nicht sofort benötigt wird (z.B. wenn sie mehr Informationen anzeigt, wenn sie angeklickt wird), sollte nicht erstellt werden, bis sie benötigt wird (siehe den kommenden Abschnitt über träge Initialisierung).
Die folgende Liste ist eine gute Zusammenfassung der Dinge, die beim Entwurf eines Delegaten zu beachten sind:
- Je weniger Elemente in einem Delegaten sind, desto schneller können sie erstellt werden, und desto schneller kann die Ansicht gescrollt werden.
- Beschränken Sie die Anzahl der Bindungen in einem Delegaten auf ein Minimum; verwenden Sie insbesondere Anker anstelle von Bindungen für die relative Positionierung innerhalb eines Delegaten.
- Vermeiden Sie die Verwendung von ShaderEffect Elementen innerhalb von Delegaten.
- Aktivieren Sie niemals Clipping auf einem Delegaten.
Sie können die cacheBuffer Eigenschaft einer Ansicht festlegen, um die asynchrone Erstellung und Pufferung von Delegaten außerhalb des sichtbaren Bereichs zu ermöglichen. Die Verwendung von cacheBuffer wird für Ansichtsdelegate empfohlen, die nicht trivial sind und wahrscheinlich nicht innerhalb eines einzelnen Frames erstellt werden.
Beachten Sie, dass ein cacheBuffer zusätzliche Delegierte im Speicher hält. Daher muss der Wert, der sich aus der Verwendung von cacheBuffer ergibt, gegen den zusätzlichen Speicherverbrauch abgewogen werden. Entwickler sollten Benchmarking verwenden, um den besten Wert für ihren Anwendungsfall zu finden, da der erhöhte Speicherdruck, der durch die Verwendung von cacheBuffer verursacht wird, in einigen seltenen Fällen zu einer verringerten Bildrate beim Scrollen führen kann.
Für zusätzliche Leistungsverbesserungen sollten Sie die Wiederverwendung von Elementen in Ansichten aktivieren. Siehe Wiederverwendung von Elementen für ListView und Wiederverwendung von Elementen für TableView und TreeView für weitere Informationen.
Visuelle Effekte
Qt Quick enthält mehrere Funktionen, die es Entwicklern und Designern ermöglichen, außergewöhnlich ansprechende Benutzeroberflächen zu erstellen. Fließende und dynamische Übergänge sowie visuelle Effekte können in einer Anwendung sehr wirkungsvoll eingesetzt werden. Bei der Verwendung einiger der Funktionen in QML ist jedoch Vorsicht geboten, da sie Auswirkungen auf die Leistung haben können.
Animationen
Im Allgemeinen führt die Animation einer Eigenschaft dazu, dass alle Bindungen, die auf diese Eigenschaft verweisen, neu ausgewertet werden. Normalerweise ist dies erwünscht, aber in anderen Fällen kann es besser sein, die Bindung vor der Durchführung der Animation zu deaktivieren und dann die Bindung nach Abschluss der Animation neu zuzuweisen.
Vermeiden Sie die Ausführung von JavaScript während der Animation. Zum Beispiel sollte die Ausführung eines komplexen JavaScript-Ausdrucks für jedes Bild einer x-Eigenschaftsanimation vermieden werden.
Entwickler sollten besonders vorsichtig sein, wenn sie Skript-Animationen verwenden, da diese im Haupt-Thread ausgeführt werden (und daher dazu führen können, dass Frames übersprungen werden, wenn sie zu lange brauchen, um abgeschlossen zu werden).
Partikel
Das Qt Quick Particles Modul erlaubt es, schöne Partikeleffekte nahtlos in Benutzeroberflächen zu integrieren. Jede Plattform verfügt jedoch über unterschiedliche Grafikhardware-Fähigkeiten, und das Partikelmodul kann die Parameter nicht auf das beschränken, was Ihre Hardware vernünftigerweise unterstützen kann. Je mehr Partikel Sie zu rendern versuchen (und je größer sie sind), desto schneller muss Ihre Grafikhardware sein, um mit 60 FPS zu rendern. Je mehr Partikel gerendert werden, desto schneller muss die CPU sein. Es ist daher wichtig, alle Partikeleffekte auf Ihrer Zielplattform sorgfältig zu testen, um die Anzahl und Größe der Partikel zu kalibrieren, die Sie bei 60 FPS rendern können.
Es sollte beachtet werden, dass ein Partikelsystem deaktiviert werden kann, wenn es nicht verwendet wird (z. B. bei einem nicht sichtbaren Element), um unnötige Simulationen zu vermeiden.
Ausführlichere Informationen finden Sie in der Anleitung zur Partikelsystemleistung.
Kontrolle der Element-Lebensdauer
Durch die Partitionierung einer Anwendung in einfache, modulare Komponenten, die jeweils in einer einzigen QML-Datei enthalten sind, können Sie die Startzeit der Anwendung verkürzen, die Speichernutzung besser kontrollieren und die Anzahl der aktiven, aber unsichtbaren Elemente in Ihrer Anwendung reduzieren.
Faule Initialisierung
Die QML-Engine hat einige Tricks auf Lager, um sicherzustellen, dass beim Laden und Initialisieren von Komponenten keine Frames übersprungen werden. Es gibt jedoch keinen besseren Weg, um die Startzeit zu verkürzen, als die Arbeit zu vermeiden, die nicht notwendig ist, und sie aufzuschieben, bis sie notwendig ist. Dies kann entweder durch die Verwendung von Loader erreicht werden.
Loader verwenden
Der Loader ist ein Element, das das dynamische Laden und Entladen von Komponenten ermöglicht.
- Mit Hilfe der "active"-Eigenschaft eines Loaders kann die Initialisierung bis zum Bedarf verzögert werden.
- Mit der überladenen Version der Funktion "setSource()" können erste Eigenschaftswerte bereitgestellt werden.
- Das Setzen der Eigenschaft "Loader asynchronous " auf "true" kann auch den Ablauf während der Instanziierung einer Komponente verbessern.
Unbenutzte Elemente zerstören
Elemente, die unsichtbar sind, weil sie ein Kind eines nicht sichtbaren Elements sind (z. B. die zweite Registerkarte in einem Tab-Widget, während die erste Registerkarte angezeigt wird), sollten in den meisten Fällen träge initialisiert und gelöscht werden, wenn sie nicht mehr verwendet werden, um die laufenden Kosten zu vermeiden, die entstehen, wenn sie aktiv bleiben (z. B. Rendering, Animationen, Bewertung der Eigenschaftsbindung usw.).
Ein Element, das mit einem Loader-Element geladen wurde, kann durch Zurücksetzen der "source"- oder "sourceComponent"-Eigenschaft des Loaders freigegeben werden, während andere Elemente explizit durch den Aufruf von destroy() freigegeben werden können. In einigen Fällen kann es notwendig sein, das Element aktiv zu lassen. In diesem Fall sollte es zumindest unsichtbar gemacht werden.
Weitere Informationen über aktive, aber unsichtbare Elemente finden Sie im nächsten Abschnitt über Rendering.
Rendering
Der für das Rendering verwendete Szenegraph in Qt Quick verwendet wird, ermöglicht es, hochdynamische, animierte Benutzeroberflächen flüssig mit 60 FPS zu rendern. Es gibt jedoch einige Dinge, die die Rendering-Leistung drastisch verringern können, und Entwickler sollten darauf achten, diese Fallstricke nach Möglichkeit zu vermeiden.
Clipping
Clipping ist standardmäßig deaktiviert und sollte nur bei Bedarf aktiviert werden.
Clipping ist ein visueller Effekt, KEINE Optimierung. Es erhöht die Komplexität für den Renderer (anstatt sie zu reduzieren). Wenn das Ausschneiden aktiviert ist, schneidet ein Element sein eigenes Bild sowie die Bilder seiner Kinder auf sein Begrenzungsrechteck. Dadurch kann der Renderer die Zeichenreihenfolge der Elemente nicht mehr frei anordnen, was zu einer suboptimalen Durchquerung des Szenegraphen im besten Fall führt.
Clipping innerhalb eines Delegaten ist besonders schlecht und sollte unter allen Umständen vermieden werden.
Überzeichnung und unsichtbare Elemente
Wenn Sie Elemente haben, die vollständig von anderen (undurchsichtigen) Elementen verdeckt werden, ist es am besten, ihre Eigenschaft "sichtbar" auf false zu setzen, da sie sonst unnötig gezeichnet werden.
In ähnlicher Weise sollten Elemente, die unsichtbar sind (z. B. die zweite Registerkarte in einem Registerkarten-Widget, während die erste Registerkarte angezeigt wird), aber zur Startzeit initialisiert werden müssen (z. B. wenn die Kosten für die Instanziierung der zweiten Registerkarte zu lange dauern, um dies erst bei der Aktivierung der Registerkarte zu tun), ihre "visible"-Eigenschaft auf false setzen, um die Kosten für das Zeichnen dieser Elemente zu vermeiden (obwohl sie, wie bereits erläutert, immer noch die Kosten für die Auswertung von Animationen oder Bindungen verursachen, da sie noch aktiv sind).
Transluzent vs. Opak
Opake Inhalte sind im Allgemeinen viel schneller zu zeichnen als transluzente. Der Grund dafür ist, dass lichtdurchlässige Inhalte überblendet werden müssen und dass der Renderer undurchsichtige Inhalte möglicherweise besser optimieren kann.
Ein Bild mit einem lichtdurchlässigen Pixel wird als vollständig lichtdurchlässig behandelt, auch wenn es größtenteils undurchsichtig ist. Dasselbe gilt für ein BorderImage mit transparenten Rändern.
Shader
Der Typ ShaderEffect ermöglicht es, GLSL-Code mit sehr geringem Overhead in eine Qt Quick Anwendung einzubinden. Es ist jedoch wichtig zu wissen, dass das Fragment-Programm für jedes Pixel in der gerenderten Form ausgeführt werden muss. Beim Einsatz von Low-End-Hardware und wenn der Shader eine große Anzahl von Pixeln abdeckt, sollte man den Fragment-Shader auf wenige Anweisungen beschränken, um eine schlechte Leistung zu vermeiden.
In GLSL geschriebene Shader ermöglichen das Schreiben komplexer Transformationen und visueller Effekte, sie sollten jedoch mit Vorsicht verwendet werden. Die Verwendung von ShaderEffectSource führt dazu, dass eine Szene in ein FBO vorgerendert werden muss, bevor sie gezeichnet werden kann. Dieser zusätzliche Overhead kann ziemlich teuer sein.
Speicherzuweisung und -sammlung
Die Menge an Speicher, die von einer Anwendung zugewiesen wird, und die Art und Weise, wie dieser Speicher zugewiesen wird, sind sehr wichtige Überlegungen. Abgesehen von den offensichtlichen Bedenken über Out-of-Memory-Bedingungen auf speicherbeschränkten Geräten ist die Zuweisung von Speicher auf dem Heap ein ziemlich rechenintensiver Vorgang, und bestimmte Zuweisungsstrategien können zu einer verstärkten Fragmentierung von Daten über Seiten hinweg führen. JavaScript verwendet einen verwalteten Speicher-Heap, der automatisch gelöscht wird, was einige Vorteile, aber auch einige wichtige Auswirkungen hat.
Eine in QML geschriebene Anwendung verwendet Speicher sowohl aus dem C++-Heap als auch aus einem automatisch verwalteten JavaScript-Heap. Der Anwendungsentwickler muss sich der Feinheiten beider Systeme bewusst sein, um die Leistung zu maximieren.
Tipps für QML-Anwendungsentwickler
Die in diesem Abschnitt enthaltenen Tipps und Vorschläge sind lediglich Richtlinien, die nicht unter allen Umständen anwendbar sind. Stellen Sie sicher, dass Sie Ihre Anwendung sorgfältig anhand empirischer Metriken bewerten und analysieren, um die bestmöglichen Entscheidungen treffen zu können.
Instanziieren und initialisieren Sie Komponenten nach dem Zufallsprinzip
Wenn Ihre Anwendung aus mehreren Ansichten besteht (z. B. mehrere Registerkarten), aber nur eine zu einem bestimmten Zeitpunkt benötigt wird, können Sie die faule Instanziierung verwenden, um die Menge an Speicher, die Sie zu einem bestimmten Zeitpunkt zuweisen müssen, zu minimieren. Weitere Informationen finden Sie im vorherigen Abschnitt über Lazy Initialization.
Unbenutzte Objekte zerstören
Wenn Sie Komponenten "lazy" laden oder Objekte dynamisch während eines JavaScript-Ausdrucks erstellen, ist es oft besser, sie manuell unter destroy() zu löschen, als auf die automatische Garbage Collection zu warten. Weitere Informationen finden Sie im vorherigen Abschnitt über die Steuerung der Elementlebensdauer.
Rufen Sie den Garbage Collector nicht manuell auf
In den meisten Fällen ist es nicht ratsam, den Garbage Collector manuell aufzurufen, da dies den GUI-Thread für einen längeren Zeitraum blockieren würde. Dies kann zu übersprungenen Frames und ruckeligen Animationen führen, was um jeden Preis vermieden werden sollte.
Es gibt einige Fälle, in denen das manuelle Aufrufen des Garbage Collectors akzeptabel ist (dies wird in einem späteren Abschnitt näher erläutert), aber in den meisten Fällen ist das Aufrufen des Garbage Collectors unnötig und kontraproduktiv.
Vermeiden Sie die Definition mehrerer identischer impliziter Typen
Wenn ein QML-Element eine in QML definierte benutzerdefinierte Eigenschaft hat, wird diese zu einem eigenen impliziten Typ. Dies wird in einem der nächsten Abschnitte näher erläutert. Wenn mehrere identische implizite Typen in einer Component definiert sind, wird etwas Speicher verschwendet. In dieser Situation ist es normalerweise besser, eine neue Komponente explizit zu definieren, die dann wiederverwendet werden kann. Erwägen Sie in einem solchen Fall die Definition einer Inline-Komponente mit dem Schlüsselwort component.
Die Definition einer benutzerdefinierten Eigenschaft kann oft eine vorteilhafte Leistungsoptimierung sein (z. B. um die Anzahl der erforderlichen oder neu zu bewertenden Bindungen zu reduzieren), oder sie kann die Modularität und Wartbarkeit einer Komponente verbessern. In diesen Fällen wird die Verwendung benutzerdefinierter Eigenschaften empfohlen. Allerdings sollte der neue Typ, wenn er mehr als einmal verwendet wird, in eine eigene Komponente (Inline oder .qml-Datei) aufgeteilt werden, um Speicherplatz zu sparen.
Wiederverwendung bestehender Komponenten
Wenn Sie erwägen, eine neue Komponente zu definieren, sollten Sie sich vergewissern, dass eine solche Komponente nicht bereits im Komponentensatz für Ihre Plattform vorhanden ist. Andernfalls zwingen Sie die QML-Engine, Typdaten für einen Typ zu generieren und zu speichern, der im Wesentlichen ein Duplikat einer anderen, bereits existierenden und möglicherweise bereits geladenen Komponente ist.
Verwenden Sie Singleton-Typen anstelle von pragma-Bibliotheks-Skripten
Wenn Sie ein Pragma-Bibliotheks-Skript verwenden, um anwendungsweite Instanzdaten zu speichern, sollten Sie stattdessen einen QObject Singleton-Typ verwenden. Dies sollte zu einer besseren Leistung führen und hat zur Folge, dass weniger JavaScript-Heap-Speicher verwendet wird.
Speicherzuweisung in einer QML-Anwendung
Die Speichernutzung einer QML-Anwendung kann in zwei Teile aufgeteilt werden: die C++-Heap-Nutzung und die JavaScript-Heap-Nutzung. Ein Teil des jeweils zugewiesenen Speichers ist unvermeidlich, da er von der QML-Engine oder der JavaScript-Engine zugewiesen wird, während der Rest von den Entscheidungen des Anwendungsentwicklers abhängt.
Der C++-Heap enthält:
- den festen und unvermeidlichen Overhead der QML-Engine (Implementierungsdatenstrukturen, Kontextinformationen usw.);
- kompilierte Daten und Typinformationen pro Komponente, einschließlich Metadaten zu den Eigenschaften pro Typ, die von der QML-Engine generiert oder aus dem Festplattencache geladen werden, je nachdem, welche Module und welche Komponenten von der Anwendung geladen werden;
- C++-Daten pro Objekt (einschließlich Eigenschaftswerte) sowie eine Metaobjekthierarchie pro Element, je nachdem, welche Komponenten die Anwendung instanziiert;
- alle Daten, die speziell durch QML-Importe (Bibliotheken) zugewiesen werden.
Der JavaScript-Heap enthält:
- den festen und unvermeidbaren Overhead der JavaScript-Engine selbst (einschließlich eingebauter JavaScript-Typen);
- den festen und unvermeidbaren Overhead unserer JavaScript-Integration (Konstruktorfunktionen für geladene Typen, Funktionsschablonen usw.);
- Layout-Informationen pro Typ und andere interne Typdaten, die von der JavaScript-Engine zur Laufzeit für jeden Typ generiert werden (siehe Anmerkung unten zu Typen);
- JavaScript-Daten pro Objekt ("var"-Eigenschaften, JavaScript-Funktionen und Signalhandler sowie nicht optimierte Bindungsausdrücke);
- Variablen, die während der Auswertung von Ausdrücken zugewiesen werden.
Außerdem wird ein JavaScript-Heap für die Verwendung im Haupt-Thread und optional ein weiterer JavaScript-Heap für die Verwendung im WorkerScript -Thread zugewiesen. Wenn eine Anwendung kein WorkerScript Element verwendet, fällt dieser Overhead nicht an. Der JavaScript-Heap kann mehrere Megabyte groß sein, so dass Anwendungen, die für Geräte mit begrenztem Speicherplatz geschrieben wurden, am besten auf das Element WorkerScript verzichten sollten.
Beachten Sie, dass sowohl die QML-Engine als auch die JavaScript-Engine automatisch ihre eigenen Zwischenspeicher für Typdaten über beobachtete Typen erzeugen. Jede von einer Anwendung geladene Komponente ist ein eindeutiger (expliziter) Typ, und jedes Element (Komponenteninstanz), das seine eigenen benutzerdefinierten Eigenschaften in QML definiert, ist ein impliziter Typ. Jedes Element (Instanz einer Komponente), das keine benutzerdefinierte Eigenschaft definiert, wird von den JavaScript- und QML-Engines als Typ betrachtet, der explizit von der Komponente definiert wurde, und nicht als sein eigener impliziter Typ.
Betrachten Sie das folgende Beispiel:
import QtQuick 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" } }
Im vorherigen Beispiel haben die Rechtecke r0 und r1 keine benutzerdefinierten Eigenschaften, so dass die JavaScript- und QML-Engines sie beide als vom gleichen Typ betrachten. Das heißt, r0 und r1 werden beide als vom explizit definierten Typ Rectangle betrachtet. Die Rechtecke r2, r3 und r4 haben jeweils benutzerdefinierte Eigenschaften und werden als unterschiedliche (implizite) Typen betrachtet. Beachten Sie, dass r3 und r4 jeweils als unterschiedliche Typen betrachtet werden, obwohl sie identische Eigenschaftsinformationen haben, einfach weil die benutzerdefinierte Eigenschaft nicht in der Komponente deklariert wurde, deren Instanzen sie sind.
Wenn r3 und r4 beide Instanzen einer RectangleWithString Komponente wären und diese Komponentendefinition die Deklaration einer String-Eigenschaft mit dem Namen customProperty enthielte, dann würden r3 und r4 als vom gleichen Typ angesehen werden (d.h. sie wären Instanzen des Typs RectangleWithString, anstatt ihren eigenen impliziten Typ zu definieren).
Detaillierte Überlegungen zur Speicherzuweisung
Bei Entscheidungen über die Speicherzuweisung oder Leistungsabwägungen ist es wichtig, die Auswirkungen der CPU-Cache-Leistung, des Paging des Betriebssystems und der Garbage Collection der JavaScript-Engine zu berücksichtigen. Mögliche Lösungen sollten sorgfältig verglichen werden, um sicherzustellen, dass die beste Lösung gewählt wird.
Kein Satz allgemeiner Leitlinien kann ein solides Verständnis der zugrundeliegenden Prinzipien der Informatik in Verbindung mit einer praktischen Kenntnis der Implementierungsdetails der Plattform, für die der Anwendungsentwickler entwickelt, ersetzen. Außerdem kann keine noch so große Menge an theoretischen Berechnungen einen guten Satz von Benchmarks und Analysewerkzeugen ersetzen, wenn es darum geht, Entscheidungen über Kompromisse zu treffen.
Fragmentierung
Fragmentierung ist ein Problem der C++-Entwicklung. Wenn der Anwendungsentwickler keine C++-Typen oder Plugins definiert, kann er diesen Abschnitt getrost ignorieren.
Im Laufe der Zeit wird eine Anwendung große Teile des Speichers zuweisen, Daten in diesen Speicher schreiben und anschließend einige Teile des Speichers wieder freigeben, sobald sie einen Teil der Daten verwendet hat. Dies kann dazu führen, dass "freier" Speicher in nicht zusammenhängenden Abschnitten liegt, die nicht an das Betriebssystem zurückgegeben werden können, um von anderen Anwendungen genutzt zu werden. Es hat auch Auswirkungen auf die Caching- und Zugriffseigenschaften der Anwendung, da die "lebenden" Daten über viele verschiedene Seiten des physischen Speichers verteilt sein können. Dies wiederum könnte das Betriebssystem zum Auslagern zwingen, was zu Dateisystem-E/A führen kann - was vergleichsweise ein extrem langsamer Vorgang ist.
Die Fragmentierung kann vermieden werden, indem Pool-Allokatoren (und andere Allokatoren für zusammenhängenden Speicher) verwendet werden, indem die Menge des jeweils zugewiesenen Speichers durch eine sorgfältige Verwaltung der Lebensdauer von Objekten reduziert wird, indem Caches regelmäßig bereinigt und neu aufgebaut werden oder indem eine speicherverwaltete Laufzeitumgebung mit Garbage Collection (wie JavaScript) verwendet wird.
Garbage Collection
JavaScript bietet Garbage Collection. Speicher, der auf dem JavaScript-Heap (im Gegensatz zum C++-Heap) zugewiesen wird, gehört der JavaScript-Engine. Die Engine sammelt in regelmäßigen Abständen alle nicht referenzierten Daten auf dem JavaScript-Heap ein.
Auswirkungen der Garbage Collection
Die Garbage Collection hat Vor- und Nachteile. Sie bedeutet, dass die manuelle Verwaltung der Objektlebensdauer weniger wichtig ist. Es bedeutet aber auch, dass die JavaScript-Engine zu einem Zeitpunkt, der außerhalb der Kontrolle des Anwendungsentwicklers liegt, einen potenziell langwierigen Vorgang auslösen kann. Wenn die JavaScript-Heap-Nutzung vom Anwendungsentwickler nicht sorgfältig bedacht wird, kann die Häufigkeit und Dauer der Garbage Collection negative Auswirkungen auf die Anwendungserfahrung haben. Seit Qt 6.8 ist der Garbage Collector inkrementell, was bedeutet, dass er kürzere, aber potenziell mehr Unterbrechungen verursacht.
Manuelles Aufrufen des Garbage Collectors
Eine in QML geschriebene Anwendung wird (höchstwahrscheinlich) irgendwann die Durchführung einer Garbage Collection erfordern. Zwar wird die Garbage Collection automatisch von der JavaScript-Engine nach einem eigenen Zeitplan ausgelöst, doch ist es gelegentlich besser, wenn der Anwendungsentwickler selbst entscheidet, wann er den Garbage Collector manuell aufruft (obwohl dies normalerweise nicht der Fall ist).
Der Anwendungsentwickler weiß wahrscheinlich am besten, wann eine Anwendung für längere Zeit im Leerlauf sein wird. Wenn eine QML-Anwendung viel JavaScript-Heap-Speicher verwendet, was regelmäßige und störende Garbage-Collection-Zyklen während besonders leistungssensibler Aufgaben (z. B. Scrollen von Listen, Animationen usw.) verursacht, kann es für den Anwendungsentwickler sinnvoll sein, den Garbage-Collector in Zeiten ohne Aktivität manuell aufzurufen. Leerlaufzeiten sind ideal für die Durchführung der Garbage Collection, da der Benutzer keine Beeinträchtigung der Benutzererfahrung (übersprungene Frames, ruckelnde Animationen usw.) bemerkt, die sich aus dem Aufruf des Garbage Collectors bei laufender Aktivität ergeben würde.
Der Garbage Collector kann manuell durch den Aufruf von gc() in JavaScript aufgerufen werden. Dies führt dazu, dass ein vollständiger, nicht inkrementeller Sammelzyklus durchgeführt wird, der zwischen einigen hundert und mehr als tausend Millisekunden dauern kann und daher möglichst vermieden werden sollte.
Abwägung zwischen Speicher und Leistung
In manchen Situationen ist es möglich, einen Kompromiss zwischen erhöhtem Speicherverbrauch und verringerter Verarbeitungszeit zu finden. Wenn beispielsweise das Ergebnis einer Symbolsuche, die in einer engen Schleife verwendet wird, in einer temporären Variablen in einem JavaScript-Ausdruck zwischengespeichert wird, führt dies zu einer erheblichen Leistungssteigerung bei der Auswertung dieses Ausdrucks, erfordert aber die Zuweisung einer temporären Variablen. In einigen Fällen sind diese Kompromisse sinnvoll (wie im obigen Fall, der fast immer sinnvoll ist), aber in anderen Fällen kann es besser sein, die Verarbeitung etwas länger dauern zu lassen, um den Speicherdruck auf das System nicht zu erhöhen.
In einigen Fällen können die Auswirkungen eines erhöhten Speicherbedarfs extrem sein. In manchen Situationen kann der Austausch von Speicherplatz gegen einen vermeintlichen Leistungsgewinn zu erhöhtem Page-Thrash oder Cache-Thrash führen, was eine enorme Leistungsminderung zur Folge hat. Es ist immer notwendig, die Auswirkungen von Kompromissen sorgfältig zu prüfen, um zu bestimmen, welche Lösung in einer bestimmten Situation die beste ist.
Ausführliche Informationen zur Cache-Leistung und zu Kompromissen bei der Speicherzeit finden Sie in den folgenden Artikeln:
- Ulrich Dreppers ausgezeichneter Artikel: "Was jeder Programmierer über Speicher wissen sollte", unter: https://people.freebsd.org/~lstewart/articles/cpumemory.pdf.
- Agner Fogs ausgezeichnete Handbücher zur Optimierung von C++-Anwendungen unter: http://www.agner.org/optimize/.
Optimierung von Fast Boot und Startup
Basierend auf praktischen Erfahrungen bei der Optimierung von Qt Quick Anwendungen für einen schnellen Start, sollten Sie die folgenden Best Practices berücksichtigen:
- Entwerfen Sie Ihre Anwendung so, dass sie von Anfang an schnell startet. Überlegen Sie, was der Benutzer zuerst sehen soll.
- Verwenden Sie die QML Profiler um Engpässe beim Starten zu identifizieren.
- Verwenden Sie Kettenladen. Lassen Sie nur so viele loaders laufen, wie Sie Kerne in Ihrer CPU haben (z.B. zwei Kerne: zwei Lader laufen gleichzeitig).
- Der erste loader sollte nicht asynchron sein, so dass einige Inhalte sofort angezeigt werden. Lösen Sie die asynchronen Lader danach aus.
- Stellen Sie nur bei Bedarf eine Verbindung zu Backend-Diensten her.
- Erstellen Sie QML-Module, die bei Bedarf importiert werden. Mit Lazily-Load-Modulen und -Typen können Sie Ihrer Anwendung unkritische Dienste nach Bedarf zur Verfügung stellen.
- Optimieren Sie Ihre PNG/JPG-Bilder mit Tools wie optipng.
- Optimieren Sie Ihre 3D-Modelle, indem Sie die Anzahl der Scheitelpunkte reduzieren und nicht sichtbare Teile entfernen.
- Optimieren Sie das Laden von 3D-Modellen durch die Verwendung von glTF.
- Schränken Sie die Verwendung von Clip und Deckkraft ein, da diese die Leistung beeinträchtigen können.
- Messen Sie die Grenzen der GPU und berücksichtigen Sie diese bei der Gestaltung der Benutzeroberfläche. Siehe Frame Captures und Performance Profiling für weitere Informationen.
- Verwenden Sie Qt Quick Compiler zum Vorkompilieren der QML-Dateien.
- Untersuchen Sie, ob statisches Linking für Ihre Architektur möglich ist.
- Bemühen Sie sich um deklarative Bindungen anstelle von imperativen Signalhandlern.
- Halten Sie Property-Bindings einfach. Generell sollte der QML-Code einfach, unterhaltsam und lesbar sein. Daraus folgt eine gute Leistung.
- Ersetzen Sie komplexe Steuerelemente durch Bilder oder Shader, wenn die Erstellungszeit ein Problem ist.
Tun Sie das nicht:
- Übertreiben Sie es nicht mit QML. Selbst wenn Sie QML verwenden, müssen Sie nicht alles in QML machen.
- Initialisieren Sie alles in Ihrer main.cpp.
- Erstellen Sie große Singletons, die alle erforderlichen Schnittstellen enthalten.
- Erstellen Sie komplexe Delegates für ListView oder andere Ansichten.
- Verwenden Sie Clip, wenn es nicht unbedingt notwendig ist.
- Tappen Sie in die häufige Falle der übermäßigen Verwendung von Loadern. Loader ist großartig für das langsame Laden größerer Dinge wie Anwendungsseiten, führt aber zu viel Overhead für das Laden einfacher Dinge ein. Es handelt sich nicht um schwarze Magie, die alles und jedes beschleunigt. Es handelt sich um ein zusätzliches Element mit einem zusätzlichen QML-Kontext.
Diese Praktiken tragen dazu bei, Startzeiten von weniger als einer Sekunde zu erreichen und ein reibungsloses Benutzererlebnis zu gewährleisten, insbesondere auf eingebetteten Geräten.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.