Введение


Первое, что я хочу сказать: статья не претендует на сильно глубокий уровень, скорее я хочу рассказать о том, что производительность это не только «быстрее с NDK на С++» и «экономьте память, а то сборка мусора будет часто запускаться», а это целый комплекс мер, потому что проблемы с производительностью возникают не когда одна функция медленно работает, а в комплексе.
Не было ли у вас ощущения, что приложение тормозит, а вы уже не знаете что делать — и память вроде не жрет, и профайлером уже посмотрели, а решения все нет. Если да, то эти заметки для вас.
Понятия и термины я переводить не буду, так как я думаю что почти все разработчики их не переводят.

Ссылки

(да, ссылки вначале)

Все что я описываю здесь я почерпнул из:
1. Курс по производительность от Google (рекомендую);
2. developer.android.com (если вы сюда не заглядывали, то у меня для вас плохие новости).

Overdraw


Как выглядит процесс превращения вашей xml-разметки в то, что вы видите на экране? Это довольно сложный процесс, но сейчас нас интересует одна деталь. На одном из этапов происходит растроризация (rasterization). Это тот самый процесс, когда высокоуровневые объекты вроде шрифтов и кружков делятся на пиксели.

Представьте себе простой пример:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">


    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        ....

    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#000000">


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ff0000"
            android:text="Hello, habrahabr!" />

    </LinearLayout>

</LinearLayout>


У нас есть TextView, которая лежит в LinearLayout, который тоже лежит в LinearLayout. И у каждого из них есть свой background. Это значит, что когда будет рисоваться TextView, оно будет рисоваться по уже нарисованному LinearLayout, часть которого тоже была нарисовано по уже нарисованному. Это и называется overdraw. Возможно, само по себе это не так критично, но говорит о том, что у вас есть проблемы, например, с иерархией.

Для того, чтобы найти overdraw, нужно поставить галочку в настройках Android: Settings -> Developer Options ->Debug GPU Overdraw

И ваш телефон сразу преобразится!

Все сразу окрашивается в разные красивые цвета:



Синий цвет — все в порядке
Зеленый — 2x overdraw
Светлокрасный — 3x overdraw
Красный — 4x и более

Вот так, например, выгдядит Gmail:



А вот так знакомое многим приложение Vivino:



Видно, у кого есть проблемы, а у кого нет.
Первое, что можно сделать чтобы избавить от overdraw, это убрать лишние определения background. Например:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    
    android:orientation="vertical">


    <LinearLayout
        android:background="#ffffff"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        ....

    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ff0000"
            android:text="Hello, habrahabr!" />

    </LinearLayout>

</LinearLayout>

И, еще можно написать в Activity:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        getWindow().setBackgroundDrawable(null);
...
}

Иерархия


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

Чтобы найти проблемные места в вашем приложении, можно использовать утилиту monitor, которая входит в набор Android SDK. В нем есть вид "Hierarchy View", и это то, что нам нужно.
* о том как настроить и подробности читайте здесь: http://developer.android.com/training/improving-layouts/optimizing-layout.html
и здесь: http://developer.android.com/tools/debugging/debugging-ui.html

Эта утилита показывает всю иерархию вьюх в приложении, и показывает насколько каждая вью производительнее (относительно остальных). Выглядит это примерно так:

image

Что показывают цветные точки

monitor меряет скорость отрисовки вьюх для каждой фазы отрисовки Activity:
Левая — Draw
Средняя — Layout
Правая — Measure
Что касается цвета, то:
Зеленый означает, что вью рендерится быстрее по крайней мере половины соседних вьюх.
Yellow означает, что вью рендерится медленнее по крайней мере половины соседних вьюх.
Красный говорит о том, что все плохо вью одна из самых времязатратных.

Как трактовать эти точки?

Если у вас все красное у «крайних» вью (те, что листы дерева), то на них точно стоит смотреть.
Если у вас ViewGroup с большим количество детей, и у них точка показывающая measure красная, то тут точно надо смотреть на иерархию.

В нашем примере, ее можно было бы сделать плоской достаточно легко:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <LinearLayout
        android:id="@+id/my_layout"
        android:background="#ffffff"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        ....

    </LinearLayout>

    <TextView
        android:layout_below="@+id/my_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000"
        android:text="Hello, habrahabr!" />
    
</RelativeLayout>


ViewStub


ViewStub, это вью, которая является простым контейнером и может содержать в себе только одного ребенка:

<ViewStub android:id="@+id/stub"
               android:inflatedId="@+id/subTree"
               android:layout="@layout/mySubTree"
               android:layout_width="120dip"
               android:layout_height="40dip" />

Но вся штука в том, что layout, который содержится в ViewStub, будет создан только, если ViewStub видима, или у нее напрямую вызван метод inflate().

Подробнее: http://developer.android.com/reference/android/view/ViewStub.html

Это можно и нужно использовать, если у вас часть разметки становится видимой только в каких-то случаях или по нажатию на какую-то кнопку. То есть, это что-то вроде ленивой инициализации.

Пишите быстрые приложения, коллеги.

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


  1. forceLain
    17.04.2015 17:29
    +3

    Плоская разметка — идеально.

    Это не всегда так. К примеру, RelativeLayout'у приходится выполнять measure-процесс 2 раза и если он содержит детей со сложным measure-подсчетом, то он может оказаться медленнее, чем несколько других более «простых» ViewGroup.


    1. forceLain
      17.04.2015 17:36

      Кстати, та же история с множественным measure и у LinearLayout, если рисовать её детей с помощью параметра weight


  1. forceLain
    17.04.2015 17:44
    +2

    Спасибо за статью, я всё никак не мог понять, по какому принципу монитор мне окрашивал эти точки. Ну т.е. понятно, что красный цвет это плохо, но как он определял, что такое-то время measure/layout/draw это уже много было загадкой :)
    От себя бы добавил:
    1. Если нужно заполнить место каким то холдером/пустым местом, есть специальная вьюшка Space, которая участвует в процессах measure/layout, но ничего не рисует, тем самым не отнимает ресурсов.
    2. Бывают случаи, когда нужно нарисовать сложный, но не очень изменяемый layout и в таких случаях есть смысл подумать: «а не написать ли мне свой ViewGroup с легким и быстром measure/layout/draw процессом»


  1. kaftanati
    17.04.2015 18:28

    Большое спасибо за наводку на: «Debug GPU Overdraw/Отладка превышения GPU». Надеюсь на продолжение.


  1. gurinderu
    18.04.2015 11:37
    +1

    Не понимаю я утверждения, что ndk быстрее. Для чего быстрее? Для обсчета какой-нибудь трудоемкой задачи да, а для всего явно нет. Не забывайте что jni call медленный


    1. stalkerg
      21.04.2015 10:50

      Можно GUI и на NDK писать. Есть лёгкие варианты и есть монстрики типо Qt.


      1. gurinderu
        21.04.2015 12:20

        Конечно можно. Напрямую делая колы через opengl. Только вот зачем? Для обычного приложения потратить годы на разработку, по пути наделав кучу багов. Для игр это только нужно.


        1. stalkerg
          22.04.2015 12:32

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


  1. anton9088
    18.04.2015 18:10

    Еще можно добавить остальные warning'и, которые кидает Lint в плане производительности


  1. ZiGR
    19.04.2015 16:26

    Спасибо. Лично для меня статья оказалась очень полезной.