В этой небольшой статье я хочу поделиться с вами опытом, как программно оптимизировать производительность приложения Андроид за 5 простых шагов на примере создания цифровой версии игры «Корона Эмбера».
До создания серьезных приложения со сложной структурой View и Layout'ов мы особо не задумывались над тем, как простые и логичные действия в стиле «смотрите, я набросал дизайн из лэйаутов» могут серьезно замедлить работу всей программы.
Помимо прочего, задача с «Короной Эмбера» осложнялась еще и тем, что игра, которую мы задумали перенести на Андроид платформу, была сама по себе достаточно насыщенной различными компонентами, которые как-то надо было умещать на игровом поле или рядом с ним.
В статье я собрал наш успешный опыт и облёк его в удобную и читабельную форму, полезную для тех, кто все еще гуглит «как программно оптимизировать приложение под Андроид» или «почему мое приложение лагает».
Итак. Исходная точка (то, как это все выглядело ДО оптимизации)
Дизайн приложения был создан из «правильной» кучи около двух сотен View и десятка Layout'ов. Загрузка игрового поля происходила около 5 секунд, а почти каждое действие зависало еще на 1-2 секунды. И если на прогрессивных устройствах и эмуляторах лаги были практически незаметны, то на большинстве менее современных устройств картина выглядела достаточно печальной.
Понятно, что это не устраивало ни меня, ни нашу команду, ни тестеров. И хотя мы были искреннее уверены, что все оптимизировано дальше некуда, мы принялись искать информацию.
Часть знаний, которыми я хочу поделиться, мы нашли здесь — классные видео-уроки с русскими субтитрами (рекомендую), часть — на Хабре, часть — в глубинах Интернета, на том же сайте Google Developers.
Причем, каждый раз получая новую порцию и проводя очередную оптимизацию, мы были уверены «все теперь дальше некуда, быстрее не будет» и с каждым шагом открывали для себя все больше и больше нового.
И вот, как это происходило.
Шаг 1. Измерение
По сути это даже не шаг, а настойчивая рекомендация постоянно измерять свою производительность. Для этого в Андроид Студио предусмотрено несколько специальных программ: Android Device Monitor с HierarhyViewer, SystemTracing, Method Profiling; Android Monitor с Memory, CPU и GPU мониторами. Описывать их работу не цель данной статьи, вы легко можете найти гайды здесь же, на Хабре, или в упомянутых видео-уроках.
Суть в том, что чтобы оптимизировать производительность, нужно сначала понять где же она «спотыкается».
Хотя, даже без измерения, есть несколько вещей, на которые необходимо обращать внимание каждому разработчику и мы шагаем дальше.
Шаг 2. Оптимизация иерархии и снижение веса
Основная потеря производительности приложений происходит при пересчете и перерисовке отображенных Layout'ов. Причем чем «тяжелее» лэйаут — тем дольше происходит перерасчет его показателей. И тут надо обратить внимание на следующие моменты:
— в вашей иерархии не должно быть вложенных LinearLayout с параметрами «weigh» (вес). Измерение такого лэйаута занимает в два (три, четыре! — в зависимости от вложенности) больше времени. Лучше заменить эту конструкцию на GridLayout (или support.v7.GridLayout для API меньше 21);
— некоторые LinearLayout можно заменить на RelativeLayout, тем самым убрав дополнительные вложения, например;
— оказывается, RelativeLayout также измеряется дважды. Где возможно — его нужно заменить на более «легкий» FrameLayout.
Пример:
Изучив нашу разметку мы с ужасом обнаружили, что в ней есть вложенные до четырех (!) раз LinearLayout с параметром вес, игровое поле состоит из почти сотни RelativeLayout (клетки поля), а некоторые Layout просто не нужны.
(Зеленым отмечено допустимое вложение, желтым — нежелательное, оранжевым — опасное, красным — «никогда-так-не-делайте!»)
После этого мы немного переработали структуру. «Персонажа» вынесли в отдельный фрагмент с RelativeLayout, все вложенные Linear заменили на (один!) support.v7.GridLayout, а все RelativeLayout (на скрине их не видно — это клетки поля) заменили на FrameLayout. Получилось и симпатишней и производительней.
Однако до конца наши проблемы это не решило, и мы пошли дальше.
Шаг 3. Лишняя перерисовка (Overdraw)
Оказалось, что Андроид тщательно прорисовывает каждую картинку, каждый background у каждого View на экране. Здесь он, конечно, молодец. Но что делать, если одни изображения перекрывают другие, бэкграунды накладываются и часть этой прорисовки становится абсолютно ненужной? Правильно — отключать все, что не видно и не нужно.
Измерить наложение перерисовки оказывается очень просто: на своем устройстве в «Параметрах разработчика» нужно включить «Показывать превышение GPU», запустить приложение и посмотреть на экран.
Цвета View-элементов покажут, насколько у вас все хорошо (или плохо).
Свой цвет — уровень 0, «идеально»
Синий цвет — уровень 1, «нормально.
Зеленый цвет — уровень 2, „допустимо“.
Светло-красный цвет — уровень 3, „нежелательно“.
Красный цвет — уровень 4+, „не дай бог“.
Наша картинка вновь ввергла нас в ужас:
Нам пришлось снова пройтись по всей разметке и отключить background там, где он оказался лишним. Не забыли и про основной бэк приложения — одной простой строчкой:
<item name="android:windowBackground">@null</item>
В теме приложения выключили и его. И вот что получилось в итоге. Может и не везде идеально, но вполне допустимо. (Почти все View опустились на 3 уровня!)
Шаг 4. Кэширование — ключ к успеху
Тоже вполне очевидная вещь, которая не казалась обязательной — это кэширование. В частности (в данном случае) кэширование шрифта.
Как вы уже наверное обратили внимание, везде, включая логи, используется свой шрифт и, о (опять) ужас!, каждый раз, для каждого нового текста он подгружается заново из Assets. Каждый раз, Карл! (черным цветом — основной потом, коричневым — работа Assesnager'а).
С кэшированием до этого никто из нас не сталкивался, но тут нас выручил один хороший человек со StackOverflow и подсказал простой код:
private static final Hashtable<String, Typeface> cache = new Hashtable<String, Typeface>();
public static Typeface get(Context c, String name) {
synchronized (cache) {
if (!cache.containsKey(name)) {
String path = "fonts/" + name;
try {
Typeface t = Typeface.createFromAsset(c.getAssets(), path);
cache.put(name, t);
} catch (Exception e) {
e.printStackTrace();
}
}
return cache.get(name);
}
}
Применив который, лично я вообще практически забыл, что такое лаги.
Шаг 5. „А это — под укроп!“
Как в анекдоте, где мужику сказали, что он получит под надел столько земли, сколько сам сможет измерить и он скакал, бежал, шел, полз и в конце изможденный снял с себя шапку и кинул на сколько смог со словами: „А это — под укроп“, так и мы стремились максимально до мельчайших деталей оптимизировать производительность.
Поэтому пришло время оптимизации кода. Конечно, то как пишется код — это уже больше дело правильной привычки, и тут можно только дать несколько общих рекомендаций (более подробно их можно найти в любой статье по программной оптимизации). Вот с чем столкнулась наша команда:
— создавайте поменьше „ненужных“ циклов. Если что-то можно сделать без них — делайте без них;
— не создавайте переменных в цикле. Лучше создать их за его пределами, а в цикле просто обновлять;
— если есть необходимость обратиться к ресурсам неоднократно (с помощью gerResources() или же просто в другой класс/массив, например, myList.get (0)) — лучше запишите полученный результат в новую переменную и используйте ее: int myItem = myList.get(0); — это тоже подарит вашему приложению миллисекунды свободного времени.
Подводя итог
Уверен, что сделанные шаги не являются окончательными и всегда будет „место под укроп“, до которого мы не добрались. Всегда готов узнать о шаге №6, 7… и даже №157, если вы захотите ими поделиться в комментариях. Более того, за „науку“ буду благодарен.
А пока, резюмируем, что же нужно сделать, чтобы ваше приложение начало „летать“:
1. Измерьте производительность своего приложения и найдите источник проблемы. Может у вас это будет не шрифт, а, например, утечка памяти (включаем Memory Monitor);
2. Оптимизируйте структуру и иерархию View (включаем HierarhyViewer и здравый смысл);
3. Убираем везде все ненужные бэки, включая background приложения (в этом случае учтите, что для корректного отображения вам все равно понадобится общий бэк);
4. Смотрим на часто используемые ресурсы/загрузки/картинки/шрифты и кэшируем их;
5. Делаем оптимизацию программного кода.
В итоге, после всех этих шагов, картинка трассинга приложения стала выглядеть вот так:
задержки практически исчезли (или стали незаметны), что нас вполне устроило.
Ну и конечно, первая мысль, которая пришла ко мне в голову после всей нашей оптимизации: „Вау! Теперь же можно добавить еще больше View и Layout на основной экран!“
facepalm, занавес…
Автор: Abrog