Советы и инструменты для оптимизации приложений Android

Опубликовано: 2022-03-11

Устройства Android имеют много ядер, поэтому написание плавных приложений — простая задача для всех, верно? Неправильно. Поскольку все на Android можно сделать разными способами, выбрать лучший вариант может быть непросто. Если вы хотите выбрать наиболее эффективный метод, вы должны знать, что происходит под капотом. К счастью, вам не нужно полагаться на свои чувства или обоняние, поскольку существует множество инструментов, которые могут помочь вам найти узкие места, измеряя и описывая происходящее. Правильно оптимизированные и плавные приложения значительно улучшают работу пользователя, а также меньше разряжают батарею.

Давайте сначала посмотрим на некоторые цифры, чтобы понять, насколько на самом деле важна оптимизация. Согласно сообщению Nimbledroid, 86% пользователей (включая меня) удалили приложения после того, как использовали их только один раз из-за низкой производительности. Если вы загружаете некоторый контент, у вас есть менее 11 секунд, чтобы показать его пользователю. Только каждый третий пользователь даст вам больше времени. Из-за этого вы также можете получить много плохих отзывов в Google Play.

Создавайте лучшие приложения: шаблоны производительности Android

Проверка терпения ваших пользователей — это кратчайший путь к удалению.
Твитнуть

Первое, что каждый пользователь снова и снова замечает, — это время запуска приложения. Согласно другому сообщению Nimbledroid, из 100 лучших приложений 40 запускаются менее чем за 2 секунды, а 70 запускаются менее чем за 3 секунды. Поэтому, если это возможно, вам следует отображать некоторый контент как можно скорее и немного отложить фоновые проверки и обновления.

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

Советы по производительности

Достаточно теории, вот список некоторых вещей, которые вы должны учитывать, если для вас важна производительность.

1. Строка против StringBuilder

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

 String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }

Вы можете видеть на мониторах Android Studio, насколько неэффективной может быть конкатенация некоторых строк. Происходит множество сборок мусора (GC).

Строка против StringBuilder

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

 StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();

На том же устройстве это происходит практически мгновенно, менее чем за 5 мс. Визуализация процессора и памяти почти полностью плоская, поэтому вы можете себе представить, насколько велико это улучшение. Обратите внимание, что для достижения этой разницы нам пришлось добавить 10 тысяч строк, что вы, вероятно, делаете нечасто. Поэтому, если вы добавите всего пару строк один раз, вы не увидите никаких улучшений. Кстати, если вы:

 String string = "hello" + " world";

Он внутренне преобразуется в StringBuilder, поэтому он будет работать нормально.

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

 String myString = "hello"; myString += " world";

То, что вы получите в памяти, это не 1 строка «hello world», а на самом деле 2 строки. Строка myString будет содержать «hello world», как и следовало ожидать. Однако исходная строка со значением «hello» все еще жива, без какой-либо ссылки на нее, ожидая сборки мусора. Это также причина, по которой вы должны хранить пароли в массиве символов вместо строки. Если вы храните пароль в виде строки, он останется в памяти в удобочитаемом формате до следующего GC в течение непредсказуемого промежутка времени. Возвращаясь к описанной выше неизменяемости, String останется в памяти, даже если вы присвоите ей другое значение после ее использования. Если вы, однако, очистите массив символов после использования пароля, он исчезнет отовсюду.

2. Выбор правильного типа данных

Прежде чем вы начнете писать код, вы должны решить, какие типы данных вы будете использовать для своей коллекции. Например, вы должны использовать Vector или ArrayList ? Ну, это зависит от вашего варианта использования. Если вам нужна потокобезопасная коллекция, которая позволит одновременно работать с ней только одному потоку, вам следует выбрать Vector , так как он синхронизирован. В других случаях вам, вероятно, следует придерживаться ArrayList , если у вас действительно нет особой причины использовать векторы.

