Измерение производительности работы приложения

Введение

Как правило, недостаточно написать инновационное и полезное приложение. Если оно будет зависать, долго запускаться и медленно работать, то все его достоинства будут с лёгкостью перечёркнуты этими недостатками.

Поэтому производительность приложения крайне важна, хотя о ней легко забыть, когда вы делаете дополнительные штрихи к своему UI или придумываете новые интересные функции. Ровно до тех пор, пока не появятся первые негативные отзывы в Google Play.

В этой статье мы рассмотрим, с помощью каких средств можно улучшить производительность в приложениях, чтобы сделать их качественнее, а также на примере оптимизируем отрисовку экрана в одном из наших приложений — Менеджере паролей от Wi-Fi сетей.

GPU Overdraw

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

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

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

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

Поэтому когда вы тестируете своё приложение на выявление проблем с производительностью, то лучше начинать с overdraw. Проверить приложение на наличие наложений достаточно просто. На любом Android-устройстве уже есть встроенная утилита, которая позволяет вам проверить количество наложений в любом приложении. Чтобы её запустить, вам будет нужно зайти в Настройки и выбрать «Для разработчиков». Этот раздел виден, если вы активировали на своём устройстве режим разработчика. Сделать это можно, перейдя в раздел «О телефоне» и нажав несколько раз на «Номер сборки», пока не появится уведомление о том, что вы стали разработчиком.

В настройках для разработчиков нужно будет выбрать «Отладка наложения», после чего откроется окно с параметрами отладки, в котором нужно выбрать «Показывать области наложения».

Теперь вы можете зайти в любое приложение, работу которого хотите проверить. Например, посмотрим области наложения в приложении Google Карты.

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

  • Голубой — пиксели перерисовываются один раз, всё в порядке.
  • Зелёный — пиксели перерисовываются дважды. Возможно нужна оптимизация, однако в большинстве случаев это допустимый предел наложения.
  • Светло-красный — пиксели перерисовываются трижды. Такие области могут быть неизбежны, однако если их слишком много, стоит задуматься о причине их появления.
  • Темно-красный — пиксели перерисовываются четырежды и более. Очень сильное уменьшение производительности приложения.
  • Нет цвета — область не перерисовывается.

Изучив области наложения, можно перейти к способам их уменьшения. Самый верный способ — открыть XML-файл с разметкой вашего интерфейса и искать в нём области, которые были окрашены светло-красным или темно-красным цветом, чтобы их оптимизировать.

Например, к наложению может привести атрибут background дочернего элемента. Если в качестве его значения выбран белый цвет и цвет родительского элемента тоже указан как белый, то это приведёт к избыточной перерисовке экрана.

Android может сократить количество наложений, однако это распространяется только на простые случаи. Если у вас достаточно сложный интерфейс, система не сможет понять, как вы рисуете свой контент.

Hierarchy Viewer

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

  1. Измерение
  2. Разметка
  3. Рисование

Сначала система измеряет элементы представлений. Каждое представление имеет конкретные данные, которые описывают его размер: некоторые имеют определённый размер, другие имеют размер, который адаптирует к размеру родительского контейнера. Затем система расставляет эти элементы представлений, учитывая их размеры, и отрисовывает их на экране устройства.

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

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

Во время рендеринга каждое представление предоставляет свои размеры родительскому элементу. Если родитель обнаруживает проблему этими размерами, он заставляет представления их измерить заново. Повторные измерения могут возникать даже при отсутствии ошибки. Например, разметки с RelativeLayout должны измерять свои дочерние элементы дважды, чтобы всё было в порядке. Разметки с LinearLayout, дочерние элементы которого имеют атрибут layout_weight, тоже измеряются дважды.

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

Поэтому главной целью здесь является удаление лишний и ненужных представлений в поиске возможности оптимизировать разметку.

