
Содержание статьи
Андреа Биззотто

В эти выходные мне пора было поиграть с новой системой интерфейса Flutter от Google.
На бумаге звучит здорово!
Согласно документам, следует ожидать высокой производительности:
Flutter разработан, чтобы помочь разработчикам легко добиться постоянных 60 кадров в секунду.
Но как насчет использования ЦБ?
TL; DR: Не такой хороший, как родной И делать это нужно правильно:
- Частая перерисовка пользовательского интерфейса стоит дорого
- Если звонить
setState()
часто убедитесь, что он перерисовывает как можно меньше пользовательского интерфейса.
Я создал простую программу секундомера во Flutter и профилировал ее для анализа использования процессора и памяти.

Реализация
Пользовательский интерфейс управляется двумя объектами: секундомером и таймером.
- Пользователь может запускать, останавливать и сбрасывать секундомер, нажав две кнопки.
- Каждый раз, когда запускается секундомер, создается периодический таймер с обратным вызовом, который запускается каждые 30 мс и обновляет пользовательский интерфейс.
Основной UI построен так:
Как это работает?
- Две кнопки управляют состоянием объекта секундомера.
- Когда секундомер обновляется,
setState()
вызывается, запускаяbuild()
метод. - В составе
build()
метод, новыйTimerText
создается.
The TimerText
класс выглядит так:
Пара примечаний:
- Таймер создается вместе с
TimerTextState
объект. Каждый раз, когда срабатывает обратный вызов,setState()
это называется если работает секундомер. - Это вызывает
build()
вызванный метод, рисующий новыйText
объект с обновленным временем.
Делать это правильно
Когда я впервые создал это приложение, я управлял всеми состояниями и пользовательским интерфейсом в TimerPage
класс, включавший как секундомер, так и таймер.
Это означало, что каждый раз, когда запускался обратный вызов таймера, весь пользовательский интерфейс перестраивался. Это излишне и неэффективно: только Text
объект, содержащий прошедшее время, следует перерисовать — особенно потому, что таймер срабатывает каждые 30 мс.
Это становится очевидным, если мы рассмотрим неоптимизированную и оптимизированную иерархию виджетов:

Создание отдельного TimerText
класс для инкапсуляции логики таймера менее интенсивным для ЦП.
Иными словами:
- Частая перерисовка пользовательского интерфейса стоит дорого
- Если звонить
setState()
часто убедитесь, что он перерисовывает как можно меньше пользовательского интерфейса.
В документах Flutter отмечено, что платформа оптимизирована для быстрого распределения:
Фреймворк Flutter использует поток в функциональном стиле, который в значительной степени зависит от базового распределителя памяти, эффективно обрабатывающего небольшие, кратковременные распределения.
Возможно, перестройка дерева виджетов не считается небольшим, кратковременным распределением. На практике мои оптимизации кода привели к меньшему использованию процессора и памяти (см. ниже).
Обновление 19–03–2018
После публикации этой статьи некоторые инженеры Google обратили на это внимание и любезно внесли дополнительные оптимизации.
Обновленный код еще больше уменьшает перерисовку пользовательского интерфейса путем разделения TimerText
на двоих MinutesAndSeconds
и Hundredths
виджеты:

Они регистрируются как слушатели обратного вызова таймера и перерисовываются, только когда их состояние меняется. Это дополнительно оптимизирует производительность как только Hundredths
виджет теперь рендерится каждые 30 мс.
Результаты бенчмаркинга
Я запустил программу в режиме выпуска (flutter run --release
):
- устройство: iPhone 6 бег iOS 11.2
- Версия Flutter: 0.1.5 (22 февраля 2018 г.).
- Xcode 9.2
Я наблюдал за использованием ЦП и памяти в Xcode в течение трех минут и измерял производительность трех разных режимов.
Не оптимизированный код
- Использование ЦБ: 28%
- Использование памяти: 32 МБ (от базового уровня 17 МБ после запуска программы)

Проход оптимизации 1 (отдельный текстовый виджет таймера)
- Использование ЦБ: 25%
- Использование памяти: 25 МБ (от базового уровня 17 МБ после запуска программы)