Как насчет случая, когда вам нужна коллекция с уникальными объектами? Что ж, вам, вероятно, следует выбрать Set . Они не могут содержать дубликатов по дизайну, поэтому вам не придется заботиться об этом самостоятельно. Существует несколько типов наборов, поэтому выберите тот, который соответствует вашему варианту использования. Для простой группы уникальных элементов вы можете использовать HashSet . Если вы хотите сохранить порядок элементов, в котором они были вставлены, выберите LinkedHashSet . TreeSet сортирует элементы автоматически, поэтому вам не придется вызывать для него какие-либо методы сортировки. Он также должен эффективно сортировать элементы, и вам не нужно думать об алгоритмах сортировки.

Данные преобладают. Если вы выбрали правильные структуры данных и хорошо все организовали, алгоритмы почти всегда будут очевидны. Структуры данных, а не алгоритмы, занимают центральное место в программировании.
- 5 правил программирования Роба Пайка

Сортировка целых чисел или строк довольно проста. Однако что, если вы хотите отсортировать класс по некоторому свойству? Допустим, вы пишете список блюд, которые вы едите, и сохраняете их названия и метки времени. Как бы вы отсортировали блюда по метке времени от самого низкого до самого высокого? К счастью, это довольно просто. Достаточно реализовать интерфейс Comparable в классе Meal и переопределить функцию compareTo() . Чтобы отсортировать блюда от самой низкой временной метки до самой высокой, мы могли бы написать что-то вроде этого.

 @Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }

3. Обновления местоположения

Существует множество приложений, которые собирают данные о местоположении пользователя. Для этого вам следует использовать Google Location Services API, который содержит множество полезных функций. О его использовании есть отдельная статья, поэтому повторяться не буду.

Я просто хотел бы подчеркнуть некоторые важные моменты с точки зрения производительности.

Прежде всего, используйте только самое точное местоположение, которое вам нужно. Например, если вы делаете прогноз погоды, вам не нужно самое точное местоположение. Получение только очень грубой области на основе сети происходит быстрее и эффективнее батареи. Вы можете добиться этого, установив приоритет LocationRequest.PRIORITY_LOW_POWER .

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

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

И, наконец, третье правило — запрашивать обновления местоположения только в том случае, если они вам нужны. Если каждые x секунд вы показываете какие-то близлежащие объекты на карте, а приложение переходит в фоновый режим, вам не нужно знать новое местоположение. Нет смысла обновлять карту, если пользователь все равно ее не видит. Обязательно перестаньте прослушивать обновления местоположения, когда это уместно, предпочтительно в onPause() . Затем вы можете возобновить обновления в onResume() .

4. Сетевые запросы

Существует высокая вероятность того, что ваше приложение использует Интернет для загрузки или выгрузки данных. Если это так, у вас есть несколько причин обратить внимание на обработку сетевых запросов. Одним из них являются мобильные данные, которые очень ограничены для большого количества людей, и вы не должны тратить их впустую.

Второй - батарея. И Wi-Fi, и мобильные сети могут потреблять довольно много, если они используются слишком часто. Допустим, вы хотите скачать 1 кб. Чтобы сделать сетевой запрос, вы должны разбудить сотовую связь или радио WiFi, после чего вы можете загрузить свои данные. Однако магнитола не уснет сразу после операции. Он будет оставаться в довольно активном состоянии еще около 20-40 секунд, в зависимости от вашего устройства и оператора.

Сетевые запросы

Итак, что вы можете с этим поделать? Партия. Чтобы не будить радио каждые пару секунд, предварительно загрузите то, что может понадобиться пользователю в ближайшие минуты. Правильный способ пакетной обработки очень динамичен в зависимости от вашего приложения, но если это возможно, вы должны загрузить данные, которые могут понадобиться пользователю в течение следующих 3-4 минут. Можно также отредактировать параметры пакета в зависимости от типа интернета пользователя или состояния зарядки. Например, если пользователь использует Wi-Fi во время зарядки, вы можете предварительно получить гораздо больше данных, чем если бы пользователь был в мобильном Интернете с низким зарядом батареи. Принятие во внимание всех этих переменных может быть сложной задачей, на которую способны лишь немногие. К счастью, на помощь приходит GCM Network Manager!

GCM Network Manager — действительно полезный класс с множеством настраиваемых атрибутов. Вы можете легко планировать как повторяющиеся, так и разовые задачи. При повторяющихся задачах можно установить как самый низкий, так и самый высокий интервал повторения. Это позволит группировать не только ваши запросы, но и запросы от других приложений. Радио должно быть разбужено только один раз в какой-то период, и пока оно работает, все приложения в очереди загружают и загружают то, что им положено. Этот диспетчер также знает о типе сети устройства и состоянии зарядки, поэтому вы можете настроить его соответствующим образом. Вы можете найти более подробную информацию и образцы в этой статье, я призываю вас ознакомиться с ней. Пример задачи выглядит так:

 Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();

Кстати, начиная с Android 3.0, если вы выполняете сетевой запрос в основном потоке, вы получите NetworkOnMainThreadException . Это определенно предупредит вас, чтобы вы не делали этого снова.

5. Отражение

Отражение — это способность классов и объектов проверять свои собственные конструкторы, поля, методы и т. д. Обычно он используется для обратной совместимости, чтобы проверить, доступен ли данный метод для конкретной версии ОС. Если вам нужно использовать отражение для этой цели, обязательно кэшируйте ответ, так как использование отражения довольно медленное. Некоторые широко используемые библиотеки также используют Reflection, например Roboguice для внедрения зависимостей. Вот почему вам следует предпочесть Dagger 2. Подробнее об отражении вы можете прочитать в отдельной статье.

6. Автобокс

Автоупаковка и распаковка — это процессы преобразования примитивного типа в объектный тип или наоборот. На практике это означает преобразование int в Integer. Для этого компилятор использует внутреннюю функцию Integer.valueOf() . Преобразование не просто медленное, объекты также занимают намного больше памяти, чем их примитивные эквиваленты. Давайте посмотрим на некоторый код.

 Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Хотя это занимает в среднем 500 мс, переписывание, чтобы избежать автобоксинга, значительно ускорит его.

 int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Это решение работает со скоростью около 2 мс, что в 25 раз быстрее. Если не верите мне, проверьте. Числа, очевидно, будут разными для каждого устройства, но все равно это должно быть намного быстрее. И это также очень простой шаг для оптимизации.

Хорошо, вы, вероятно, не часто создаете переменную типа Integer. Но что делать в тех случаях, когда его сложнее избежать? Как на карте, где вы должны использовать объекты, такие как Map<Integer, Integer> ? Посмотрите на решение, которое используют многие люди.

 Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }

Вставка 100 000 случайных целых чисел в карту занимает около 250 мс. Теперь посмотрим на решение с SparseIntArray.

 SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }

Это занимает намного меньше, примерно 50 мс. Это также один из самых простых способов повышения производительности, так как ничего сложного делать не нужно, а код остается читабельным. При запуске ясного приложения с первым решением у меня ушло 13 МБ памяти, а использование примитивных целых чисел заняло что-то менее 7 МБ, так что только половина.

SparseIntArray — это лишь одна из замечательных коллекций, которые могут помочь вам избежать автоматической упаковки. Карта типа Map<Integer, Long> может быть заменена SparseLongArray , так как значение карты имеет тип Long . Если вы посмотрите на исходный код SparseLongArray , то увидите кое-что довольно интересное. Под капотом это просто пара массивов. Вы также можете использовать SparseBooleanArray аналогичным образом.

Если вы читали исходный код, то могли заметить примечание о том, что SparseIntArray может работать медленнее, чем HashMap . Я много экспериментировал, но для меня SparseIntArray всегда был лучше как с точки зрения памяти, так и с точки зрения производительности. Я думаю, вам все еще решать, что вы выберете, поэкспериментируйте со своими вариантами использования и посмотрите, что вам больше всего подходит. Обязательно держите в голове SparseArrays при использовании карт.