Android SDK включает в себя утилиту Hierarchy Viewer, которая позволяет визуализировать всю вашу иерархию представлений. Этот инструмент позволяет определить как избыточные представления, так и вложенные разметки.

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

Чтобы воспользоваться утилитой, запустите приложение на устройстве или эмуляторе, затем в Android Studio откройте Tools — Android — Android Device Monitor.

В открывшемся окне вам нужно нажать на Window — Open perspective, затем в списке выбрать Hierarchy Viewer. В результате вы должны увидеть следующее окно.

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

  • Окно Tree View это основное окно, в котором вы можете найти нужный элемент и посмотреть данные о нём.
  • Окно Tree Overview представляет собой карту вашей иерархии, по которой можно смотреть, на каком участке вы находитесь.
  • Окно Layout View это представление вашей иерархии.

Эти окна связаны между собой, если вы выделите элемент в Tree View, он отобразится в двух других окнах.

Если вы не уверены, действительно ли представление вносит какой-либо вклад в окончательное изображение, просто перейдите в Tree View и щелкните по соответствующему представлению. Вы увидите предварительный просмотр того, как это представление будет отображаться на экране.

Таким образом, анализируя данные, вы сможете увидеть, как представления в вашей иерархии лишние либо работают медленно и требуют оптимизации.

Выявление утечек памяти

В Android Device Monitor также есть ещё инструмент, позволяющий собирать информацию об утечках памяти и других проблемах с памятью — это вкладка Heap.

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

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

Чтобы получить доступ к Heap, снова запустите Android Device Monitor и выберите вкладку DDMS. На панели Devices выберите устройство и процесс, который вы хотите протестировать, затем нажмите на кнопку над списком Update Heap и выберите вкладку Heap.

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

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

Systrace

Android Device Monitor, кроме всего прочего, также предоставляет утилиту Systrace. Эта утилита помогает анализировать производительность вашего приложения, захватывая и отображая время выполнения ваших процессов и других системных процессов. Systrace объединяет данные из ядра Android, такие как планировщик процессора, активность диска, и потоки приложений для генерации отчёта в HTML, который показывает общую картинку на момент захвата.

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

Чтобы воспользоваться этой утилитой, достаточно запустить Android Device Monitor, выбрать нужно устройство и нажать на Capture system wide trace using Andorid systrace.

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

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

Оптимизация отрисовки экрана

В нашем приложении Менеджер паролей от Wi Fi-сетей используется красивый анимированный плейсхолдер для пустого списка сетей, реализованный в библиотеке desertplaceholder. Исходный код вы можете посмотреть, перейдя по ссылке ранее на GitHub.

Особенность этой библиотеки в том, что в качестве контейнера она использует ConstraintLayout, а отрисовка анимации происходит с помощью Canvas в методе onDraw().

Попробуем слегка переписать код, используя вместо ConstraintLayout RelativeLayout, а вместо onDraw() нативные средства анимации Android, затем посмотрим, изменится ли время загрузки экрана.

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

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

Trace.beginSection("SectionName");
...
тестируемый код
...
Trace.endSection();

Следует учесть, что использование класса Trace требует в качестве минимальной версии SDK указать minSdkVersion 18.

Таким образом, при следующем захвате мы увидим блок в отчёте с именем SectionName и сможем увидеть длительность его выполнения. В данном случае нам нужно проверить, как быстро рисуется изображение.

Trace.beginSection("MainActivity.onCreate");
setContentView(R.layout.activity_main);
Trace.endSection();

Запустим Systrace и сгенерируем отчёт. В результате получился следующий отчет, который можно увидеть на скриншоте ниже.

