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

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

(Читать первую часть)



Общие советы по работе с памятью


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

  • Перечисления уже являются предметом горячих споров о производительности. Вот видео, в котором обсуждается размер памяти, который тратят перечисления, и обсуждение данного видео и некоторой информации, потенциально вводящей в заблуждение. Используют ли перечисления больше памяти, чем обычные константы? Определенно. Плохо ли это? Не обязательно. Если вы пишете библиотеку и нуждаетесь в сильной типобезопасности, она могла бы оправдать их использование по сравнению с другими решениями, такими как @IntDef. Если у вас просто есть куча констант, которые могут быть сгруппированы вместе, использование перечислений будет не очень мудрым решением. Как обычно, есть компромисс, который нужно учесть при принятии решения.

  • Обёртка — это автоматическое конвертирование примитивных типов к их объектному представлению (например, int -> Integer). Каждый примитивный тип «оборачивается» в объектное представление, создается новый объект (шокирует, я знаю). Если у нас есть много таких объектов, вызов сборщика мусора будет выполняться чаще. Легко не заметить количество оборачиваний, потому что это делается автоматически для нас при назначении примитивному типу объекта. В качестве решения, постарайтесь использовать соответствующие типы. Если вы используете примитивные типы в своем приложении, постарайтесь избежать их обёртки без реальной на то необходимости. Вы можете использовать инструменты профилирования памяти, чтобы найти объекты, представляющие примитивные типы. Вы также можете использовать Traceview и искать Integer.valueOf(), Long.valueOf() и пр.

  • HashMap vs ArrayMap / Sparse*Array — также, как и в случае с обёртками, использование HashMap требует использование объектов в качестве ключей. Если мы используем примитивный тип int в своем приложении, он автоматически оборачивается в Integer при взаимодействии с HashMap, в этом случае мы могли бы использовать SparseIntArray. В случае, когда мы по-прежнему используем объекты в качестве ключей, мы можем использовать ArrayMap. Оба варианта требуют меньший объем памяти, чем HashMap, но они работают по-другому, что делает их более эффективными в потреблении памяти ценой уменьшения скорости. Обе альтернативы имеют меньший отпечаток памяти, чем HashMap, но время, требуемое для извлечения элемента или выделения памяти, немного выше, чем у HashMap. Если у вас нет более 1000 элементов, различия во времени выполнения не существенны, что делает жизнеспособными эти два вариантом.

  • Осознание контекста — как вы видели ранее, относительно легко создать утечки памяти. Вы, возможно, не будете удивлены, узнав, что активити являются наиболее частой причиной утечек памяти в Android(!). Их утечки также очень дорого стоят, так как они содержат все представления иерархии их UI, которые сами по себе могут занять много места. Убедитесь в том, что понимаете, что происходит в активити. Если ссылка на объект в кэше и этот объект живет дольше, чем ваша активити, без очистки этой ссылки вы получите утечку памяти.

  • Избегайте использование нестатических внутренних классов. При создании нестатического внутреннего класса и его экземпляра вы создаете неявную ссылку на ваш внешний класс. Если экземпляр внутреннего класса необходим на более длительный период времени, чем внешний класс, внешний класс будет продолжать находиться в памяти, даже если он больше не нужен. Например, создается нестатический класса, который наследует AsyncTask внутри класса Activity, затем происходит переход к новой асинхронной задаче и, пока она длится, завершение работы активити. Пока длится эта асинхронная задача, она будет сохранять активити живой. Решение простое — не делайте этого, объявите внутренний статический класс, если это необходимо.

Профилирование GPU


Новым добавлением в Android Studio 1.4 является профилирование GPU рендеринга.
Под окном Android перейдите на вкладку GPU, и вы увидите график, показывающий время, которое потребовалось, чтобы сделать каждый кадр на экране:

image