7. При рисовании

Как я сказал выше, когда вы оптимизируете производительность, вы, вероятно, увидите наибольшую выгоду от оптимизации часто выполняемого кода. Одна из часто выполняемых функций — onDraw() . Возможно, вас не удивит, что он отвечает за отрисовку видов на экране. Поскольку устройства обычно работают со скоростью 60 кадров в секунду, функция запускается 60 раз в секунду. У каждого кадра есть 16 мс на полную обработку, включая его подготовку и отрисовку, поэтому вам действительно следует избегать медленных функций. Только основной поток может рисовать на экране, поэтому вам следует избегать выполнения над ним дорогостоящих операций. Если вы заморозите основной поток на несколько секунд, вы можете получить печально известный диалог Application Not Responding (ANR). Для изменения размера изображений, работы с базой данных и т. д. используйте фоновый поток.

Если вы думаете, что ваши пользователи не заметят падения частоты кадров, вы ошибаетесь!
Твитнуть

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

Одна из вещей, которых вам следует избегать в onDraw() , — это выделение объектов, таких как Paint. Подготовьте все в конструкторе, чтобы было готово при рисовании. Даже если вы оптимизировали onDraw() , вы должны вызывать его так часто, как это необходимо. Что может быть лучше вызова оптимизированной функции? Ну, вообще не вызывая никакой функции. Если вы хотите нарисовать текст, есть довольно удобная вспомогательная функция, называемая drawText() , где вы можете указать такие вещи, как текст, координаты и цвет текста.

8. Вьюхолдеры

Вы, наверное, знаете это, но я не могу пропустить его. Шаблон проектирования Viewholder — это способ сделать прокрутку списков более плавной. Это своего рода кэширование представлений, которое может серьезно сократить вызовы findViewById() и раздувание представлений путем их сохранения. Это может выглядеть примерно так.

 static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }

Затем внутри функции getView() вашего адаптера вы можете проверить, есть ли у вас пригодное для использования представление. Если нет, вы создаете его.

 ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");

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

9. Изменение размера изображений

Скорее всего, ваше приложение будет содержать несколько изображений. Если вы загружаете некоторые файлы JPG из Интернета, они могут иметь очень большое разрешение. Однако устройства, на которых они будут отображаться, будут намного меньше. Даже если вы делаете снимок камерой своего устройства, его необходимо уменьшить перед отображением, поскольку разрешение фотографии намного больше, чем разрешение дисплея. Изменение размера изображений перед их отображением является важной вещью. Если вы попытаетесь отобразить их в полном разрешении, у вас довольно быстро закончится память. В документации по Android много написано об эффективном отображении растровых изображений, я попытаюсь подытожить.

Итак, у вас есть растровое изображение, но вы ничего о нем не знаете. К вашим услугам полезный флаг растровых изображений, называемый inJustDecodeBounds, который позволяет узнать разрешение растрового изображения. Предположим, что ваше растровое изображение имеет размер 1024x768, а ImageView, используемый для его отображения, имеет размер всего 400x300. Вы должны продолжать делить разрешение растрового изображения на 2, пока оно не станет больше, чем заданный ImageView. Если вы это сделаете, растровое изображение будет уменьшено в 2 раза, что даст вам растровое изображение 512x384. Битовая карта с пониженной частотой дискретизации использует в 4 раза меньше памяти, что очень поможет вам избежать знаменитой ошибки OutOfMemory.

Теперь, когда вы знаете, как это сделать, вы не должны этого делать. … По крайней мере, если ваше приложение сильно зависит от изображений, и это не просто 1-2 изображения. Определенно избегайте таких вещей, как изменение размера и переработка изображений вручную, используйте для этого сторонние библиотеки. Самые популярные из них — Picasso от Square, Universal Image Loader, Fresco от Facebook или мой любимый Glide. Вокруг него существует огромное активное сообщество разработчиков, поэтому вы также можете найти много полезных людей в разделе проблем на GitHub.

10. Строгий режим

