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

frizy i prosedaniya fps na android18 Фризи і просідання FPS на Android

В Android тільки головний потік програми має право оновлювати екран і обробляти натискання на екран. Це означає, що, коли твоє додаток займається складною роботою в головному потоці, воно не має можливості реагувати на натискання. Для користувача додаток буде виглядати завислим. У даному випадку знову ж таки допоможе винос складних операцій в окремі потоки.

Є, однак, набагато більш тонкий і неочевидний момент. Android оновлює екран зі швидкістю 60 FPS. Це означає, що під час показу анімації або промотки списків у нього є всього 16,6 мс на відображення кожного кадру. У більшості випадків Android справляється з цією роботою і не втрачає кадрів. Але некоректно написане додаток може загальмувати його.

frizy i prosedaniya fps na android19 Фризи і просідання 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. Після його включення екран перефарбується в різні кольори, які означають наступне:

  • звичайний колір — одинарне накладення;
  • синій — подвійне накладення;
  • зеленій — потрійне;
  • червоний — четверте і більше.

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

frizy i prosedaniya fps na android20 Фризи і просідання FPS на AndroidOverdraw здорового програми та overdraw курця

Ну і кілька порад:

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