Каждая панель на графике представляет собой один отрисованный кадр, а цвет представляет различные этапы процесса.

  • Draw (синий) — представляет метод View#onDraw(). Эта часть строит/обновляет объекты DisplayList, которые затем будут конвертированы в OpenGL команды, понятные для GPU. Высокие показатели могут быть из-за сложных используемых представлений, требующих больше времени для построения их списка отображения, либо много представлений стали недействительными в короткий промежуток времени.

  • Prepare (фиолетовый) — в Lollipop был добавлен ещё один поток, чтобы помочь отрисовывать UI быстрее. Он называется RenderThread. Он отвечает за преобразование списков отображения в OpenGL команды и отправку их графическому процессору. Как только это происходит, поток UI может переходить к обработке следующего кадра. Время, затраченное потоком UI, чтобы передать все необходимые ресурсы RenderThread, отражается на данном этапе. Если у нас длинные/тяжелые списки отображения, этот шаг может занять больше времени.

  • Process (красный) — выполнение списков отображения для создания OpenGL команд. Этот шаг может занять больше времени, если списки длинные или сложные, потому что необходимо перерисовать множество элементов. Представление может быть перерисовано, так как стало недействительным или было открыто после передвижения скрывающего его представления.

  • Execute (жёлтый) — отправка команд OpenGL на GPU. Этот этап может длиться долго, так как CPU отправляет буфер с командами на GPU, ожидая получить обратно чистый буфер для следующего кадра. Количество буферов ограничено, и если GPU слишком занят, процессор будет ждать, пока освободится первый. Поэтому если мы видим высокие значения на этом шаге, это может означать, что GPU был занят отрисовкой нашего интерфейса, который может быть слишком сложным, чтобы быть отрисованным за более короткое время.

В Marshmallow было добавлено больше цветов для отображения большего количества шагов, таких как Measure/Layout, Input Handing и другие:
image

EDIT 09/29/2015: John Reck, фреймворк инженер из Google, добавил следующую информацию про некоторые из этих цветов:
«Точное определение слова «анимация» — это всё, что зарегистрировано с Choreographer как CALLBACK_ANIMATION. Это включает в себя Choreographer#postFrameCallback и View#postOnAnimation, которые используются view.animate(), ObjectAnimator, Transitions и другие… И да, это то же самое, что и отмечается меткой «анимация» в systrace.

«Misc» это задержка между временной меткой vsync и текущей временной меткой, когда он был получен. Если вы уже видели логи из Choreographer «Missed vsync by blabla ms skipping blabla frames», сейчас это будет представлено как “misc”. Существует различие между INTENDED_VSYNC и VSYNC в framestats dump (https://developer.android.com/preview/testing/performance.html#timing-info).»


Но перед тем как начать использовать эту возможность, нам необходимо включить режим GPU рендеринга в меню разработчика:
image

This will allow the tool to use ADB commands to get all the information it needs, and so are we (!), using:

 adb shell dumpsys gfxinfo <PACKAGE_NAME>


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

image

Если у нас есть автоматизированное тестирование UI нашего приложения, мы можем сделать сборку, запускающую эту команду после определенных действий (прокрутка списка, тяжелая анимация и др.) и увидеть, есть ли изменения в значениях, таких как «Janky Frames», с течением времени. Это может помочь определить упадок производительности после нескольких изменений в коде, позволяя нам устранить проблему, пока приложение не вышло на рынок. Мы можем получить еще более точную информацию, когда используем ключевое слово «framestats», как это показано здесь.

Но это не единственный способ увидеть такой график!

Как видно в меню разработчика «Profile GPU Rendering», есть также вариант “On screen as bars”. Его выбор покажет график для каждого окна на вашем экране вместе с зеленой линией, указывающей на порог 16 мс.

image

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

Hierarchy Viewer


Я обожаю этот инструмент, и мне очень жаль, что многие его не используют!
Используя Hierarchy Viewer, мы можем получить статистику производительности, увидеть полную иерархию представления на экране и иметь доступ ко всем свойствам элемента. Вы также можете сделать дамп всех данных темы, увидеть все параметры, используемые для каждого атрибута стиля, но это возможно только если Hierarchy Viewer запущен standalone, не из Android Monitor. Я использую этот инструмент, когда создаю макеты приложения и хочу оптимизировать их.

image

По центру мы можем видеть дерево, отражающее иерархию представления. Иерархия представления может быть широкой, но если она слишком глубокая (~10 уровней), это может нам дорого стоить. Каждый раз, когда происходит измерения представления в View#onMeasure(), или когда размещаются его потомки в View#onLayout(), эти команды распространяются на потомков этих представлений, делая то же самое. Некоторые макеты будут делать каждый шаг дважды, например, RelativeLayout и некоторые LinearLayout конфигурации, и если они вложены — число проходов увеличивается в геометрической прогрессии.

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

image

Для каждого представления у нас есть время, которое потребовалось для измерения/создания макета/отрисовки его и всех его потомков. Цвета отражают, как эти представления выполняются в сравнении с другими представлениями в дереве, это отличный способ найти слабое звено. Поскольку мы также видим предпросмотр представления, мы можем пройти по дереву и следовать инструкциям, создающим их, находя лишние шаги, которые мы могли бы удалить. Одна из таких вещей, которая влияет на производительность, называется Overdraw.

Overdraw


Как видно в разделе профилирования GPU — фаза Execute, представленная желтым цветом на графике, может занять больше времени, чтобы завершиться, если GPU должен отрисовать много элементов, увеличивая время, требуемое для отрисовки каждого кадра. Overdraw происходит когда мы рисуем что-то поверх чего-то другого, например желтую кнопку на красном фоне. GPU требуется нарисовать, во-первых, красный фон и затем желтую кнопку поверх него, что делает неизбежным overdraw. Если у нас слишком много слоев overdraw, это может стать причиной, по которой GPU работает интенсивнее и не успевает в 16 мс.

image

Используя настройку «Debug GPU Overdraw» в меню разработчика, все overdraw будут подкрашены, чтобы продемонстрировать степень overdraw в данной области. Если overdraw имеет степень 1x/2x, это нормально, даже небольшие красные области это тоже неплохо, но если мы имеем слишком много красных элементов на экране, вероятно у нас есть проблема. Давайте посмотрим на несколько примеров:

image

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

Заметьте: тема по умолчанию задает фоновый цвет для всего вашего окна. Если у вас есть активити с непрозрачным макетом, который перекрывает весь экран, вы можете убрать задний фон для этого окна, чтобы убрать один слой overdraw. Это может быть сделано в теме или в коде путем вызова getWindow().setBackgroundDrawable(null) в onCreate().

Используя Hierarchy Viewer, вы можете экспортировать все ваши слои иерархии в PSD файл, чтобы открыть его в Photoshop. В результате исследования различных слоев будут раскрыты все overdraw в макете. Используйте эту информацию, чтобы убрать избыточные overdraws, и не соглашайтесь на зеленый, стремитесь к голубому!

Альфа


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

image

Мы видим макет, который содержит 3 ImageView, которые перекрывают друг друга. В прямой реализации, устанавливая альфа с помощью setAlpha(), будет вызвана команда, чтобы распространиться на всех представления потомков, ImageView в данном случае. Затем эти ImageView будут нарисованы с параметром alpha в буфере кадра. Результат:

image

Это не то, что мы хотим видеть.

Поскольку каждый ImageView был отрисован со значением альфа, все перекрывающиеся изображения сольются. К счастью, у ОС есть решение такой проблемы. Макет будет скопирован в отдельный off-screen буфер, альфа будет применяться в этом буфере в целом и результат будет скопирован в буфер кадра. Результат:

image

Но… мы заплатим за это свою цену.

Дополнительная отрисовка представления в off-screen буфере до того, как это будет сделано в буфере кадра, добавляет еще один незаметный overdraw слой. ОС не знает, когда именно использовать этот или прямой подход раньше, так что по умолчанию всегда использует один комплексный. Но есть еще способы, позволяющие установить альфа и избежать сложностей с добавлением off-screen буфера:
  • TextViews — используйте setTextColor() вместо setAlpha(). Использование канала альфа для текста приведет к тому, текст будет перерисован.
  • ImageView — используйте setImageAlpha() вместо setAlpha(). Те же причины, что и для TextView.
  • Пользовательские представления — если наше пользовательское представление не поддерживает перекрытия, это комплексное решение не имеет для нас значение. Нет способа, которым наши представления-потомки будут соединены вместе, как это показано на примере выше. Переопределив метод hasOverlappingRendering() таким образом, чтобы он возвращал false, мы сигнализирует ОС взять непосредственно путь к нашему представлению. Также есть вариант вручную обрабатывать то, что происходит при установке альфа путем переопределения метода onSetAlpha() так, чтобы он возвращал true.

Аппаратное ускорение


Когда аппаратное ускорение было включено в Honeycomb, мы получили новую модель отрисовки для отображения нашего приложения на экране. Это включает структуру DisplayList, которая записывает команды отрисовки представления для более быстрого получения изображения. Но есть еще другая интересная особенность, о которой разработчики обычно забывают — уровни представления.

Используя их, мы можем отрисовывать представление в off-screen буфере (как мы видели ранее, когда использовали альфа-канал) и манипулировать с ним как мы пожелаем. Это отлично подходит для анимации, поскольку мы можем анимировать сложные представления быстрее. Без уровней анимация представлений стала бы недоступной после изменения свойств анимации (таких как x-координата, масштаб, альфа-значение и др.). Для сложных представлений эта неспособность распространяется на представления потомков, и они в свою очередь будут перерисовывать себя, что является дорогостоящей операцией. Используя уровни представления, опираясь на аппаратные средства, текстура для нашего представления создается в графическом процессоре. Существует несколько операций, которые мы можем применить к текстуре, без необходимости её замены, такие как изменение положения по осям x/y, вращение, альфа и другие. Всё это означает, что мы можем анимировать сложные представления на нашем экране без их замены в процессе анимации! Это делает анимацию более гладкой. Вот пример кода, демонстрирующий, как это можно сделать:


// Using the Object animator
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 20f);
objectAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});
objectAnimator.start();

// Using the Property animator
view.animate().translationX(20f).withLayer().start();



Просто, верно?

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

  • Делайте очистку после работы вашего представления — аппаратный уровень использует место на ограниченном пространстве памяти на вашем GPU. Попробуйте использовать его только в случаях, когда он нужны, например, в анимации, и делайте очистку после. В примере выше с ObjectAnimator я использовал метод withLayers(), который автоматически создает слой в начале и удаляет его, когда анимация заканчивается.
  • Если вы изменяете ваше представление после использования аппаратного уровня, это приведет к его непригодности и придется вновь воспроизводить представление на off-screen буфере. Это произойдет, когда изменится свойство, не оптимизированное для аппаратного уровня (на данный момент оптимизированы: поворот, масштабирование, передвижения по осям x/y, альфа). Например, если вы анимируете представление, используя аппаратный уровень, и двигаете представление по экрану, пока обновляется цвет фона, это приведет к постоянным обновлениям на аппаратном уровне. Это требует дополнительных расходов, которые могут сделать его использование неоправданным.

Для второго случая есть способ визуализировать это обновление. Используя настройки разработчика, мы можем активировать “Show hardware layers updates”.

image

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

image

Обе страницы были зелеными на протяжении всей прокрутки!

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

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

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

Сделай сам


При подготовке для всех этих примеров, показанных здесь, я написал множество кода для симуляции этих ситуаций. Вы можете найти их на репозитории Github, а также на Google Play. Я разделил сценарии для различных активити, и старался задокументировать их насколько возможно, чтобы помочь понять, с какого рода проблемами вы можете столкнуться, используя активити. Прочтите документацию по активити, откройте инструменты и поиграйте с приложением.

Больше информации


По мере развития ОС Android развиваются и способы, с помощью которых вы можете оптимизировать ваши приложения. Новые инструменты вводятся в Android SDK, добавляются и новые функции в ОС (например, аппаратные уровни). Очень важно осваивать новое и изучать компромиссы, прежде чем решить что-то изменить.

Есть замечательный плейлист на YouTube, который называется Android Performance Patterns, с множеством коротких видео от Google, объясняющими различные вещи, связанные с производительностью. Вы можете найти сравнения между различными структурами данных (HashMap vs ArrayMap), оптимизацию Bitmap и даже про оптимизацию сетевых запросов. Я очень рекомендуют посмотреть всех их.

Вступите в Android Performance Patterns Google+ community и обсуждайте производительность с другими участниками, включая сотрудников Google, чтобы поделиться идеями, статьями и вопросами.

Другие интересные ссылки


  • Изучите, как работает Graphics Architecture in Android. Там есть все, что вам надо знать про отрисовку вашего UI в Android, объясняются различные системные компоненты, такие как SurfaceFlinger, и как они все друг с другом взаимодействуют.

  • Talk from Google IO 2012 показывает как работает модель отрисовки и как/почему мы получаем мусор, когда наше UI отрисовывается.

  • Android Performance Workshop talk из Devoxx 2013 демонстрирует некоторые оптимизации, которые были сделаны в Android 4.4 в модели отрисовки, и представляет различные инструменты для оптимизации производительности (Systrace, Overdraw и др.).

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

  • Управление памятью в Android — старое видео с Google IO 2011, которое всё еще актуально. Оно демонстрирует, как Android управляет памятью наших приложений, и как использовать инструменты, такие как Eclipse MAT, чтобы выявить проблемы.

  • Тематическое исследование, сделанное инженером Google Romain Guy, для оптимизации популярного twitter-клиента. В этом исследовании Romain показывает, как он нашел проблемы производительности в приложении и что он рекомендует сделать, чтобы исправить их. Вот следующий пост, демонстрирующий другие проблемы, после того как оно было переработано.

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

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

Комментарии (4)


  1. Ghedeon
    27.11.2015 18:05
    +1

    К спору о перечислениях (enums), рекоммендую новую библиотеку от Square: github.com/pyricau/fragnums. Как-никак, фирма веников не вяжет.

    #enumsmatter


    1. andreich
      30.11.2015 10:29

      ага, особенное если прочитать вот этот пункт github.com/pyricau/fragnums#is-this-serious


      1. Ghedeon
        30.11.2015 10:59

        Я ценю Ваши открытия, но если кому-то нужен этот пункт, чтобы понять, что это троллинг, то у меня для него плохие новости). Лично я расстроился, что они вообще его добавили, я за интригу!


  1. 3draven
    28.11.2015 09:06
    +1

    Очень интересная статья.