Строгий режим — довольно полезный инструмент разработчика, о котором многие люди не знают. Обычно он используется для обнаружения сетевых запросов или обращений к диску из основного потока. Вы можете установить, какие проблемы должен искать строгий режим и какое наказание он должен вызывать. Образец Google выглядит следующим образом:

 public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }

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

Производительность отладки: профессиональный подход

Давайте теперь рассмотрим некоторые инструменты, которые могут помочь вам найти узкие места или, по крайней мере, показать, что что-то не так.

1. Android-монитор

Это инструмент, встроенный в Android Studio. По умолчанию вы можете найти монитор Android в левом нижнем углу, и вы можете переключаться между двумя вкладками. Логкэт и мониторы. Раздел «Мониторы» содержит 4 разных графика. Сеть, процессор, графический процессор и память. Они довольно понятны, поэтому я просто быстро пройдусь по ним. Вот скриншот графиков, сделанных при разборе некоторого JSON по мере его загрузки.

Android-монитор

Часть «Сеть» показывает входящий и исходящий трафик в КБ/с. Часть ЦП отображает загрузку ЦП в процентах. Монитор графического процессора показывает, сколько времени требуется для рендеринга кадров окна пользовательского интерфейса. Это самый детализированный монитор из этих 4, поэтому, если вы хотите узнать о нем больше, прочтите это.

Наконец, у нас есть монитор памяти, который вы, вероятно, будете использовать чаще всего. По умолчанию показывает текущий объем свободной и выделенной памяти. Вы также можете принудительно выполнить сборку мусора, чтобы проверить, уменьшается ли объем используемой памяти. Он имеет полезную функцию под названием Dump Java Heap, которая создает файл HPROF, который можно открыть с помощью средства просмотра и анализа HPROF. Это позволит вам увидеть, сколько объектов вы выделили, сколько памяти занято и, возможно, какие объекты вызывают утечку памяти. Научиться пользоваться этим анализатором — не самая простая задача, но оно того стоит. Следующее, что вы можете сделать с монитором памяти, — это отследить распределение по времени, которое вы можете запускать и останавливать по своему усмотрению. Это может быть полезно во многих случаях, например, при прокрутке или вращении устройства.

2. Перегрузка графического процессора

Это простой вспомогательный инструмент, который вы можете активировать в параметрах разработчика после включения режима разработчика. Выберите «Отладка перерисовки графического процессора», «Показать области перерисовки», и ваш экран приобретет странные цвета. Это нормально, это то, что должно произойти. Цвета означают, сколько раз конкретная область была перерисована. Истинный цвет означает, что не было перерисовки, это то, к чему нужно стремиться. Синий означает один овердрафт, зеленый — два, розовый — три, красный — четыре.

Перегрузка графического процессора

Хотя лучше всего видеть истинный цвет, вы всегда будете видеть некоторые перерисовки, особенно вокруг текстов, навигационных ящиков, диалогов и многого другого. Так что не пытайтесь избавиться от него полностью. Если ваше приложение голубоватое или зеленоватое, это, вероятно, нормально. Однако, если вы видите слишком много красного на некоторых простых экранах, вам следует выяснить, что происходит. Возможно, будет слишком много фрагментов, наложенных друг на друга, если вы будете добавлять их вместо замены. Как я уже упоминал выше, рисование — самая медленная часть приложений, поэтому нет смысла рисовать что-то, если на нем будет нарисовано более 3 слоев. Не стесняйтесь проверять свои любимые приложения с ним. Вы увидите, что даже у приложений с более чем миллиардом загрузок есть красные области, так что не торопитесь, когда пытаетесь оптимизировать.

3. Рендеринг на графическом процессоре

Это еще один инструмент из параметров разработчика, называемый рендерингом Profile GPU. Выбрав его, выберите «На экране в виде полос». Вы заметите, что на экране появятся цветные полосы. Поскольку у каждого приложения есть отдельные панели, странным образом строка состояния имеет свои собственные панели, а если у вас есть программные навигационные кнопки, у них тоже есть свои собственные панели. В любом случае, полосы обновляются по мере того, как вы взаимодействуете с экраном.

