Четверг , Ноябрь 23 2017
Главная / Кодинг / Фризы и проседания FPS на Android

Фризы и проседания FPS на Android

В Android только главный поток приложения имеет право обновлять экран и обрабатывать нажатия на экран. Это значит, что, когда твое приложение занимается сложной работой в главном потоке, оно не имеет возможности реагировать на нажатия. Для пользователя приложение будет выглядеть зависшим. В данном случае опять же поможет вынос сложных операций в отдельные потоки.

Есть, однако, намного более тонкий и неочевидный момент. Android обновляет экран со скоростью 60 FPS. Это значит, что во время показа анимации или промотки списков у него есть всего 16,6 мс на отображение каждого кадра. В большинстве случаев Android справляется с этой работой и не теряет кадров. Но некорректно написанное приложение может затормозить его.

проседания FPS на Android
Упс, мы пропустили кадр

Простой пример: RecyclerView — элемент интерфейса, позволяющий создавать чрезвычайно длинные проматываемые списки, которые занимают одинаковое количество памяти вне зависимости от длины самого списка. Такое возможно благодаря переиспользованию одних и тех же наборов элементов интерфейса (ViewHolder) для отображения разных элементов списка. Когда элемент списка скрывается с экрана, его ViewHolder перемещается в кеш и затем используется для отображения следующих элементов списка.

Когда RecyclerView извлекает ViewHolder из кеша, он запускает метод onBindViewHolder() твоего адаптера, чтобы наполнить его данными конкретного элемента списка. И тут происходит интересное: если метод onBindViewHolder() будет делать слишком много работы, RecyclerView не успеет вовремя сформировать следующий элемент для отображения и список начнет тормозить во время промотки.

Еще один пример. К RecyclerView можно подключить кастомный RecyclerView.OnScrollListener(), метод OnScrolled() которого будет вызван при промотке списка. Обычно его используют для динамического скрытия и показа круглой action-кнопки в углу экрана (FAB — Floating Action Button). Но если реализовать в этом методе более сложный код, список опять же будет притормаживать.

Ну и третий пример. Допустим, интерфейс твоего приложения состоит из множества фрагментов, между которыми можно переключаться с помощью бокового меню (Drawer). Кажется, что самый очевидный вариант решения этой задачи — поместить в обработчик нажатия на пункты меню примерно такой код:

// Переключаем фрагмент
getSupportFragmentManager
.beginTransaction()
.replace(R.id.container, fragment, "fragment")
.commit()

// Закрываем Drawer
drawer.closeDrawer(GravityCompat.START)

Все логично, вот только если ты запустишь приложение и протестируешь его, то увидишь, что анимация закрытия меню тормозит. Проблема кроется в том, что метод commit() асинхронный, то есть переключение между фрагментами и закрытие меню будут происходить одновременно и смартфон просто не успеет вовремя обработать все операции обновления экрана.

Чтобы этого не происходило, переключать фрагмент нужно уже после того, как анимация закрытия меню закончилась. Сделать это можно, подключив к меню кастомный DrawerListener:

mDrawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
@Override public void onDrawerSlide(View drawerView, float slideOffset) {}
@Override public void onDrawerOpened(View drawerView) {}
@Override public void onDrawerStateChanged(int newState) {}

@Override
public void onDrawerClosed(View drawerView) {
if (mFragmentToSet != null) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, mFragmentToSet)
.commit();
mFragmentToSet = null;
}
}
});

Еще один совсем не очевидный момент. Начиная с Android 3.0 рендеринг интерфейса приложений происходит на графическом процессоре. Это значит, что все битмапы, drawable и ресурсы, указанные в теме приложения, выгружаются в память GPU и поэтому доступ к ним происходит очень быстро.

Любой элемент интерфейса, показанный на экране, преобразуется в набор полигонов и GPU-инструкций и поэтому отображается в случае, например, промотки быстро. Так же быстро будет скрываться и показываться View с помощью изменения атрибута visibility (button.setVisibility(View.GONE) и button.setVisibility(View.VISIBLE)).

А вот при изменении View, даже самом минимальном, системе вновь придется пересоздать View с нуля, загружая в GPU новые полигоны и инструкции. Более того, при изменении TextView эта операция станет еще более дорогой, так как Android придется сначала произвести растеризацию шрифта, то есть превратить текст в набор прямоугольных картинок, затем сделать все замеры и сформировать инструкции для GPU. А еще есть операция пересчета положения текущего элемента и других элементов лайота. Все это необходимо учитывать и изменять View только тогда, когда это действительно нужно.

Overdraw — еще одна серьезная проблема. Как мы говорили выше, разбор сложных лайотов с множеством вложенных элементов будет медленным сам по себе, но также он наверняка принесет с собой проблему частой перерисовки экрана.

Представь, что у тебя есть несколько вложенных друг в друга LinearLayout и некоторые из них к тому же со свойством background, то есть они не только вмещают в себя другие элементы интерфейса, но и имеют фон в виде рисунка или заливки цветом. В результате на этапе рендеринга интерфейса GPU будет делать так: заполнит пикселями нужного цвета область, занимаемую корневым LinearLayout, затем заполнит часть экрана, занимаемую вложенным лайотом, другим цветом (или тем же) и так далее. В результате во время рендеринга одного кадра многие пиксели на экране будут обновлены несколько раз. А это не имеет никакого смысла.

Полностью избежать overdraw невозможно. Например, если необходимо отобразить кнопку на красном фоне, тебе все равно придется сначала залить экран красным, а затем повторно изменить пиксели, отображающие кнопку. К тому же Android умеет оптимизировать рендеринг так, чтобы overdraw не происходил (например, если два элемента одинакового размера находятся друг над другом и второй непрозрачен, первый просто не будет отрисован). Однако многое зависит от программиста, который должен всеми силами стараться минимизировать overdraw.

Поможет в этом инструмент отладки наложений. Он встроен в Android и находится здесь: Settings → Developer Options → Debug GPU overdraw → Show overdraw areas. После его включения экран перекрасится в разные цвета, которые означают следующее:

  • обычный цвет — одинарное наложение;
  • синий — двойное наложение;
  • зеленой — тройное;
  • красный — четверное и больше.

Правило здесь простое: если большая часть интерфейса твоего приложения стала зеленой или красной — у тебя проблемы. Если же ты видишь преимущественно синий (или родной цвет приложения) с небольшими вкраплениями зеленого и красного там, где отображаются разные переключатели или другие динамические элементы интерфейса, — все нормально.

проседания FPS на Android
Overdraw здорового приложения и overdraw курильщика

Ну и несколько советов:

  • Постарайся не использовать свойство background в лайотах.
  • Сократите количество вложенных лайотов.
  • Вставьте в начало кода Activity такую строку: getWindow().setBackgroundDrawable(null);.
  • Не используйте прозрачность там, где можно обойтись без нее.
  • Используйте инструмент Hierarchy Viewer для анализа иерархии своих лайотов, их отношений друг к другу, оценки скорости рендеринга и расчета размеров.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *