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



Несколько недель назад я выступил на Droidcon в Нью-Йорке с докладом об оптимизации производительности Android.

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

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

Мои правила


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

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

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

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

Systrace


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

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

(картинки кликабельны)
image

Вы можете получить результат трассировки с помощью Android Device Monitor или используя командную строку. Более подробную информацию вы можете найти здесь.

На видео я рассказывал про составляющие отчета. Особенно интересные из них Alerts и Frames, представляющие нам предположения, сгенерированные на основе собранных данных. Давайте посмотрим на трассировку, которую я взял, и выберем одно из предупреждений наверху.

image

В Alert указано, что был длительный вызов View#draw(). Мы также получаем описание, ссылки на документацию и даже ссылки на видео с обсуждениями данной темы. Взглянув ниже на строку Frames, мы видим метки, соответствующие каждому отрисованному кадру, они окрашены в зеленый, желтый или красный для отражения проблем с производительностью, возникающих в процессе отрисовки кадра. Давайте выберем один из кадров, отмеченных красным.

image

Внизу мы увидим все подходящие уведомления для этого кадра. В данном случае у нас 3 сообщения, одно из них мы уже видели ранее. Давайте увеличим этот кадр и раскроем уведомление «Inflation during ListView recycling»:

image

Мы видим, что в сумме для этой части потребовалось 32мс, из-за чего отрисовка кадра длится дольше 16мс, требуемых для достижения 60fps. Здесь указано более подробная информация по каждому составляющему элементу в ListView для этого кадра – на 5 из них было потрачено около 6мс. Их описание поможет нам понять проблему и даже найти решение. На диаграмме сверху изображены все вызовы, мы можем увеличить или растянуть её, чтобы увидеть, какие части отрисовки требуют больше времени.

Другой пример медленной отрисовки кадра:

image

После выбора кадра, мы может нажать клавишу «m», чтобы выделить его и увидеть, как много времени занимает эта часть. Глядя выше, мы видим, что это требуется 19мс для отрисовки кадра. Раскрыв уведомление для этого кадра, мы видим сообщение «Scheduling delay».

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

image

Wall duration — это время, потраченное с момента начала до момента окончания работы элемента. Оно называется “Wall duration”, потому что это похоже на слежение за настенными часы с того момента, когда поток начал работу.

CPU duration — это фактическое время, которое потратил процессор на эту часть.
Заметна большая разница между этими двумя измерениями. В то время как общая работа занимает 18мс, CPU тратит на работу с потоком только 4мс. Это немного странно, так что было бы хорошо посмотреть, что делает процессор оставшееся время:

image

Все 4 ядра очень заняты.

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

В то время как подобный сценарий обычно является временным, так как другие приложения обычно не крадут процессор в фоновом режиме (… правда?), но такие потоки могут прийти из разных процессов в вашем приложении или даже из главного процесса. Таким образом Systrace представляет собой инструмент для обзора, но есть предел в том, как глубоко он может заглянуть. Чтобы найти то, что занимает время нашего процессора, мы будем использовать другой инструмент, называемый Traceview.

Traceview


Traceview является инструментом профилирования, показывающим, как долго длится каждый запущенный метод. Давайте посмотрим, как выглядит результат трассировки:

image

Данный инструмент может быть запущен из Android Device Monitor или из кода. Более подробная информация есть здесь.

Давайте рассмотрим следующие колонки.

  • Name — название метода и соответствующий ему цвет на графике.
  • Inclusive CPU Time — время, которое потребовалось процессору для обработки процесса и его потомков (т.е. всех вызванных им методов).
  • Exclusive CPU Time — время, которое потребовалось процессору для обработки только самого метода.
  • Inclusive / Exclusive Real Time — время, которое прошло с момента запуска метода до момента завершения. То же, что и «Wall duration» в Systrace.
  • Calls+Recursion — сколько раз этот метод был вызван, в том числе количество рекурсивных вызовов.
  • CPU/Real time per Call — сколько в среднем потребовалось времени процессора/реального времени на вызов этого метода. Остальные поля показывают совокупное время всех вызовов метода.

Я открыл приложение, у которого были проблемы с гладкостью прокрутки. Я начал трассировку, пролистал немного и остановился на одной строке. Я наткнулся на метод getView() и раскрыл его, и вот то, что я увидел:

image

Этот метод был вызван 12 раз, процессор потратил около 3 мс на каждый вызов, но реальное время, которое требуется для каждого вызова 162 мс! Определенно проблема…