В отчёте нужный блок легко найти, введя в строку поиска внутри отчёта имя секции. Увеличив блок, мы видим, где начинает загружаться собственно плейсхолдер и замеряем, что длительность выполнения занимает 13,412 мс.

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

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/placeholder"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

  <RelativeLayout
      android:id="@+id/sky"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:background="@color/background_sky"
      >
    <ImageView
        android:id="@+id/mountains"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:src="@drawable/mountains"
        />
  </RelativeLayout>
  <RelativeLayout
      android:id="@+id/desert"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:background="@color/background_desert"
      >
    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="48dp"
        android:layout_marginStart="48dp"
        android:layout_marginTop="24dp"
        android:src="@drawable/sign_bed"
        />

    <ImageView
        android:id="@+id/imageView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_alignTop="@+id/imageView2"
        android:layout_marginEnd="63dp"
        android:layout_marginRight="63dp"
        android:src="@drawable/cactus_high"
        />
    <TextView
        android:id="@+id/placeholder_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/imageView2"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="14dp"
        android:paddingLeft="48dp"
        android:paddingRight="48dp"
        android:text="@string/msg_desert_placeholder"
        style="@style/PlaceHolder.Message"
        />
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/imageView3"
        android:layout_alignStart="@+id/imageView3"
        android:layout_alignTop="@+id/imageView4"
        android:layout_marginLeft="12dp"
        android:layout_marginStart="12dp"
        android:src="@drawable/stone"
        />
    <ImageView
        android:id="@+id/imageView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignEnd="@+id/imageView2"
        android:layout_alignRight="@+id/imageView2"
        android:layout_below="@+id/placeholder_message"
        android:layout_marginEnd="21dp"
        android:layout_marginRight="21dp"
        android:layout_marginTop="16dp"
        android:src="@drawable/cactus"
        />
  </RelativeLayout>
</LinearLayout>

Для того, чтобы добавить анимацию, используются классы TranslateAnimation и RotateAnimation. Ниже представлен код добавления анимации для облаков.

public static void addCloud(final Activity context, RelativeLayout sky) {
  DisplayMetrics displaymetrics = new DisplayMetrics();
  context.getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
  final int screenWidth = displaymetrics.widthPixels;
  int screenHeight = displaymetrics.heightPixels / 2;
  List<Cloud> clouds = new ArrayList<>();
  Resources res = context.getResources();
  Bitmap cloudTop = decodeResource(res, R.drawable.cloud3);
  Bitmap cloudMiddle = decodeResource(res, R.drawable.cloud2);
  Bitmap cloudBottom = decodeResource(res, R.drawable.cloud1);
  clouds.add(new Cloud(DIRECTION_RIGHT, cloudTop, 3.2f, -1 * cloudTop.getWidth(), 30));
  clouds.add(new Cloud(DIRECTION_LEFT, cloudMiddle, 3.0f, -1 * cloudMiddle.getWidth(),
      screenHeight / 2 - cloudMiddle.getHeight() / 2));
  clouds.add(new Cloud(DIRECTION_RIGHT, cloudBottom, 3.7f, -1 * cloudBottom.getWidth(),
      screenHeight - cloudBottom.getHeight()));

  for (final Cloud cloud : clouds) {
    final ImageView cloudImage = new ImageView(context);
    cloudImage.setImageBitmap(cloud.getBitmap());
    cloudImage.post(new Runnable() {
      @Override public void run() {
        TranslateAnimation animation;
        if (cloud.getDirection() == DIRECTION_LEFT) {
          animation = new TranslateAnimation(screenWidth + cloud.getBitmap().getWidth(),
              -1 * cloud.getBitmap().getWidth(), 0.0f, 0.0f);
        } else {
          animation = new TranslateAnimation(-1 * cloud.getBitmap().getWidth(),
              screenWidth + cloud.getBitmap().getWidth(), 0.0f,
              0.0f);          //  new TranslateAnimation(xFrom,xTo, yFrom,yTo)
        }
        animation.setDuration((long) (10000 * cloud.getSpeedMultiplier()));  // animation duration
        animation.setRepeatCount(Animation.INFINITE);  // animation repeat count
        cloudImage.startAnimation(animation);
      }
    });
    cloudImage.setPadding(0,
        (int) (cloud.getY() / context.getResources().getDisplayMetrics().density), 0, 0);
    sky.addView(cloudImage);
  }
}

Следующий же код показывает добавление анимации для перекати-поле вместе с тенью.

public static void addTumbleweed(final Activity context, RelativeLayout desert) {
  DisplayMetrics displaymetrics = new DisplayMetrics();
  context.getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
  final int screenWidth = displaymetrics.widthPixels;
  Resources res = context.getResources();
  final Bitmap tumbleweedBitmap = decodeResource(res, R.drawable.tumbleweed);
  final ImageView tumbleweedImage = new ImageView(context);
  tumbleweedImage.setImageBitmap(tumbleweedBitmap);
  tumbleweedImage.setId(R.id.tumbleweed);
  tumbleweedImage.post(new Runnable() {
    @Override public void run() {
      AnimationSet animationSet = new AnimationSet(true);
      RotateAnimation rotateAnimation =
          new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,
              Animation.RELATIVE_TO_SELF, 0.5f);
      rotateAnimation.setStartOffset(0);
      rotateAnimation.setDuration(500);
      rotateAnimation.setRepeatCount(Animation.INFINITE);
      animationSet.addAnimation(rotateAnimation);
      TranslateAnimation translateAnimation =
          new TranslateAnimation(-1 * tumbleweedBitmap.getWidth(),
              screenWidth + tumbleweedBitmap.getWidth(),
              40 * context.getResources().getDisplayMetrics().density,
              40 * context.getResources().getDisplayMetrics().density);
      translateAnimation.setDuration(10000);
      translateAnimation.setRepeatCount(Animation.INFINITE);
      animationSet.addAnimation(translateAnimation);
      TranslateAnimation jumpAnimation = new TranslateAnimation(0.0f, 0.0f, 0.0f,
          -1 * 20 * context.getResources().getDisplayMetrics().density);
      jumpAnimation.setDuration(500);
      jumpAnimation.setRepeatCount(Animation.INFINITE);
      jumpAnimation.setRepeatMode(2);
      animationSet.addAnimation(jumpAnimation);
      tumbleweedImage.startAnimation(animationSet);
    }
  });
  final Bitmap shadowBitmap = decodeResource(res, R.drawable.shadow_tumbleweed);
  final ImageView shadowImage = new ImageView(context);
  shadowImage.setImageBitmap(shadowBitmap);
  RelativeLayout.LayoutParams params =
      new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT);
  params.addRule(RelativeLayout.BELOW, R.id.tumbleweed);
  params.setMargins(0, (int) (38 * context.getResources().getDisplayMetrics().density), 0, 0);
  shadowImage.setLayoutParams(params);
  shadowImage.post(new Runnable() {
    @Override public void run() {
      AnimationSet animationSet = new AnimationSet(true);
      TranslateAnimation translateAnimation = new TranslateAnimation(-1 * shadowBitmap.getWidth(),
          screenWidth + shadowBitmap.getWidth(), 0.0f, 0.0f);
      translateAnimation.setDuration(10000);
      translateAnimation.setRepeatCount(Animation.INFINITE);
      animationSet.addAnimation(translateAnimation);
      AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.4f);
      alphaAnimation.setDuration(500);
      alphaAnimation.setRepeatCount(Animation.INFINITE);
      alphaAnimation.setRepeatMode(2);
      animationSet.addAnimation(alphaAnimation);
      shadowImage.startAnimation(animationSet);
    }
  });
  desert.addView(shadowImage);
  desert.addView(tumbleweedImage);
}

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

Здесь можно увидеть, что отрисовка плейсхолдера занимает 7,143 мс, а добавление анимации 3,913 мс. Вместе на всё уходит 11,056 мс. То есть изменив способ анимации мы немного, но всё же уменьшили время выполнения кода, добившись аналогичного результата.

Заключение

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

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

Нашли ошибку в тексте?

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

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