Графический рендеринг

Полосы состоят из 3-4 цветов, и, согласно документации Android, их размер действительно имеет значение. Чем меньше, тем лучше. Внизу у вас есть синий цвет, который представляет время, затраченное на создание и обновление списков отображения представления. Если эта часть слишком высокая, это означает, что в ней много рисунков пользовательского вида или много работы, выполненной в onDraw() . Если у вас Android 4.0+, вы увидите фиолетовую полосу над синей. Это представляет собой время, затраченное на передачу ресурсов потоку рендеринга. Затем идет красная часть, которая представляет время, затрачиваемое модулем 2D-рендеринга Android на выдачу команд OpenGL для рисования и перерисовки списков отображения. Вверху находится оранжевая полоса, которая представляет время, в течение которого ЦП ожидает завершения работы графического процессора. Если он слишком высокий, приложение слишком много работает с графическим процессором.

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

4. Просмотр иерархии

Это один из моих любимых инструментов, так как он действительно мощный. Вы можете запустить его из Android Studio через Tools -> Android -> Android Device Monitor, или он также находится в вашей папке sdk/tools как «монитор». Вы также можете найти там автономный исполняемый файл hierarachyviewer, но, поскольку он устарел, вам следует открыть монитор. Как бы вы ни открывали Android Device Monitor, переключитесь на проекцию Hierarchy Viewer. Если вы не видите ни одного запущенного приложения, назначенного вашему устройству, вы можете исправить это несколькими способами. Также попробуйте проверить эту ветку проблем, там есть люди со всеми видами проблем и всевозможными решениями. Что-то должно сработать и у вас.

С помощью Hierarchy Viewer вы можете получить действительно четкий обзор ваших иерархий представлений (очевидно). Если вы видите каждый макет в отдельном XML, вы можете легко обнаружить бесполезные представления. Однако, если вы продолжаете комбинировать макеты, это может легко привести к путанице. Такой инструмент позволяет легко обнаружить, например, какой-то RelativeLayout, у которого есть только 1 дочерний элемент, другой RelativeLayout. Это делает один из них съемным.

Избегайте вызова requestLayout() , так как это вызывает обход всей иерархии представлений, чтобы узнать, насколько большим должно быть каждое представление. Если есть какой-то конфликт с измерениями, иерархия может быть пройдена несколько раз, что, если произойдет во время какой-либо анимации, обязательно приведет к пропуску некоторых кадров. Если вы хотите узнать больше о том, как Android рисует свои представления, вы можете прочитать это. Давайте посмотрим на одно представление, как оно видно в средстве просмотра иерархии.

Просмотрщик иерархии

В правом верхнем углу находится кнопка для максимального предварительного просмотра конкретного вида в отдельном окне. Под ним вы также можете увидеть фактический предварительный просмотр представления в приложении. Следующий элемент — это число, которое представляет, сколько дочерних элементов имеет данное представление, включая само представление. Если вы выберете узел (предпочтительно корневой) и нажмете «Получить время макета» (3 цветных кружка), у вас будет заполнено еще 3 значения вместе с цветными кружками, помеченными как «мера», «макет» и «рисовать». Неудивительно, что этап измерения представляет собой время, которое потребовалось для измерения данного представления. Фаза компоновки — это время рендеринга, а рисование — это фактическая операция рисования. Эти значения и цвета соотносятся друг с другом. Зеленый означает, что представление отображается в верхних 50% всех представлений в дереве. Желтый означает рендеринг в более медленных 50% всех представлений в дереве, красный означает, что данное представление является одним из самых медленных. Поскольку эти значения относительные, всегда будут красные. Вы просто не можете избежать их.