Глядя на вызовы из этого метода, мы можем видеть, как общее время делится между разными методами. Thread.join() занимает около 98% реального времени. Этот метод используется когда мы хотим подождать окончания другого процесса. Одним из других потомков является Thread.start(), что позволяет мне предположить, что getView() открывает поток и ждет его окончания.

Но где этот поток?

Мы не можем видеть, что делает этот поток, так как непосредственно getView() не делает эту работу. Чтобы найти его, я искал метод Thread.run(), который вызывается при появлении нового потока. Я следовал за ним, пока не нашел виновника:

image

Я обнаружил, что BgService.doWork() требует около 14 мс за вызов, и мы имеем около 40 таких вызовов! Также существует шанс, что каждый getView() вызовет его более одного раза, и это объясняет, почему каждый getView() вызов занимает так много времени. Этот метод занимает процессор на протяжении длительного времени. Глядя на Exclusive CPU time, мы видим, что он использует 80% времени процессора!

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

Отслеживание таких критических методов как getView(), View#onDraw() и других, поможет нам найти причину, по которой наше приложение медленно выполняется. Но иногда существует что-то, что нагружает наш процессор, забирая драгоценные рабочие циклы процессора, которые могут быть потрачены на более гладкую отрисовку нашего UI. Сборщик мусора работает изредка, удаляя неиспользуемые объекты, и обычно не имеет большого влияния на приложение, работающее на переднем плане. Если сборщик мусора работает слишком часто, это может замедлить наше приложение, и вполне возможно, что мы сами в этом виноваты…

Профилирование работы памяти


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

image

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

Есть 2 доступных инструмента в левой части графика: Heap dump и Allocation Tracker.

Heap dump

Для исследования того, что в данный момент находится в нашей куче, мы можем использовать кнопку слева Heap dump. Этот инструмент сделает снимок того, что в данный момент содержится в куче, и покажет его в специальном экране отчета внутри Android Studio:

image

Слева мы можем видеть гистограмму экземпляров объектов в куче, сгруппированных по имени класса. Для каждого из них указывается суммарное число объектов, под которые выделена память, размер этих объектов (Schallow size) и размер объектов, сохраненных в памяти. Последнее говорит нам о том, сколько памяти может быть освобождено, если экземпляры объектов будут уничтожены. Это дает нам важное представление об отпечатке памяти нашего приложения, помогая определить большие структуры данных и отношения между объектами. Эта информация может помочь нам построить более эффективные структуры данных, удалить связи между объектами, чтобы уменьшить потребление памяти и в конечном итоге снизить объем памяти на сколько это возможно.

Глядя на гистограмму, мы видим, что MemoryActivity имеет 39 экземпляров объектов, что кажется странным для активити. Выбрав один из его экземпляров справа, раскроем список всех ссылок этого объекта в базовом дереве снизу.

image

Одна из них является частью массива внутри объекта ListenersManager. Глядя на другие экземпляры активити, оказывается, что все они сохраняются в данном объекте. Это объясняет, почему объект этого класса использует так много памяти.

image

Такие ситуации, как известно, называются «утечка памяти», поскольку активити явно были уничтожены, но неиспользуемая память не может быть очищена сборщиком мусора из-за этой ссылки. Мы можем избежать таких ситуаций, если будем уверены, что наши объекты не ссылаются на другие объекты, пережившие его. В данном случае ListenersManager не нужно сохранять эту ссылку после того, как активити была уничтожена. Решением будет удаление ссылки перед тем, как экземпляр класса будет уничтожен, в методе onDestory().

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

Более продвинутым инструментом является Eclipse Memory Analyzer Tool (Eclipse MAT):

image

Этот инструмент может делать то же самое, что и Android Studio, кроме того может выявить возможные утечки памяти и обеспечит более продвинутый поиск объектов, такой как поиск всех Bitmap размером больше 2 MB, или всех пустых Rect-объектов.

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

image

Allocation Tracker

Allocation Tracker можно открыть/закрыть с помощью кнопки слева от графика памяти. Он составит отчет о всех экземплярах класса, выделенных в памяти, за этот период времени, сгруппированный по классам:

image

или по методам:

image

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

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

Продолжение во второй части.

P.S.
Доклад был записан на видео и вы сможете посмотреть его здесь:


Также доступны слайды.

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


  1. xoxol_89
    30.11.2015 22:19

    В Android studio 1.5 и выше Heap Dump даже позволяет запускать проверку на утечки Activities, Strings… вообще красота)