В эти выходные у меня было время поэкспериментировать с новым UI-фреймворком Flutter от Google. На бумаге он выглядит отлично: горячая перезагрузка, декларативное программирование интерфейса, управляемое состоянием. Документация Flutter обещает высокую производительность: “Flutter создан, чтобы разработчики легко могли достичь постоянных 60 кадров в секунду”.
Но за счет чего все это работает? Что насчет использования процессора?
Вкратце, все не так хорошо, как в нативных приложениях. Постоянное обновление интерфейса потребляет много ресурсов, и если вы часто вызываете setState(), убедитесь, что он изменяет интерфейс как можно меньше.
Я создал в Flutter простое приложение с секундомером и использовал его для анализа использования памяти и процессора.
Реализация
Интерфейс состоит из двух объектов: секундомера и таймера. Пользователь может включать, останавливать и сбрасывать секундомер при помощи двух кнопок. При запуске секундомера создается периодический таймер с обратным вызовом, который срабатывает каждые 30 мс и обновляет пользовательский интерфейс.
Интерфейс выглядит так:
Как это работает?
Две кнопки управляют состоянием объекта stopwatch. Когда секундомер обновляется, вызывается setState(), который вызывает метод build(). Часть метода build() — создание нового TimerText.
Класс TimerText выглядит так:
Таймер создается вместе с объектом TimerTextState. Каждый раз при обратном вызове вызывается setState(), но только если секундомер работает. Это вызывает метод build(), который создает новый объект Text с обновленным временем.
Сделать все правильно
Когда я создавал это приложение в первый раз, я управлял состоянием и интерфейсом в классе TimerPage, который включал секундомер и таймер. Это означает, что каждый раз при обратном вызове таймера, изменялся весь интерфейс. Это бессмысленно, потому что нужно изменить только объект Text, особенно потому, что таймер отправляет вызов каждые 30 миллисекунд.
Это очевидно, когда мы посмотрим на неоптимизированную и оптимизированную иерархии:
Создание отдельного класса TimerText для таймера лучше с точки зрения использования процессора.
Документация Flutter утверждает, что платформа оптимизирована для быстрого выделения ресурсов:
Фреймворк Flutter использует функциональный поток, который в значительной степени зависит от базового распределителя памяти, эффективно обрабатывающего небольшие краткосрочные выделения.
Возможно, ребилдинг дерева виджетов не считается за “небольшие краткосрочные выделения”. Но на деле моя оптимизация кода привела к сокращению использования процессора и памяти.
Обновление: с момента публикации этой статьи инженеры Google предложили ещё несколько оптимизаций.
Обновленный код сокращает изменение интерфейса при помощи разделения TimerText на виджеты MinutesAndSeconds и Hundredths:
Они являются слушателями обратных вызовов таймера и изменяются только при смене состояния. Это позволяет еще больше оптимизировать производительность, так как только виджет Hundredths обновляется каждые 30 миллисекунд.
Сравнение эффективности
Я запустил приложение в режиме release (flutter run — release):
- Устройство: iPhone 6 с iOS 11.2
- Версия Flutter: 0.1.5
- Xcode 9.2
Я отслеживал использование процессора и памяти три минуты и измерял производительность в трех разных режимах.
Неоптимизированный код
Использование процессора: 28%
Использование памяти: 32 МБ (на старте — 17 МБ)
Оптимизация 1 (отдельный виджет таймера)
Использование процессора: 25%
Использование памяти: 25 МБ (на старте — 17 МБ)
Оптимизация 2 (отдельные минуты, секунды и доли секунды)
Использование процессора: от 15 до 25%
Использование памяти: 26 МБ (на старте — 17 МБ)
В последнем тесте использование процессора соответствует использованию GPU, а потребление ресурсов интерфейсом остается постоянным.
Примечание: запуск того же теста в медленном режиме приводит к потреблению ресурсов процессора более чем на 50%, а использование памяти стабильно растет. Это может указывать на то, что память не освобождается в режиме разработки.
Убедитесь, что запускаете приложения в режиме релиза. Также помните, что Xcode показывает очень высокое потребление энергии, когда использование процессора превышает 20%.
Копаем глубже
Результаты заставили меня задуматься. Таймер, который обновляется примерно 30 раз в секунду и изменяет текст, не должен использовать почти 25% двухъядерного процессора с частотой в 1,4 ГГц.
Дерево виджетов в приложении Flutter построено на декларативной парадигме, а не на императивной модели, которая используется в iOS и Android.
Но будет ли императивная модель более производительной? Чтобы узнать это, я создал то же самое приложение на iOS.
Вот код в Swift для установки таймера и обновления текста каждые 30 миллисекунд:
И вот код, который я использовал в Dart (оптимизация 1):
Результат?
Flutter. Процессор: 25%, память: 22 МБ.
iOS. Процессор: 7%, память: 8 МБ.
Версия на Flutter использует в три раза больше мощности процессора и в три раза больше памяти. Если таймер не запущен, использование процессора сокращается до 1%. Это подтверждает, что вся работа процессора используется на коллбеки таймера и изменение интерфейса.
Это неудивительно. В приложении Flutter я создаю виджет Text каждый раз заново. В iOS я просто обновляю текст UILabel.
Но код форматирования строки времени отличается, откуда я знаю, что разница в использовании процессора не обусловлена этим?
Давайте изменим оба примера, чтобы в них не было форматирования:
Swift:
Dart:
Обновленные результаты:
Flutter. Процессор: 15%, память: 22 МБ.
iOS. Процессор: 8%, память: 8 МБ.
Версия на Flutter все равно использует процессор в два раза больше. При этом она сильно использует и другие потоки. На iOS активен только один поток.
Заключение
Я сравнил производительность Flutter/Dart и iOS/Swift в очень конкретном примере использования.
И числа не врут. Постоянное обновление интерфейса требует сравнительно больших вычислительных мощностей.
Но Flutter дает разработчикам возможность создавать приложения для iOS и Android с использованием одного и того же кода. Такие функции, как горячая перезагрузка, увеличивают производительность разработки. Flutter ещё находится в самом начале. Я надеюсь, что Google и сообщество Flutter смогут улучшить работу приложений, чтобы принести пользу конечным пользователям.
В своих приложениях постарайтесь подстроить свой код так, чтобы минимизировать изменения интерфейса. Я добавил код этого проекта в репозиторий GitHub, поэтому вы можете поиграться с ним самостоятельно.