Пять шагов по оптимизации производительности приложения для Андроид

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

До создания серьезных приложения со сложной структурой View и Layout'ов мы особо не задумывались над тем, как простые и логичные действия в стиле «смотрите, я набросал дизайн из лэйаутов» могут серьезно замедлить работу всей программы.

Помимо прочего, задача с «Короной Эмбера» осложнялась еще и тем, что игра, которую мы задумали перенести на Андроид платформу, была сама по себе достаточно насыщенной различными компонентами, которые как-то надо было умещать на игровом поле или рядом с ним.

В статье я собрал наш успешный опыт и облёк его в удобную и читабельную форму, полезную для тех, кто все еще гуглит «как программно оптимизировать приложение под Андроид» или «почему мое приложение лагает».

Итак. Исходная точка (то, как это все выглядело ДО оптимизации)


Дизайн приложения был создан из «правильной» кучи около двух сотен View и десятка Layout'ов. Загрузка игрового поля происходила около 5 секунд, а почти каждое действие зависало еще на 1-2 секунды. И если на прогрессивных устройствах и эмуляторах лаги были практически незаметны, то на большинстве менее современных устройств картина выглядела достаточно печальной.

image

Понятно, что это не устраивало ни меня, ни нашу команду, ни тестеров. И хотя мы были искреннее уверены, что все оптимизировано дальше некуда, мы принялись искать информацию.

Часть знаний, которыми я хочу поделиться, мы нашли здесь — классные видео-уроки с русскими субтитрами (рекомендую), часть — на Хабре, часть — в глубинах Интернета, на том же сайте 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 просто не нужны.

(Зеленым отмечено допустимое вложение, желтым — нежелательное, оранжевым — опасное, красным — «никогда-так-не-делайте!»)

image

После этого мы немного переработали структуру. «Персонажа» вынесли в отдельный фрагмент с RelativeLayout, все вложенные Linear заменили на (один!) support.v7.GridLayout, а все RelativeLayout (на скрине их не видно — это клетки поля) заменили на FrameLayout. Получилось и симпатишней и производительней.

image

Однако до конца наши проблемы это не решило, и мы пошли дальше.

Шаг 3. Лишняя перерисовка (Overdraw)


Оказалось, что Андроид тщательно прорисовывает каждую картинку, каждый background у каждого View на экране. Здесь он, конечно, молодец. Но что делать, если одни изображения перекрывают другие, бэкграунды накладываются и часть этой прорисовки становится абсолютно ненужной? Правильно — отключать все, что не видно и не нужно.

Измерить наложение перерисовки оказывается очень просто: на своем устройстве в «Параметрах разработчика» нужно включить «Показывать превышение GPU», запустить приложение и посмотреть на экран.

Цвета View-элементов покажут, насколько у вас все хорошо (или плохо).

Свой цвет — уровень 0, «идеально»
Синий цвет — уровень 1, «нормально.
Зеленый цвет — уровень 2, „допустимо“.
Светло-красный цвет — уровень 3, „нежелательно“.
Красный цвет — уровень 4+, „не дай бог“.

Наша картинка вновь ввергла нас в ужас:

image

Нам пришлось снова пройтись по всей разметке и отключить 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, занавес…
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 14
  • 0
    Я не так давно оторвался от нуля в разработке под Android)) Хоть и мое приложение будет очень легким — все равно сохраню в закладки, проверю, когда оно будет готово)
    Спасибо!
    • +3
      Спасибо за статью!
      Минутка рекламы хорошего сервиса, для тех, кто печется о производительности android приложений: https://nimbledroid.com/
      • 0
        логинилка через гугл не работает у вас
        • 0
          У меня сработала.
          • 0
            Добрый день, если есть желание, напишите пожалуйста в личку свою почту / ОС / браузер, попробуем разобраться почему у Вас не работает.
            • 0
              Фокус в том, чтобы сначала вбить почту и пароль.
            • 0
              "Hooray! No Memory Leaks Found!" <- эту фразу надо вынести в шапку!
              Правда дальше второго шага по иерархии UI уйти не шмогла ). На досуге еще поэксперементирую, интересно, спасибо.
              • 0
                Если будут вопросы, то пишите, постараюсь ответить. На самом деле можно много интересного найти :) Правда часто проблемы в SDK и даже не понятно чего с этим делать, яркий тому пример это TapJoy, очень сильно замедляет старт… Может напишу пост тут в скором времени об этом.
            • +1
              Ну, раз просите добавить, добавлю немного по оптимизации кода
              1. Static everywhere. Если нет нужды использовать внутренние поля объекта (инстанса) — непременно ставьте static. Вызов становится на 15-20 процентов быстрее.
              2. Экономия на геттерах — сеттерах. Прямой доступ к полю класса быстрее геттера-сеттера в 3-7 раз (без JIT и с JIT). Поэтому во внутренних методах класса — лучше прямое обращение к полю. Если геттер-сеттер стоит в цикле — тем более.
              3. Операции с float на Android в два раза медленнее операций с int
              4. Массив примитивов намного быстрее массива объектов. К примеру, два массива int намного быстрее, чем массив объектов (int, int)

              Это из руководства Google, там есть ещё несколько советов, которые я счёл более экзотическими или слишком очевидными.
              • 0
                пункт 4 следует развернуть так — избегайте создания любых кратковременных служебных объектов. Где есть необходимость в оптимизации — эмулируйте за счёт примитивов или уже имеющихся объектов.
                Так, два параллельных массива объектов Foo() и Bar() будут работать намного быстрее, чем один массив объектов класса FooBar — матрёшки, который содержит в себе Foo() и Bar()
                • 0
                  Точно, статики! забыл упомянуть. Убирание геттеров и сеттеров почему-то сочли незначительным ускорением (хотя про них везде пишут), но ваши цифры впечатлили.
                  Добавил оба пункта "под укроп".
                  Спасибо! Особенно за цифры.
                • +2
                  Никто не упомянул библиотеку для отлавливания memory leak-ов от square — leakcanary.
                  Все, что нужно сделать, — добавить одну строчку в класс Application
                  LeakCanary.install(this);

                  Далее прямо на девайсе будут показываться утечки. Подробнее уже писали на хабре.
                  image
                  • 0
                    спасибо, затестирую в ближайшем проекте

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.