Учет производительности и рекомендации
Учет временных параметров
Как разработчику приложений, вы должны стремиться обеспечить согласованную частоту обновления рендеринга в 60 кадров в секунду. 60 кадров в секунду означают, что между каждым кадром имеется примерно 16 миллисекунд для обработки, что включает обработку, необходимую для загрузки примитивов рисования в графическое оборудование.
На практике это означает, что разработчик приложения должен:
- использовать асинхронное, управляемое событиями программирование, где это возможно
- использовать потоки обработки для выполнения значительных вычислений
- никогда не запускать цикл событий вручную
- никогда не тратить более нескольких миллисекунд за кадр в блокирующих функциях
Несоблюдение этих рекомендаций приведет к пропуску кадров, что оказывает существенное негативное влияние на пользовательский опыт.
Примечание: Искушение создать собственный QEventLoop или вызвать QCoreApplication::processEvents() для избежания блокировок внутри блока кода C++, вызываемого из QML, должно быть преодолено. Это опасно, поскольку при входе в цикл событий в обработчике сигналов или привязке движок QML продолжает выполнять другие привязки, анимации, переходы и т. д. Эти привязки могут затем вызвать побочные эффекты, которые, например, уничтожают иерархию, содержащую ваш цикл событий.
Профилирование
Самый важный совет: используйте профилировщик QML, включенный в Qt Creator. Знание того, где тратится время в приложении, позволит сосредоточиться на реальных проблемах, а не на потенциальных. Обратитесь к руководству Qt Creator для получения дополнительной информации о том, как использовать инструмент профилирования QML.
Определение того, какие привязки выполняются чаще всего или в каких функциях ваше приложение тратит больше всего времени, позволит вам решить, нужно ли оптимизировать проблемные области или перепроектировать некоторые детали реализации приложения для повышения производительности. Попытка оптимизировать код без профилирования, скорее всего, приведет к незначительным, а не к существенным улучшениям производительности.
Код JavaScript
Большинство приложений QML содержат большое количество кода JavaScript в виде динамических функций, обработчиков сигналов и выражений привязки свойств. Обычно это не проблема. Благодаря некоторым оптимизациям в движке QML, таким как оптимизации компилятора привязок, в некоторых случаях это может быть быстрее, чем вызов функции C++. Однако необходимо принять меры, чтобы случайно не инициировать ненужную обработку.
Привязки
В QML существуют два типа привязок: оптимизированные и неоптимизированные. Хорошо сохранять выражения привязки максимально простыми, поскольку движок QML использует оптимизированный оценочный механизм привязок, способный оценивать простые выражения привязки без необходимости перехода в полную среду выполнения JavaScript. Эти оптимизированные привязки оцениваются гораздо эффективнее, чем более сложные (неоптимизированные) привязки. Основное требование для оптимизации привязок заключается в том, что информация о типе каждого используемого символа должна быть известна во время компиляции.
Следует избегать следующих элементов в выражениях привязок для повышения оптимизируемости:
- объявление промежуточных переменных JavaScript
- доступ к свойствам "var"
- вызов функций JavaScript
- создание замыканий или определение функций внутри выражения привязки
- доступ к свойствам за пределами непосредственной области оценки
- запись в другие свойства в качестве побочных эффектов
Привязки работают быстрее, когда они знают тип объектов и свойств, с которыми работают. Это означает, что поиск свойств, которые не являются окончательными, в выражении привязки в некоторых случаях может быть медленнее, когда возможно изменение типа искомого свойства (например, производным типом).
Непосредственную область оценки можно суммировать следующим образом: она содержит:
- свойства объекта области выражения (для выражений привязки – это объект, к которому принадлежит привязка свойства)
- идентификаторы любых объектов в компоненте
- свойства корневого элемента в компоненте
Идентификаторы объектов из других компонентов и свойства любых таких объектов, а также символы, определенные или включенные из импорта JavaScript, не входят в непосредственную область оценки, и поэтому привязки, которые обращаются к этим элементам, не будут оптимизированы.
Обратите внимание, что если движок QML не может оптимизировать привязку с помощью своего оптимизированного оценочного механизма выражений привязки и поэтому должен оценивать ее в полной среде JavaScript, некоторые из перечисленных выше советов больше не будут применимы. Например, иногда полезно кэшировать результат разрешения свойства в промежуточной переменной JavaScript в очень сложном выражении привязки. В последующих разделах представлена дополнительная информация об оптимизациях такого рода.
Преобразование типов
Главная стоимость использования JavaScript заключается в том, что в большинстве случаев при обращении к свойству типа QML создается объект JavaScript с внешним ресурсом, содержащим основанные на C++ данные (или ссылку на них). В большинстве случаев это довольно недорого, но в других случаях может быть весьма затратным. Один из примеров затрат – присвоение QVariantMap C++ Q_PROPERTY свойству QML "variant". Списки также могут быть затратными, хотя последовательности определенных типов (QList int, qreal, bool, QString и QUrl) должны быть недорогими; другие типы списков предполагают значительные затраты на преобразование (создание нового массива JavaScript и добавление новых типов один за другим с преобразованием из экземпляра типа C++ в значение JavaScript).
Преобразование между некоторыми базовыми типами свойств (такими как свойства "string" и "url") также может быть затратным. Использование наиболее подходящего типа свойства позволит избежать ненужных преобразований.
Если вам необходимо предоставить QVariantMap в QML, используйте свойство "var" вместо свойства "variant". В целом, "property var" следует предпочесть "property variant" для всех случаев использования из QtQuick 2.0 и новее (обратите внимание, что "property variant" отмечен как устаревший), так как он позволяет хранить истинную ссылку JavaScript (что может уменьшить количество преобразований, необходимых в некоторых выражениях).
Разрешение свойств
Разрешение свойств требует времени. В некоторых случаях результат поиска можно кэшировать и повторно использовать, но всегда лучше избегать ненужной работы, если это возможно.
В следующем примере у нас есть блок кода, который выполняется часто (в данном случае это содержимое явного цикла; но это может быть, например, часто оцениваемое выражение привязки), и в нём мы многократно разрешаем объект с id "rect" и его свойство "color":
// bad.qml
import QtQuick 2.3
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which, value) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
printValue("red", rect.color.r);
printValue("green", rect.color.g);
printValue("blue", rect.color.b);
printValue("alpha", rect.color.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
} Вместо этого мы можем разрешить общую основу только один раз в блоке:
// 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" шесть раз (что затем приводит к повторной оценке всех остальных свойств привязок, которые зависят от значения текста, а также обработчика сигналов 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;
}
} Рекомендации по последовательностям
Как упоминалось ранее, некоторые типы последовательностей быстры (например, QList<int>, QList<qreal>, QList<bool>, QList<QString>, QStringList и QList<QUrl>), в то время как другие значительно медленнее. Помимо использования этих типов там, где это возможно, вместо медленных типов, существует несколько других связанных с производительностью семантических аспектов, о которых нужно знать для достижения наилучшей производительности.
Во-первых, для типов последовательностей существуют две различные реализации: одна для случаев, когда последовательность является свойством Q_PROPERTY объекта QObject (назовем ее последовательностью ссылок), и другая, когда последовательность возвращается из вызываемой функции Q_INVOKABLE объекта QObject (назовем ее последовательностью копий).
Последовательность ссылок читается и записывается с помощью 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");
}
} Чтение и запись свойства объекта QObject во внутреннем цикле, вызванное выражением "qrealListProperty[j] = j", делает этот код очень неэффективным. Вместо этого функционально эквивалентный, но намного более быстрый код будет выглядеть так:
// 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");
}
} В приведенном выше примере каждый раз будет переоцениваться только промежуточная привязка, что приведёт к значительному увеличению производительности.
Советы по свойствам типа значения
Свойства типа значения (шрифт, цвет, vector3d и т. д.) имеют аналогичную QObject семантику свойств и уведомлений об изменениях, что и свойства типа последовательности. Таким образом, вышеприведённые советы для последовательностей также применимы к свойствам типа значения. Хотя с типами значений они обычно менее проблематичны (поскольку количество дочерних свойств типа значения обычно намного меньше, чем количество элементов в последовательности), любое увеличение количества излишне переоцениваемых привязок негативно скажется на производительности.
Другие объекты JavaScript
Разные движки JavaScript предоставляют разные оптимизации. Движок JavaScript, используемый Qt Quick 2, оптимизирован для создания объектов и поиска свойств, но предоставляемые им оптимизации полагаются на определённые критерии. Если ваше приложение не соответствует критериям, движок JavaScript переключается на режим «медленного пути» с гораздо худшей производительностью. Поэтому всегда старайтесь обеспечить выполнение следующих критериев:
- По возможности избегайте использования eval()
- Не удаляйте свойства объектов
Общие элементы интерфейса
Элементы текста
Вычисление макетов текста может быть медленной операцией. По возможности используйте формат PlainText, а не StyledText, так как это уменьшает объём работы, необходимой макетному движку. Если вы не можете использовать PlainText (потому что вам нужно встраивать изображения или использовать теги для указания диапазонов символов, которые должны иметь определённый форматирование (полужирным, курсивом и т. д.) в отличие от всего текста), то вы должны использовать StyledText.
Вы должны использовать AutoText только в том случае, если текст может быть (но, вероятно, не является) StyledText, так как этот режим потребует затрат на парсинг. Режим RichText не следует использовать, так как StyledText предоставляет почти все его функции с меньшими затратами.
Изображения
Изображения являются важной частью любого пользовательского интерфейса. К сожалению, они также являются значительным источником проблем из-за времени, необходимого для их загрузки, объёма занимаемой памяти и способа их использования.
Асинхронная загрузка
Изображения часто бывают достаточно большими, поэтому разумно гарантировать, что загрузка изображения не блокирует поток пользовательского интерфейса. Установите свойство «asynchronous» элемента QML Image в значение true, чтобы включить асинхронную загрузку изображений из локальной файловой системы (удалённые изображения всегда загружаются асинхронно), если это не приведёт к негативному воздействию на эстетику пользовательского интерфейса.
Элементы изображения со свойством «asynchronous», установленным в значение true, будут загружать изображения в потоке обработчика низкого приоритета.
Явное указание размера источника
Если ваше приложение загружает большое изображение, но отображает его в элементе малого размера, установите свойство «sourceSize» в размер отображаемого элемента, чтобы убедиться, что в памяти хранится уменьшенная версия изображения, а не большая.
Обратите внимание, что изменение sourceSize приведёт к перезагрузке изображения.
Избегайте композиции во время выполнения
Также помните, что вы можете избежать выполнения работы по композиции во время выполнения, предоставив предварительно составленный ресурс изображения вместе с вашим приложением (например, предоставляя элементы с эффектами тени).
Избегайте сглаживания изображений
Включите image.smooth только в случае необходимости. На некоторых устройствах это медленнее, и оно не имеет визуального эффекта, если изображение отображается в своём естественном размере.
Рисование
Избегайте рисования одной и той же области несколько раз. Используйте Item в качестве корневого элемента вместо Rectangle, чтобы избежать многократного рисования фона.
Расположение элементов с помощью якорей
Для размещения элементов относительно друг друга эффективнее использовать якоря, а не привязки. Рассмотрим это использование привязок для размещения 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, width и height. Координаты элементов всегда относительны к их родительскому элементу, поэтому, если вы хотите иметь фиксированный смещение от координат 0,0 родительского элемента, не используйте якоря. В следующем примере дочерние прямоугольники находятся в одном и том же месте, но показанный код с якорями не так эффективен, как код, использующий фиксированное позиционирование через статическую инициализацию:
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. Хотя оптимальная реализация любой такой модели будет сильно зависеть от решаемой задачи, некоторые общие рекомендации таковы:
- Максимально использовать асинхронные операции
- Выполнять все вычисления в потоке обработчика (низкого приоритета)
- Группировать операции бэкэнда, чтобы минимизировать (потенциально медленные) операции ввода-вывода и межпроцессного взаимодействия
- Использовать скользящее окно срезов для кеширования результатов, параметры которого определяются с помощью профилирования
Важно отметить, что использование потока обработчика низкого приоритета рекомендуется, чтобы минимизировать риск перегрузки потока пользовательского интерфейса (что может привести к худшей воспринимаемой производительности). Также помните, что механизмы синхронизации и блокировок могут быть значительной причиной медленной производительности, поэтому следует избегать ненужных блокировок.
Тип QML ListModel
QML предоставляет тип ListModel, который можно использовать для подачи данных в ListView. Он должен быть достаточно эффективным для большинства случаев использования и достаточно производительным, если используется правильно.
Заполнение в потоке обработчика
Элементы ListModel могут быть заполнены в потоке обработчика (низкого приоритета) в JavaScript. Разработчик должен явно вызвать «sync()» на ListModel изнутри WorkerScript, чтобы изменения были синхронизированы с главным потоком. Дополнительную информацию см. в документации WorkerScript.
Обратите внимание, что использование элемента WorkerScript приведёт к созданию отдельного движка JavaScript (поскольку движок JavaScript работает в каждом потоке). Это приведёт к увеличению использования памяти. Несколько элементов WorkerScript будут использовать один и тот же поток обработчика, но влияние на память от использования второго или третьего элемента WorkerScript будет незначительным, если приложение уже использует один.
Не используйте динамические роли
Элемент ListModel в QtQuick 2 намного производительнее, чем в QtQuick 1. Улучшения производительности в основном связаны с предположениями о типе ролей внутри каждого элемента в данной модели — если тип не меняется, производительность кеширования резко улучшается. Если тип может динамически меняться от элемента к элементу, эта оптимизация становится невозможной, и производительность модели ухудшится в разы.
Поэтому динамическая типизация по умолчанию отключена; разработчик должен явно установить свойство boolean «dynamicRoles» модели для включения динамической типизации (и понести соответствующее снижение производительности). Мы рекомендуем не использовать динамическую типизацию, если возможно перепроектировать своё приложение, чтобы избежать её.
Представления
Делегат представления должен быть максимально простым. В делегате должно быть только достаточно QML для отображения необходимой информации. Любая дополнительная функциональность, которая не требуется немедленно (например, если при нажатии отображается больше информации), не должна создаваться до тех пор, пока не понадобится (см. следующий раздел о ленивой инициализации).
Следующий список является хорошим резюме вещей, которые следует учитывать при проектировании делегата:
- Чем меньше элементов в делегате, тем быстрее они создаются, и тем быстрее представление может прокручиваться.
- Сведите к минимуму количество привязок в делегате; в частности, используйте якоря, а не привязки для относительного позиционирования внутри делегата.
- Избегайте использования элементов ShaderEffect в делегатах.
- Никогда не включайте обрезку в делегате.
Вы можете установить свойство cacheBuffer представления, чтобы разрешить асинхронное создание и буферизацию делегатов за пределами видимой области. Использование cacheBuffer рекомендуется для делегатов представлений, которые являются нетривиальными и вряд ли будут созданы за один кадр.
Помните, что cacheBuffer хранит дополнительные делегаты в памяти. Поэтому выгоду от использования cacheBuffer необходимо уравновесить с дополнительным использованием памяти. Разработчики должны использовать бенчмаркинг, чтобы найти оптимальное значение для своего случая использования, так как повышенное давление на память, вызванное использованием cacheBuffer, в некоторых редких случаях может привести к снижению частоты кадров при прокрутке.
Визуальные эффекты
Qt Quick 2 включает в себя несколько функций, которые позволяют разработчикам и дизайнерам создавать исключительно привлекательные пользовательские интерфейсы. Жидкость и динамические переходы, а также визуальные эффекты могут быть эффективно использованы в приложении, но необходимо соблюдать осторожность при использовании некоторых функций в QML, так как они могут иметь последствия для производительности.
Анимации
В общем случае анимация свойства приведёт к переоценке любых привязок, которые ссылаются на это свойство. Обычно это желательно, но в других случаях может быть лучше отключить привязку перед выполнением анимации и затем повторно назначить привязку после завершения анимации.
Избегайте выполнения JavaScript во время анимации. Например, следует избегать выполнения сложного выражения JavaScript для каждого кадра анимации свойства x.
Разработчики должны быть особенно внимательны при использовании анимаций на основе скриптов, так как они выполняются в основном потоке (и, следовательно, могут привести к пропускам кадров, если они занимают слишком много времени для завершения).
Частицы
Модуль Qt Quick Particles позволяет интегрировать красивые эффекты частиц в пользовательские интерфейсы без проблем. Однако каждая платформа обладает различными возможностями графического оборудования, и модуль Particles не может ограничивать параметры тем, что ваше оборудование может поддерживать без проблем. Чем больше частиц вы пытаетесь отобразить (и чем они больше), тем быстрее должно быть ваше графическое оборудование для отображения с частотой 60 кадров в секунду. Влияние на большее количество частиц требует более быстрого процессора. Поэтому важно тщательно тестировать все эффекты частиц на вашей целевой платформе, чтобы откалибровать количество и размер частиц, которые вы можете отобразить с частотой 60 кадров в секунду.
Следует отметить, что система частиц может быть отключена, когда она не используется (например, на невидимом элементе), чтобы избежать ненужного моделирования.
Для более подробной информации см. Руководство по производительности систем частиц.
Управление сроком жизни элемента
Разделив приложение на простые, модульные компоненты, каждый из которых содержится в отдельном файле QML, вы можете достичь более быстрого времени запуска приложения, лучшего контроля за использованием памяти и уменьшения количества активных, но невидимых элементов в вашем приложении.
Ленивая инициализация
Двигатель QML выполняет некоторые хитроумные действия, чтобы гарантировать, что загрузка и инициализация компонентов не приводят к пропускам кадров. Однако нет лучшего способа сократить время запуска, чем избежать выполнения работы, которая вам не нужна, и отложить работу до необходимости. Этого можно достичь, используя либо Loader, либо создавая компоненты динамически.
Использование Loader
Элемент Loader позволяет динамически загружать и выгружать компоненты.
- Используя свойство «active» Loader, инициализация может быть отложена до необходимости.
- Используя перегруженную версию функции «setSource()», можно задать начальные значения свойств.
- Установить свойство Loader asynchronous в значение true также может улучшить плавность работы во время создания компонента.
Использование динамического создания
Разработчики могут использовать функцию Qt.createComponent() для динамического создания компонента во время выполнения из JavaScript и затем вызвать createObject() для его экземпляризации. В зависимости от семантики владения, указанной в вызове, разработчик может должен вручную удалить созданный объект. Подробнее см. Динамическое создание объектов QML из JavaScript.
Уничтожение неиспользуемых элементов
Элементы, которые невидимы, потому что являются дочерними элементами невидимого элемента (например, вторая вкладка в виджете с вкладками, пока отображается первая вкладка), в большинстве случаев должны инициализироваться лениво и удаляться, когда они больше не используются, чтобы избежать непрерывных затрат на их активность (например, отрисовка, анимации, вычисление привязки свойств и т. д.).
Элемент, загруженный с помощью элемента Loader, может быть освобожден путем сброса свойства «source» или «sourceComponent» Loader, в то время как другие элементы могут быть явно освобождены путем вызова destroy() на них. В некоторых случаях может потребоваться оставить элемент активным, в этом случае его следует сделать по крайней мере невидимым.
Дополнительную информацию об активных, но невидимых элементах см. в предстоящем разделе «Отрисовка».
Отрисовка
Используемый для отрисовки в QtQuick 2 граф сцены позволяет создавать высокодинамичные, анимированные пользовательские интерфейсы, которые отображаются плавно с частотой 60 кадров в секунду. Однако существуют некоторые факторы, которые могут значительно снизить производительность отрисовки, и разработчики должны избегать этих проблем по возможности.
Обрезка
Обрезка по умолчанию отключена и должна быть включена только при необходимости.
Обрезка — это визуальный эффект, а НЕ оптимизация. Она увеличивает (а не уменьшает) сложность для рендерера. Если обрезка включена, элемент обрезает как собственное рисование, так и рисование своих дочерних элементов до своего ограничивающего прямоугольника. Это препятствует возможности рендерера свободно изменять порядок рисования элементов, что приводит к не оптимальному обходу графа сцены в наилучшем случае.
Обрезка внутри делегата особенно вредна и должна быть избегаема во что бы то ни стало.
Перерисовка и невидимые элементы
Если у вас есть элементы, которые полностью закрыты другими (непрозрачными) элементами, лучше всего установить их свойство «visible» в false , иначе они будут ненужно отрисовываться.
Аналогично, элементы, которые невидимы (например, вторая вкладка в виджете с вкладками, пока отображается первая вкладка), но должны быть инициализированы во время запуска (например, если стоимость создания второй вкладки занимает слишком много времени, чтобы сделать это только при активации вкладки), должны иметь свое свойство «visible» установленным в значение false, чтобы избежать затрат на их отрисовку (хотя, как уже объяснялось, они все равно понесут затраты на любые анимации или вычисления привязок, так как они все еще активны).
Полупрозрачность против непрозрачности
Непрозрачный контент, как правило, отображается гораздо быстрее, чем полупрозрачный. Причина в том, что для полупрозрачного контента требуется смешивание, и рендерер может потенциально лучше оптимизировать непрозрачный контент.
Изображение с одним полупрозрачным пикселем обрабатывается как полностью полупрозрачное, даже если оно в основном непрозрачное. То же самое относится к BorderImage с прозрачными краями.
Схемы
Тип ShaderEffect позволяет размещать код GLSL в приложении Qt Quick с минимальными накладными расходами. Однако важно понимать, что фрагментная программа должна выполняться для каждого пикселя в отрисовываемой форме. При развертывании на оборудовании низкого класса и если схема охватывает большое количество пикселей, следует ограничить фрагментную схему несколькими инструкциями, чтобы избежать плохой производительности.
Схемы, написанные на GLSL, позволяют создавать сложные преобразования и визуальные эффекты, но их следует использовать с осторожностью. Использование ShaderEffectSource заставляет сцену предварительно отображаться в FBO перед отрисовкой. Эти дополнительные накладные расходы могут быть довольно значительными.
Распределение и сборка памяти
Количество памяти, которое будет выделено приложением, и способ этого выделения — очень важные соображения. Помимо очевидных опасений по поводу условий нехватки памяти на устройствах с ограниченным объёмом памяти, выделение памяти в куче является довольно дорогостоящей операцией с вычислительной точки зрения, и некоторые стратегии выделения могут привести к увеличению фрагментации данных по страницам. JavaScript использует управляемую кучу памяти, которая автоматически собирается по мусору, и это имеет некоторые преимущества, а также важные последствия.
Приложение, написанное на QML, использует память как из C++ кучи, так и из автоматически управляемой кучи JavaScript. Разработчик приложения должен учитывать тонкости каждой из них, чтобы максимизировать производительность.
Советы для разработчиков приложений QML
Советы и рекомендации, содержащиеся в этом разделе, являются лишь руководством и могут не применяться во всех случаях. Убедитесь, что вы тщательно проверяете и анализируете своё приложение, используя эмпирические метрики, чтобы принять наилучшие решения.
Инициализировать компоненты лениво
Если ваше приложение состоит из нескольких представлений (например, нескольких вкладок), но требуется только одно в данный момент, вы можете использовать ленивую инициализацию, чтобы минимизировать количество памяти, которое вам нужно выделить в любой момент. Дополнительную информацию см. в предыдущем разделе о ленивой инициализации.
Уничтожить неиспользуемые объекты
Если вы лениво загружаете компоненты или создаёте объекты динамически во время выражения JavaScript, зачастую лучше destroy() их вручную, а не ждать, пока автоматическая сборка мусора это сделает. Подробнее см. в предыдущем разделе о управлении сроком жизни элемента.
Не вызывайте сборщик мусора вручную
В большинстве случаев не рекомендуется вызывать сборщик мусора вручную, так как он заблокирует поток пользовательского интерфейса на значительный период времени. Это может привести к пропускам кадров и рывковым анимациям, чего следует избегать всеми силами.
Существуют некоторые случаи, когда ручное вызов сборщика мусора приемлем (и это подробно объясняется в предстоящем разделе), но в большинстве случаев вызов сборщика мусора не нужен и контрпродуктивен.
Избегайте сложных привязок
Помимо снижения производительности сложных привязок (например, из-за необходимости входа в контекст выполнения JavaScript для выполнения вычислений), они также занимают больше памяти как в C++ куче, так и в куче JavaScript, чем привязки, которые могут быть вычислены оптимизированным оценщиком выражений привязки QML.
Избегайте определения нескольких идентичных неявных типов
Если элемент QML имеет настраиваемое свойство, определённое в QML, он становится своим собственным неявным типом. Это подробно объясняется в предстоящем разделе. Если несколько идентичных неявных типов определены в строке в компоненте, будет потрачено некоторое количество памяти. В этой ситуации обычно лучше определить новый компонент, который затем можно повторно использовать.
Определение настраиваемого свойства часто может быть полезной оптимизацией производительности (например, для уменьшения количества необходимых или повторно оцениваемых привязок) или может улучшить модульность и поддерживаемость компонента. В этих случаях использование настраиваемых свойств приветствуется. Однако новый тип, если он используется более одного раза, следует разделить на собственный компонент (.qml-файл), чтобы сохранить память.
Используйте существующие компоненты
Если вы рассматриваете определение нового компонента, стоит убедиться, что такой компонент уже не существует в наборе компонентов для вашей платформы. В противном случае вы заставите движок QML генерировать и хранить данные типа для типа, который по существу является дубликатом другого уже существующего и потенциально уже загруженного компонента.
Используйте типы синглтон вместо скриптов библиотек пragma
Если вы используете скрипт библиотек pragma для хранения данных экземпляра на уровне приложения, рассмотрите возможность использования типа синглтон QObject вместо этого. Это должно повысить производительность и уменьшить использование памяти кучи JavaScript.
Распределение памяти в приложении QML
Использование памяти приложением QML может быть разделено на две части: его использование C++ кучи и его использование кучи JavaScript. Часть памяти, выделенная в каждой, будет неизбежной, поскольку она выделяется движком QML или движком JavaScript, в то время как остальная часть зависит от решений, принятых разработчиком приложения.
C++ куча будет содержать:
- фиксированные и неизбежные накладные расходы движка QML (структуры данных реализации, информация о контексте и т. д.);
- скомпилированные данные и информация о типах для каждого компонента, включая метаданные свойств для каждого типа, которые генерируются движком QML в зависимости от загруженных приложений модулей и компонентов;
- данные C++ для каждого объекта (включая значения свойств) плюс иерархия метаобъектов для каждого элемента, в зависимости от того, какие компоненты инициализирует приложение;
- любые данные, выделенные специально импортами QML (библиотеками).
В куче JavaScript будет содержаться:
- фиксированные и неизбежные накладные расходы самого движка JavaScript (включая встроенные типы JavaScript);
- фиксированные и неизбежные накладные расходы нашей интеграции JavaScript (функции конструкторов для загруженных типов, шаблоны функций и т. д.);
- информация о макете для каждого типа и другие внутренние данные типов, сгенерированные движком JavaScript во время выполнения для каждого типа (см. примечание ниже, касающееся типов);
- данные JavaScript для каждого объекта («переменные» свойства, JavaScript-функции и обработчики сигналов, а также неоптимизированные выражения привязки);
- переменные, выделенные во время вычисления выражений.
Кроме того, будет выделена одна куча JavaScript для использования в основном потоке и, по желанию, еще одна куча JavaScript для использования в потоке WorkerScript. Если приложение не использует элемент WorkerScript, эти накладные расходы не будут понесены. Размер кучи JavaScript может составлять несколько мегабайт, поэтому приложениям, написанным для устройств с ограниченным объемом памяти, лучше всего избегать элемента WorkerScript, несмотря на его полезность в асинхронном заполнении моделей списков.
Обратите внимание, что как движок QML, так и движок JavaScript автоматически создают свои собственные кэши данных типа об наблюдаемых типах. Каждый загруженный приложением компонент — это отдельный (явный) тип, а каждый элемент (экземпляр компонента), определяющий собственные пользовательские свойства в QML, — это неявный тип. Любой элемент (экземпляр компонента), не определяющий никаких пользовательских свойств, рассматривается движками JavaScript и QML как принадлежащий типу, явно определенному компонентом, а не его собственному неявный типу.
Рассмотрим следующий пример:
import QtQuick 2.3
Item {
id: root
Rectangle {
id: r0
color: "red"
}
Rectangle {
id: r1
color: "blue"
width: 50
}
Rectangle {
id: r2
property int customProperty: 5
}
Rectangle {
id: r3
property string customProperty: "hello"
}
Rectangle {
id: r4
property string customProperty: "hello"
}
} В предыдущем примере прямоугольники r0 и r1 не имеют пользовательских свойств, поэтому движки JavaScript и QML считают их обоими одного типа. То есть r0 и r1 оба считаются типа Rectangle, явно определенного. Прямоугольники r2, r3 и r4 каждый имеют пользовательские свойства и каждый считается различного (явного) типа. Обратите внимание, что r3 и r4 считаются различными типами, даже если они имеют идентичную информацию о свойствах, просто потому, что пользовательское свойство не было объявлено в компоненте, экземплярами которого они являются.
Если r3 и r4 оба были экземплярами компонента RectangleWithString, и в определении этого компонента было объявлено строковое свойство с именем customProperty, тогда r3 и r4 считались бы одного типа (то есть они были бы экземплярами типа RectangleWithString, а не определяли бы свой собственный неявный тип).
Подробные соображения по выделению памяти
При принятии решений, связанных с выделением памяти или компромиссами производительности, важно учитывать влияние производительности кэша процессора, кэширования операционной системы и сборки мусора движка JavaScript. Потенциальные решения следует тщательно протестировать, чтобы убедиться, что выбран лучшее.
Ни один набор общих рекомендаций не может заменить глубокого понимания основополагающих принципов компьютерных наук в сочетании с практическим знанием реализационных деталей платформы, для которой разрабатывает приложение разработчик. Кроме того, никакие теоретические расчеты не могут заменить хороший набор инструментов для бенчмаркинга и анализа при принятии решений о компромиссах.
Фрагментация
Фрагментация — это проблема разработки на C++. Если разработчик приложения не определяет никаких типов C++ или плагинов, он может безопасно проигнорировать этот раздел.
Со временем приложение будет выделять большие объемы памяти, записывать данные в эту память и впоследствии освобождать некоторые части после завершения использования некоторых данных. Это может привести к тому, что «свободная» память будет находиться в несмежных блоках, которые нельзя вернуть операционной системе для использования другими приложениями. Это также сказывается на характеристиках кэширования и доступа к приложению, поскольку «живые» данные могут быть распределены по многим различным страницам физической памяти. В свою очередь, это может заставить операционную систему производить подкачку, что может вызвать ввод-вывод файловой системы, который по сравнению является очень медленной операцией.
Фрагментацию можно избежать, используя пул-аллокаторы (и другие аллокаторы смежной памяти), уменьшая объем памяти, выделяемый в любой момент, тщательно управляя жизненным циклом объектов, периодически очищая и перестраивая кэши или используя управляемую память с сборкой мусора (такую как JavaScript).
Сборка мусора
JavaScript предоставляет сборку мусора. Память, выделенная в куче JavaScript (в отличие от кучи C++), принадлежит движку JavaScript. Движок периодически собирает все неиспользуемые данные в куче JavaScript.
Последствия сборки мусора
Сборка мусора имеет преимущества и недостатки. Это означает, что ручное управление жизненным циклом объектов менее важно. Однако это также означает, что потенциально длительная операция может быть инициирована движком JavaScript в момент, неподконтрольный разработчику приложения. Если разработчик приложения не будет тщательно учитывать использование кучи JavaScript, частота и продолжительность сборки мусора могут негативно сказаться на опыте работы с приложением.
Принудительное выполнение сборки мусора
Приложение, написанное на QML, (вероятно) потребует выполнения сборки мусора на определенном этапе. Хотя сборка мусора будет автоматически запускаться движком JavaScript, когда количество доступной свободной памяти будет низким, иногда лучше, если разработчик приложения принимает решения о том, когда принудительно запускать сборку мусора (хотя обычно это не так).
Разработчик приложения, скорее всего, лучше всего понимает, когда приложение будет простаивать в течение существенных периодов времени. Если приложение QML использует много памяти кучи JavaScript, что приводит к регулярным и разрушительным циклам сборки мусора во время задач, чувствительных к производительности (например, прокрутка списка, анимации и т. д.), разработчику приложения может быть полезно принудительно запускать сборку мусора во время периодов бездействия. Периоды бездействия идеально подходят для выполнения сборки мусора, так как пользователь не заметит снижения пользовательского опыта (пропущенные кадры, рывки анимаций и т. д.), которые могут возникнуть при запуске сборки мусора во время активности.
Сборщик мусора может быть вызван вручную, вызвав gc() в JavaScript. Это запустит полный цикл сборки, который может занимать от нескольких сотен до более тысячи миллисекунд, поэтому его следует избегать, если это возможно.
Компромиссы между памятью и производительностью
В некоторых ситуациях можно пожертвовать увеличением использования памяти для снижения времени обработки. Например, кэширование результата поиска символа, используемого в цикле, в временную переменную в выражении JavaScript приведет к значительному улучшению производительности при вычислении этого выражения, но при этом потребуется выделение временной переменной. В некоторых случаях эти компромиссы целесообразны (например, в случае выше, что почти всегда целесообразно), но в других случаях лучше позволить обработке занимать немного больше времени, чтобы избежать повышения нагрузки на систему.
В некоторых случаях воздействие увеличения нагрузки на память может быть экстремальным. В некоторых ситуациях обмен памяти на предполагаемый прирост производительности может привести к увеличению страниц или кэширования, что приведет к значительному снижению производительности. В любом случае необходимо тщательно тестировать влияние компромиссов, чтобы определить, какое решение лучше в данной ситуации.
Для получения подробной информации о производительности кэша и компромиссах между временем и памятью см. отличную статью Ульриха Дреппера «Что каждый программист должен знать о памяти» (доступна по адресу http://ftp.linux.org.ua/pub/docs/developer/general/cpumemory.pdf по состоянию на 18 апреля 2012 г.), а для информации об оптимизациях, специфичных для C++, см. отличные руководства Агнера Фога по оптимизации приложений C++ (доступны по адресу http://www.agner.org/optimize/ по состоянию на 18 апреля 2012 г.).
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.11/qtquick-performance.html