Проход оптимизации 2 (отдельные минуты, секунды, сотые)
- Использование ЦБ: от 15% до 25%
- Использование памяти: 26 МБ (от базового уровня 17 МБ после запуска программы)

В этом последнем тесте график использования ЦБ точно отслеживает поток GPU, в то время как поток пользовательского интерфейса остается достаточно постоянным.
ПРИМЕЧАНИЕ: запуск того же теста медленный режим обеспечивает использование ЦБ более 50%, и использование памяти постоянно растет некоторое время спустя.
Это может указывать на то, что память не освобождается в режиме разработки.
Ключевой вывод: убедитесь, что профиль ваших программ в режиме выпуска.
Обратите внимание, что Xcode сообщает a очень высоко энергетическое влияние, когда использование ЦБ превышает 20%.
Копать глубже
Результаты заставили меня задуматься. Таймер, срабатывающий примерно 30 раз в секунду и повторно воспроизводящий текстовую метку, не должен использовать до 25% двухъядерного ЦБ 1,4 ГГц.
Дерево виджетов в Flutter построено с помощью a декларативная парадигмаа не императив модель программирования, используемая в iOS/Android.
Но эффективнее ли императивная модель?
Чтобы узнать это, я создал ту же программу секундомера на iOS.
Это код Swift для настройки таймера и обновления текстовой метки каждые 30 мс:
Для полноты вот код форматирования времени, которое я использовал в Dart (проход оптимизации 1):
Окончательные результаты?
трепет. ЦБ: 25%, память: 22 Мб
iOS. ЦБ: 7%, память: 8 Мб
Реализация Flutter более чем в 3 раза тяжелее для ЦБ и использует в 3 раза больше памяти.
Когда таймер не работает, использование ЦБ возвращается до 1%. Это подтверждает, что вся работа ЦП направлена на обработку обратных вызовов таймера и перерисовку пользовательского интерфейса.
Это не совсем удивительно.
- В приложении Flutter я строю и рендер новый
Text
виджет каждый раз. - В iOS я просто обновляю текст a
UILabel
.
«Эй!» – Я слышу, как вы говорите. «Но код форматирования времени другой! Как вы знаете, что разница в использовании процессора не связана с этим?
Тогда давайте модифицируем оба примера, чтобы вообще не форматировать:
Swift:
Дартс:
Обновленные результаты:
трепет. ЦБ: 15%, память: 22 Мб
iOS. ЦБ: 8%, память: 8 Мб
Реализация Flutter все еще вдвое более интенсивна для ЦБ. Кроме того, кажется, что он выполняет достаточно много вещей в нескольких потоках (графический процессор, работа ввода/вывода). В iOS активен только один поток.
Вывод
Я сравнил производительность Flutter/Dart с iOS/Swift в очень конкретном случае использования.
Цифры не врут. Что касается частых обновлений пользовательского интерфейса, Вы не можете получить свой торт и съесть его тоже. ?
Flutter позволяет разработчикам создавать приложения для iOS и Android с одинаковой кодовой базой. А такие функции как горячая перезагрузка еще больше повышают производительность. Flutter все еще на зародыше. Я надеюсь, что Google и сообщество смогут улучшить профиль выполнения, чтобы эти преимущества были переданы конечным пользователям.
Что касается ваших программ, подумайте о тонкой настройке кода, чтобы минимизировать перерисовку пользовательского интерфейса. Это стоит усилий.
Я добавил весь код для этого проекта в это хранилище GitHub, так что вы можете поиграть с ним самостоятельно.
Добро пожаловать! ?
Этот пример проекта был моим первым экспериментом с Flutter. Если вы знаете, как написать более производительный код, я хотел бы услышать ваши комментарии.
Чтобы получить больше статей и видеоуроков, просмотрите кодировку с помощью Flutter.

Обо мне: Я разработчик iOS и Flutter, жонглирую между работой по контракту, открытым кодом, сторонними проектами и ведением блогов.
Я @biz84 в Twitter. Вы также можете просмотреть мою страницу GitHub. Отзывы, твиты, смешные гифки, приветствуются! Мой любимый? Многие???. Ну и банановый хлеб.