Под значениями у вас есть имя класса, например «TextView», внутренний идентификатор представления объекта и android:id представления, которые вы установили в файлах XML. Я призываю вас выработать привычку добавлять идентификаторы во все представления, даже если вы не ссылаетесь на них в коде. Это значительно упростит идентификацию представлений в средстве просмотра иерархии, а если в вашем проекте есть автоматизированные тесты, это также значительно ускорит нацеливание на элементы. Это сэкономит вам и вашим коллегам время на их написание. Добавление идентификаторов к элементам, добавленным в XML-файлы, довольно просто. А как насчет динамически добавляемых элементов? Что ж, оказывается, это тоже очень просто. Просто создайте файл ids.xml в папке значений и введите необходимые поля. Это может выглядеть так:

 <resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>

Затем в коде вы можете использовать setId(R.id.item_title) . Это не может быть проще.

Есть еще пара вещей, на которые следует обратить внимание при оптимизации пользовательского интерфейса. Как правило, следует избегать глубоких иерархий, предпочитая неглубокие, а может быть, и широкие. Не используйте макеты, которые вам не нужны. Например, вы, вероятно, можете заменить группу вложенных LinearLayouts на RelativeLayout или TableLayout . Не стесняйтесь экспериментировать с различными макетами, не всегда используйте LinearLayout и RelativeLayout . Также попробуйте создать несколько пользовательских представлений, когда это необходимо, это может значительно повысить производительность, если все сделано правильно. Например, знаете ли вы, что Instagram не использует TextView для отображения комментариев?

Вы можете найти дополнительную информацию о средстве просмотра иерархии на сайте разработчиков Android с описаниями различных панелей, использования инструмента Pixel Perfect и т. д. Еще одна вещь, на которую я хотел бы обратить внимание, — это захват представлений в файле .psd, который можно сделать с помощью кнопку «Захват слоев окна». Каждый вид будет на отдельном слое, поэтому его очень просто скрыть или изменить в Photoshop или GIMP. О, это еще одна причина добавить идентификатор к каждому представлению, которое вы можете. Это заставит слои иметь имена, которые действительно имеют смысл.

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

Сайт разработчиков Android содержит набор рекомендаций по повышению производительности. Они охватывают множество различных областей, включая управление памятью, о котором я особо не говорил. Я его молча проигнорил, потому что работа с памятью и отслеживание утечек памяти — это целая отдельная история. Использование сторонней библиотеки для эффективного отображения изображений очень поможет, но если у вас все еще есть проблемы с памятью, посмотрите Leak canary, созданную Square, или прочитайте это.

Подведение итогов

Итак, это была хорошая новость. Плохая новость заключается в том, что оптимизация приложений для Android намного сложнее. Есть много способов сделать все, поэтому вы должны быть знакомы с плюсами и минусами каждого из них. Как правило, не существует серебряной пули, которая имеет только преимущества. Только поняв, что происходит за кулисами, вы сможете выбрать наиболее подходящее для вас решение. Просто потому, что ваш любимый разработчик говорит, что что-то хорошо, это не обязательно означает, что это лучшее решение для вас. Есть гораздо больше областей для обсуждения и больше инструментов профилирования, которые являются более продвинутыми, поэтому мы можем вернуться к ним в следующий раз.

Убедитесь, что вы учитесь у лучших разработчиков и ведущих компаний. Вы можете найти пару сотен инженерных блогов по этой ссылке. Очевидно, что это не только материалы, связанные с Android, поэтому, если вас интересует только Android, вам нужно отфильтровать конкретный блог. Я очень рекомендую блоги Facebook и Instagram. Несмотря на то, что пользовательский интерфейс Instagram на Android вызывает сомнения, в их инженерном блоге есть несколько действительно крутых статей. Для меня удивительно, что так легко увидеть, как дела устроены в компаниях, которые ежедневно обслуживают сотни миллионов пользователей, поэтому не читать их блоги кажется безумием. Мир меняется очень быстро, поэтому, если вы не пытаетесь постоянно совершенствоваться, учиться у других и использовать новые инструменты, вы останетесь позади. Как сказал Марк Твен, человек, который не читает, не имеет преимуществ перед тем, кто не умеет читать.