
Qt CAN Bus Beispiel – Wie fange ich an?
Ich begrüße Sie zu einem weiteren Blogbeitrag. Der letzte Beitrag behandelte eine Form der Kommunikation zwischen Geräten mit Qt Bluetooth […]
Wie Sie wissen, ist die Leistung ein kritischer Aspekt jeder Anwendung. Wenn Ihre QML-App langsam oder nicht reaktionsfähig ist, werden die Benutzer schnell das Interesse verlieren und sich anderen Alternativen zuwenden. Daher ist es entscheidend, die Leistung Ihrer App zu optimieren und sie so schnell und reaktionsfähig wie möglich zu machen.
Im vorherigen Beitrag zur Verbesserung der Leistung und Optimierung von QML-Apps haben wir darüber gesprochen, warum die Optimierung Ihres Projekts aus geschäftlicher Sicht wichtig ist. Wir haben uns auch die Vorteile von Effizienz- und Geschwindigkeitssteigerungen angesehen, die durch die Verlagerung der Logik von JavaScript nach C++ erreicht werden können. Zudem haben wir die effiziente Verwaltung von Delegates mit den von Qt bereitgestellten Modell-Schnittstellen thematisiert. Da das letztgenannte Thema bereits den UI-Bereich berührt, wollen wir uns weiteren verwandten Themen widmen, die Ihnen helfen können, die Leistung einer QML-App zu verbessern. Tauchen wir ein!
Sehen Sie, was unsere Experten für Sie im Bereich Qt-Entwicklungsdienstleistungen realisieren können.
Das dynamische Erstellen von Teilen der Benutzeroberfläche ist ein entscheidender Schritt, um Ihre benutzerorientierte Anwendung effizient zu gestalten. Die Hauptidee hinter diesem Ansatz ist, die Anzahl der Objekte im Speicher auf ein Minimum zu reduzieren. Um dieses Problem zu lösen, können wir Teile der Benutzeroberfläche bei Bedarf instanziieren, basierend auf dem, was der Benutzer im jeweiligen Moment benötigt. Lassen Sie mich beschreiben, welche Techniken Sie dafür einfach anwenden können:
Die erste Technik ist allgemeiner Natur, sie sagt also nicht direkt, wie Sie die Benutzeroberfläche laden sollen, sondern wann. Wann immer Sie eine neue Seite, ein komplexes Dialogfenster oder ein anderes aufwändiges UI-Element hinzufügen, sollten Sie sich fragen: „Brauche ich es im Speicher, wenn es nicht sichtbar ist?“ Auch wenn ungenutzte Objekte für den Benutzer unsichtbar oder nicht gerendert sind, können sie unnötige Logik auslösen, wie das ständige Aktualisieren von Eigenschaftsbindungen. Das Beibehalten ungenutzter UI-Elemente erhöht zudem den Speicherbedarf der Anwendung.
Eine Faustregel hierbei ist, nur die UI-Objekte zu instanziieren, die aktuell vom Benutzer sichtbar sind und im Moment nicht anderweitig verwendet werden. Dazu können verschiedene Elemente gehören, wie Unterseiten, Pop-ups oder Panels. Natürlich sollte man dies mit Bedacht anwenden, da ich hier keine nicht-visuellen Elemente (wie Timer oder Verbindungen) oder kritische Teile der Benutzeroberfläche berücksichtige. Falls es keine Kompromisse bei der Zeit geben kann, die zwischen der Anforderung des UI-Elements durch den Benutzer und dessen Anzeige vergeht, ist das Beibehalten eines solchen Objekts im Speicher während der gesamten Laufzeit eine gültige Vorgehensweise. Für alle nicht essenziellen Elemente sollten wir jedoch in Betracht ziehen, sie bei Bedarf zu laden.
Ein häufiger Fall, bei dem ein deutlicher Leistungsunterschied zugunsten des bedarfsorientierten Ladens von Objekten zu erkennen ist, tritt auf, wenn die Sprache in einer mehrsprachigen Anwendung geändert wird, die die Funktion `qsTr` zur Übersetzung verschiedener UI-Teile verwendet. Ein solches Szenario erfordert das Laden einer neuen .qm-Datei mittels QTranslator, deren Installation und das Aufrufen der retranslate-Methode der QML-Engine. Da dies alle Bindungsausdrücke aktualisiert, die Strings mit Übersetzungsmarkierungen verwenden, dauert es umso länger, je mehr Objekte mit übersetzbaren Strings instanziiert sind.
Dabei wird nicht nur die Anzeige der Strings selbst aktualisiert – alle Bindungen, die diese Strings verwenden, werden ebenfalls neu ausgewertet. Darüber hinaus müssen alle Objekte, deren Größe auf Basis des Inhalts berechnet wird, ihre Dimensionen neu berechnen, wenn sie einen übersetzten String enthalten, der aktualisiert wurde. Das Entladen ungenutzter Teile der Benutzeroberfläche verringert die Anzahl der zu überprüfenden Bindungen, wodurch Übersetzungen deutlich schneller aktualisiert werden können.
Nachdem wir nun wissen, welche Teile der Benutzeroberfläche wir vermeiden sollten, sofort zu instanziieren, und warum wir dies tun sollten, konzentrieren wir uns darauf, wie wir solche Objekte dynamisch erstellen können.
Die beiden gängigsten Methoden zur dynamischen Erstellung von Teilen der Benutzeroberfläche sind StackView und Loader. Wenn wir Objekte bei Bedarf erstellen, müssen wir bedenken, dass die Erstellung eines Objekts Zeit in Anspruch nehmen kann, insbesondere wenn es komplex ist. Das Hinzufügen komplexer Elemente zu einer StackView kann den Haupt-Thread während der Erstellung blockieren. Deshalb sind Loader weiterhin relevant, da sie Objekte inkubieren können, wenn die Eigenschaft `asynchronous` auf true gesetzt ist, wodurch eine Blockierung des UI-Threads verhindert wird.
QML file
StackView { id: stack anchors { top: parent.top bottom: parent.bottom left: parent.left right: divider.left } Button { id: stackButton anchors { bottom: parent.bottom horizontalCenter: parent.horizontalCenter } text: "Push to stack" onClicked: { console.time("Stack") stack.push(nastyComponent) console.timeEnd("Stack") } } } Loader { id: loader anchors { top: parent.top bottom: parent.bottom left: divider.right right: parent.right } asynchronous: true active: false sourceComponent: nastyComponent Button { id: loaderButton anchors { bottom: parent.bottom horizontalCenter: parent.horizontalCenter } text: "Load component" onClicked: { console.time("Loader") loader.active = true console.timeEnd("Loader") } } } Component { id: nastyComponent Flickable { contentHeight: grid.height Grid { id: grid Repeater { model: 100 delegate: Image { height: 50 fillMode: Image.PreserveAspectFit source: "https://upload.wikimedia.org/wikipedia/commons/0/0b/Qt_logo_2016.svg" sourceSize { width: 578 height: 424 } Component.onCompleted: { let millis = 20 var date = Date.now(); var curDate = null; do { curDate = Date.now(); } while (curDate-date < millis); } } } } } }
Wie lange dauert das Laden, nachdem der im obigen QML-Dokument gezeigte Code ausgelöst wurde? Der im Protokoll angezeigte Wert könnte für einige überraschend sein:
Warum dauert das Hinzufügen eines Elements zum Stack so lange, während die Funktionalitäten, die nach dem Auslösen des Loaders verwendet werden, sofort funktionieren? Das liegt daran, dass wir einen asynchronen Loader verwendet haben, der beginnt, Quellkomponenten außerhalb des Hauptthreads zu instanziieren. Dadurch wird der Rest der Benutzeroberfläche nicht blockiert, sodass andere Logik verarbeitet werden kann – in diesem Fall wird der Timer unmittelbar nach dem Start gestoppt. Lassen Sie sich davon nicht täuschen: Das Element wird nicht sofort instanziiert, aber das Auslösen des Loaders blockiert den Rest der Logik nicht, sodass andere Anweisungen fortgesetzt werden können. Wie sieht das Timing für die vollständige Instanziierung der Komponente aus? Nach der Modifikation des Codes können wir folgende Ergebnisse sehen:
In der Realität wird das Erstellen der komplexen Komponente über einen Loader fast genauso lange dauern wie bei der Verwendung von StackView. Das mag auf den ersten Blick nicht beeindruckend wirken, ist jedoch äußerst nützlich. Während der Ladezeit, wenn der asynchrone Loader das Objekt instanziiert, können wir weiterhin andere Logik verarbeiten oder einfach einen Fortschrittsindikator anzeigen. Darüber hinaus können Benutzer weiterhin mit dem Rest der Benutzeroberfläche interagieren, sodass wir ihnen ermöglichen können, andere Funktionen der App zu nutzen, während das komplexe Element geladen wird.
Wir sind jetzt mit Wissen über einige allgemeine Techniken ausgestattet, um die Leistung der UI-Seite zu optimieren. Was halten Sie davon, mehr über spezifische Elemente des Qt-Frameworks zu erfahren, die wir zur Verbesserung der Leistung unserer Anwendung nutzen können?
Eine der weniger bekannten Funktionen in Qt ist `Qt.callLater()`. Alles, was diese Funktion tut, ist das Vermeiden redundanter Aufrufe einer Funktion oder eines Signals. Dies wird erreicht, indem der Aufruf der Funktion ausgeführt wird, sobald die QML-Engine zur Ereignisschleife zurückkehrt, jedoch nur einmal pro Ereignisschleifeniteration, wenn der Satz von Argumenten derselbe ist. Diese Funktion ist besonders nützlich, wenn Sie vermeiden möchten, dass dieselbe Funktion mehrfach in schneller Folge aufgerufen wird, was zu unnötigem Overhead oder unbeabsichtigtem Verhalten führen könnte. Schauen wir uns an, wie `Qt.callLater()` in der Praxis verwendet wird:
Item { id: example property int callLaterCounter: 0 property int counter: 0 readonly property int executionTargetCount: 5 function callLaterExample() { for(let x = 0; x < example.executionTargetCount; x++) { example.foo() } for(let y = 0; y < example.executionTargetCount; y++) { Qt.callLater(example.bar) } console.log("Counter: " + example.counter) Qt.callLater(()=>{console.log("CallLaterCounter: " + example.callLaterCounter)}) } function foo() { console.log("Doing a thing!") example.counter++ } function bar() { console.log("Need to be executed once!") example.callLaterCounter++ } Component.onCompleted: { example.callLaterExample() } }
In diesem Beispiel haben wir zwei Funktionen, `foo()` und `bar()`. Beide Funktionen werden in einer for-Schleife mehrfach ausgelöst, aber bar() soll in dieser schnellen Abfolge nur einmal ausgeführt werden. Wir können uns vorstellen, dass `bar()` für ressourcenintensive Operationen verantwortlich sein könnte, die keinen Vorteil bringen würden, wenn sie in so kurzer Zeit mehrfach ausgelöst werden.
Nach der Ausführung des Beispiels sehen wir die folgenden Nachrichten in den Protokollen:
qml: Doing a thing! qml: Doing a thing! qml: Doing a thing! qml: Doing a thing! qml: Doing a thing! qml: Counter: 5 qml: Need to be executed once! qml: CallLaterCounter: 1
Wir können deutlich sehen, dass obwohl `Qt.callLater()` mehrfach ausgelöst wurde, die Funktion `bar()` nur einmal ausgeführt wurde. Die Reduktion der Aufrufe hat somit ihren Hauptzweck erfüllt. Sie haben vielleicht bemerkt, dass wir eine Pfeilfunktion als Parameter mit einem `console.log` übergeben haben, aber es wäre auch möglich, ein JS-Funktionsobjekt zu übergeben. Dies wurde gemacht, um den aktuellen Wert von `callLaterCounter` auszugeben. Hätten wir versucht, ihn sofort auszugeben, wäre er noch nicht aktualisiert worden, da `bar` noch nicht ausgeführt wurde.
Zusätzlich können Sie weitere Argumente an `Qt.callLater()` übergeben. Diese Argumente werden an die aufgerufene Funktion weitergeleitet. Sie müssen jedoch beachten, dass bei der Eliminierung redundanter Aufrufe nur der letzte Satz von Argumenten an die Funktion übergeben wird.
Rectangle { id: backgroundRect width: 200 height: 200 color: "red" MouseArea { anchors.fill: parent onClicked: { // Assume multiple clicks happen in quick succession Qt.callLater(backgroundRect.changeBackgroundColor, "yellow"); Qt.callLater(backgroundRect.changeBackgroundColor, "blue"); Qt.callLater(backgroundRect.changeBackgroundColor, "green"); } } function changeBackgroundColor(newColor) { console.log("Hi! I'm switching to color " + newColor) backgroundRect.color = newColor; } }
In diesem Beispiel sehen Sie, dass wir die Funktion `changeBackgroundColor` dreimal in `Qt.callLater` eingewickelt mit verschiedenen Farben aufgerufen haben. Der Rechteckhintergrund hat sich jedoch von Rot zu Grün verändert und dabei die Farben Gelb und Blau durchlaufen.
Wie bei den meisten Features gibt es auch hier mehrere Möglichkeiten, dynamisch erstellte und scrollbare Listen in Ihrer Anwendung zu implementieren. Die Unterschiede zwischen den Ansätzen beschränken sich jedoch nicht nur auf die Einfachheit oder vorhandene Funktionen, sondern können sich auch erheblich auf die Leistung auswirken. Ein solches Beispiel ist der Vergleich zwischen der Verwendung von Flickable mit einem verschachtelten Repeater und ListView. Obwohl beide Ansätze ähnliche Ergebnisse liefern können, gibt es wesentliche Unterschiede hinsichtlich ihrer Auswirkungen auf die Leistung der Anwendung.
ListView ist eine Komponente, die eine komfortable Möglichkeit bietet, eine große Anzahl von Elementen in einer scrollbaren Liste anzuzeigen. Sie erlaubt die Definition eines Delegates, der verwendet wird, um Einträge im bereitgestellten Modell zu visualisieren. Darüber hinaus kann die ListView über angehängte Eigenschaften mit der Liste interagieren. Das Datenmodell selbst kann auf vielen unterstützten Typen basieren, wie z. B. ListModel, XmlListModel oder benutzerdefinierte C++-Modelle, die von QAbstractItemModel erben.
Ein entscheidender Vorteil von ListView ist die automatische Verwaltung von Positionierung, Scrollen und Recycling der instanziierten Delegates. Dies bedeutet, dass selbst bei großen Datensätzen nur eine kleine Anzahl von Elementen gleichzeitig instanziiert bleibt, was die Ressourcennutzung erheblich reduziert.
Eine alternative Methode zur Anzeige einer Serie von Elementen in einer scrollbaren Liste ist die Kombination mehrerer QML-Typen, um alle notwendigen Funktionen zu einer quasi-ListView bereitzustellen. In diesem Szenario wird Flickable wegen seiner Scroll-Funktionalitäten verwendet, die es Benutzern ermöglichen, Inhalte zu durchblättern, die nicht auf den Bildschirm passen. Obwohl ListView von Flickable abgeleitet ist, bietet der Basistyp keine Mechanismen zur dynamischen Objektinstanziierung oder Verwaltung ihrer Lebensdauer.
Objekte, die die Einträge im Modell repräsentieren, werden mit Repeater instanziiert. Dieser füllt das Flickable basierend auf dem bereitgestellten QML- oder C++-Modell. Ein weiterer wichtiger Bestandteil ist ein Layout-Handler, da weder Repeater noch Flickable die Positionierung ihrer Kinder eigenständig verwalten. Aus diesem Grund wird der Repeater in der Regel in ein Layout wie ColumnLayout für vertikale Listen oder RowLayout für horizontale Listen eingebettet.
Nachdem alle diese Elemente zusammengefügt wurden, hat man eine Liste, die scrollbar ist und ein Modell darstellt, das die Daten enthält. Alles scheint in Ordnung, und die Änderungen werden im Repository gespeichert. Am nächsten Tag bemerken die Entwickler jedoch, dass die Leistung der Anwendung erheblich abnimmt, wenn ein großer Datensatz dem neuen Listenansatz bereitgestellt wird. Warum passiert das?
Während es Szenarien gibt, in denen die Verwendung von Flickable mit Repeater sinnvoll sein könnte, gilt dieser Ansatz als Anti-Pattern für dynamische ListViews. Der Grund dafür liegt in der fehlenden Mechanik von Flickable und Repeater zur Verwaltung der Lebensdauer der enthaltenen Objekte basierend auf ihrer Sichtbarkeit.
Delegates, die vom Repeater instanziiert werden, werden beim Scrollen nicht zerstört, wenn sie sich außerhalb des Flickable-Viewports befinden. Sie können daher nicht bei Bedarf neu geladen werden, wenn sie wieder in den Viewport gelangen. Alle dynamisch erstellten Objekte werden sofort instanziiert, wenn Repeater und Flickable konstruiert werden, und bleiben während der gesamten Lebensdauer ihrer Eltern im Speicher.
Dieses Verhalten ist bei kleinen Datensätzen, bei denen nur wenige Delegates erstellt werden, möglicherweise unauffällig. Je mehr Einträge jedoch im Modell vorhanden sind, desto stärker wird die Leistung beeinträchtigt, da mehr Objekte, Bindungen, Animationen und andere Elemente von der Engine verarbeitet werden müssen.
Das Verhalten der gleichen Liste, die mit ListView implementiert wurde, unterscheidet sich grundlegend. Da dieser Typ ein System zur Verwaltung des Lebenszyklus von Delegates basierend auf ihrer Position im Viewport bereitstellt, wird die Leistung nicht von der Anzahl der Einträge im Modell beeinträchtigt. ListView instanziiert nur die Delegates, die sich derzeit im Viewport befinden, sowie einige Delegates außerhalb des Viewports als Puffer. Während der Benutzer durch die App scrollt, werden neue Delegates instanziiert, um sicherzustellen, dass die angezeigten Daten aktualisiert werden. Gleichzeitig werden Elemente, die nicht mehr sichtbar sind und das Ende des Puffers erreichen, de-konstruiert.
Ein solcher Ansatz ermöglicht es, die Anzahl der im Speicher befindlichen Objekte unter Kontrolle zu halten. Mit der cacheBuffer-Eigenschaft können Sie außerdem steuern, wie viele Delegates in den Puffer geladen werden.
In diesem Blogpost wurden verschiedene Techniken zur Optimierung der QML-Leistung untersucht. Wir haben die Auswirkungen der dynamischen UI-Erstellung analysiert und die Vor- und Nachteile des asynchronen Loaders und StackView für die bedarfsorientierte Objekterstellung verglichen. Darüber hinaus haben wir untersucht, wie Qt.callLater() redundante Funktionsaufrufe reduzieren und die Gesamtleistung verbessern kann. Schließlich haben wir die Unterschiede zwischen der Implementierung dynamischer Listen mit ListView und Flickable mit einem verschachtelten Repeater betrachtet und die jeweiligen Vorteile der ListView hervorgehoben.
Unabhängig davon, ob Sie ein erfahrener QML-Entwickler sind oder gerade erst anfangen, hoffe ich, dass dieser Blogpost Ihnen wertvolle Einblicke, Techniken und Beispiele geliefert hat, um die Leistung Ihrer App zu optimieren, die richtigen Werkzeuge für die jeweilige Aufgabe auszuwählen und eine bessere Benutzererfahrung zu schaffen. Wir laden Sie herzlich ein, Ihr Feedback zu teilen oder ein paar ermutigende Worte zu hinterlassen, wenn Ihnen dieser Beitrag gefallen hat.
Kommen wir zur Sache: Es ist eine Herausforderung, Top-Qt-QML-Entwickler zu finden. Helfen Sie sich selbst und starten Sie die Zusammenarbeit mit Scythe Studio – echten Experten im Qt C++ Framework.
Entdecken Sie unsere Fähigkeiten!Ich begrüße Sie zu einem weiteren Blogbeitrag. Der letzte Beitrag behandelte eine Form der Kommunikation zwischen Geräten mit Qt Bluetooth […]
Eingebettete Systeme sind das Rückgrat der modernen Technologie und treiben alles an – von IoT-Geräten und industrieller Automatisierung bis hin […]
Die Entwicklung von Software für die Medizingeräteindustrie erfordert eine sorgfältige Balance zwischen Innovation und Sicherheit. Da sich die Medizintechnik rasant […]