Медленный рендеринг

Рендеринг пользовательского интерфейса — это процесс генерации кадра из вашего приложения и его отображения на экране. Чтобы обеспечить плавное взаимодействие пользователя с вашим приложением, оно должно рендерить кадры менее чем за 16 мс, чтобы достичь 60 кадров в секунду (fps). Чтобы понять, почему 60 fps предпочтительнее, см. раздел «Шаблоны производительности Android: почему 60 fps?» . Если вы пытаетесь достичь 90 fps, то это окно сокращается до 11 мс, а для 120 fps — до 8 мс.

Если вы выходите за пределы этого окна на 1 мс, это не означает, что кадр отображается с задержкой в ​​1 мс, а скорее, что Choreographer полностью пропускает кадр. Если ваше приложение страдает от медленной отрисовки пользовательского интерфейса, система вынуждена пропускать кадры, и пользователь ощущает подтормаживания в приложении. Это называется «рывки» . На этой странице показано, как диагностировать и устранять рывки.

Если вы разрабатываете игры, которые не используют систему View , то обходите стороной Choreographer . В этом случае библиотека Frame Pacing Library помогает играм на OpenGL и Vulkan добиться плавной отрисовки и правильной синхронизации кадров на Android.

Выявление сбоев

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

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

Визуальный осмотр

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

Вот несколько советов по проведению визуального осмотра:

  • Запустите релизную — или хотя бы не отлаживаемую — версию вашего приложения. Среда выполнения ART отключает несколько важных оптимизаций для поддержки функций отладки, поэтому убедитесь, что вы смотрите на что-то похожее на то, что видит пользователь.
  • Включите профилирование рендеринга на графическом процессоре . При включении профилирования рендеринга на графическом процессоре на экране отображаются полосы, которые наглядно показывают, сколько времени требуется для рендеринга кадров окна пользовательского интерфейса относительно стандартного показателя в 16 мс на кадр. Каждая полоса имеет цветные компоненты, соответствующие этапу конвейера рендеринга, поэтому вы можете увидеть, какая часть занимает больше всего времени. Например, если кадр тратит много времени на обработку ввода, посмотрите код вашего приложения, который обрабатывает ввод пользователя.
  • Проанализируйте компоненты, которые часто вызывают задержки в работе , например, RecyclerView .
  • Запустите приложение с нуля .
  • Запуск приложения на медленном устройстве усугубит проблему.

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

Systrace

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

Запишите трассировку с помощью Systrace , выполняя тестовый сценарий на вашем устройстве. Инструкции по использованию Systrace см. в разделе «Запись системной трассировки из командной строки» . Systrace разделяет процессы и потоки. Найдите процесс вашего приложения в Systrace, он выглядит примерно так, как на рисунке 1.

Пример Systrace
Рисунок 1. Пример использования Systrace.

В примере Systrace на рисунке 1 представлена ​​следующая информация для выявления зависаний:

  1. Systrace показывает, когда отрисовывается каждый кадр, и использует цветовую кодировку для выделения кадров с медленной отрисовкой. Это помогает более точно находить отдельные кадры с рывками, чем при визуальном осмотре. Для получения дополнительной информации см. раздел «Проверка кадров пользовательского интерфейса и оповещений» .
  2. Systrace обнаруживает проблемы в вашем приложении и отображает предупреждения как в отдельных окнах, так и на панели оповещений . Лучше всего следовать указаниям в предупреждении.
  3. В некоторых частях фреймворка и библиотек Android, таких как RecyclerView , используются маркеры трассировки. Таким образом, временная шкала systrace показывает, когда эти методы выполняются в потоке пользовательского интерфейса и сколько времени занимает их выполнение.

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

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

Для получения более подробной информации см. раздел «Понимание Systrace» .

Настраиваемый мониторинг производительности

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

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

Для получения более подробной информации см. раздел «Начало работы с мониторингом производительности для Android» .

Застывшие кадры

Замороженные кадры — это кадры пользовательского интерфейса, отрисовка которых занимает более 700 мс. Это проблема, поскольку ваше приложение, кажется, зависло и не реагирует на ввод пользователя почти целую секунду, пока отрисовывается кадр. Мы рекомендуем оптимизировать приложения для отрисовки кадра в течение 16 мс, чтобы обеспечить плавную работу пользовательского интерфейса. Однако при запуске приложения или при переходе на другой экран нормально, что отрисовка начального кадра занимает более 16 мс, поскольку вашему приложению необходимо создать представления, отобразить экран и выполнить начальную отрисовку с нуля. Именно поэтому Android отслеживает замороженные кадры отдельно от медленной отрисовки. Ни один кадр в вашем приложении не должен отрисовываться дольше 700 мс.

Зависание кадров — это крайне медленная отрисовка, поэтому процедура диагностики и устранения проблемы одинакова.

Отслеживание сбоев

Функция FrameTimeline в Perfetto может помочь в отслеживании медленно работающих или зависших кадров.

Взаимосвязь между медленными кадрами, зависшими кадрами и ANR

Медленная работа, зависание кадров и ошибки ANR — это разные виды проблем, с которыми может столкнуться ваше приложение. См. таблицу ниже, чтобы понять разницу.

Медленные кадры Застывшие кадры АНР
Время рендеринга В диапазоне от 16 мс до 700 мс В интервале от 700 мс до 5 с Больше 5 секунд
Видимая зона воздействия на пользователя
  • Прокрутка RecyclerView ведёт себя резко.
  • На экранах со сложной анимацией, которая отображается некорректно.
  • Во время запуска приложения
  • Переход с одного экрана на другой — например, смена экранов.
  • Пока ваше приложение находится на переднем плане, оно не реагирует на события ввода или BroadcastReceiver — такие как нажатия клавиш или касания экрана — в течение пяти секунд.
  • Несмотря на отсутствие активности на переднем плане, выполнение BroadcastReceiver не завершается в течение значительного промежутка времени.

Отслеживайте медленные и зависшие кадры отдельно.

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

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

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

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

Исправление зависаний

Чтобы устранить задержки, проверьте, какие кадры не завершаются за 16 мс, и выясните, в чем проблема. Проверьте, не занимает ли Record View#draw или Layout аномально много времени в некоторых кадрах. См. раздел «Распространенные причины задержек» для получения информации об этих и других проблемах.

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

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

Распространенные источники сбоев

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

Прокручиваемые списки

ListView — и особенно RecyclerView — часто используются для сложных прокручиваемых списков, наиболее подверженных рывкам. Оба содержат маркеры Systrace, поэтому вы можете использовать Systrace, чтобы увидеть, способствуют ли они рывкам в вашем приложении. Передайте аргумент командной строки -a <your-package-name> , чтобы отобразить разделы трассировки в RecyclerView , а также любые добавленные вами маркеры трассировки. Если доступно, следуйте указаниям предупреждений, сгенерированных в выводе Systrace. Внутри Systrace вы можете щелкнуть разделы трассировки RecyclerView , чтобы увидеть объяснение работы, которую выполняет RecyclerView .

RecyclerView: notifyDataSetChanged()

Если вы видите, что каждый элемент в вашем RecyclerView переназначается — и, следовательно, перестраивается и перерисовывается в одном фрейме — убедитесь, что вы не вызываете notifyDataSetChanged() , setAdapter(Adapter) или swapAdapter(Adapter, boolean) для небольших обновлений. Эти методы сигнализируют об изменениях всего содержимого списка и отображаются в Systrace как RV FullInvalidate . Вместо этого используйте SortedList или DiffUtil для генерации минимальных обновлений при изменении или добавлении содержимого.

Например, рассмотрим приложение, которое получает с сервера новую версию списка новостных материалов. При отправке этой информации в адаптер можно вызвать notifyDataSetChanged() , как показано в следующем примере:

Котлин

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

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

Мы рекомендуем использовать DiffUtil , который вычисляет и отправляет минимальные обновления за вас:

Котлин

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

Чтобы указать DiffUtil , как проверять ваши списки, определите ваш MyCallback как реализацию Callback .

RecyclerView: Вложенные RecyclerView

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

Если при прокрутке страницы вниз вы видите, что множество внутренних элементов раздуваются, возможно, стоит проверить, используется ли общий пул RecyclerView.RecycledViewPool между внутренними (горизонтальными) экземплярами RecyclerView . По умолчанию каждый RecyclerView имеет свой собственный пул элементов. Однако в случае, когда на экране одновременно отображается дюжина itemViews , возникает проблема, когда itemViews не могут использоваться совместно различными горизонтальными списками, если все строки отображают элементы схожих типов.

Котлин

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

Если вы хотите дополнительно оптимизировать, вы также можете вызвать setInitialPrefetchItemCount(int) у LinearLayoutManager внутреннего RecyclerView . Например, если у вас всегда отображается 3,5 элемента в ряду, вызовите innerLLM.setInitialItemPrefetchCount(4) . Это сигнализирует RecyclerView о том, что когда на экране вот-вот появится горизонтальный ряд, он должен попытаться предварительно загрузить элементы внутри него, если есть свободное время в потоке пользовательского интерфейса.

RecyclerView: Слишком сильная инфляция или процесс создания занимает слишком много времени.

В большинстве случаев функция предварительной загрузки в RecyclerView может помочь обойти проблему увеличения стоимости, выполняя работу заранее, пока поток пользовательского интерфейса простаивает. Если вы наблюдаете увеличение стоимости во время кадра, а не в разделе с пометкой RV Prefetch , убедитесь, что вы тестируете на поддерживаемом устройстве и используете последнюю версию библиотеки поддержки . Функция предварительной загрузки поддерживается только в Android 5.0 API Level 21 и более поздних версиях.

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

Если ваши типы представлений выглядят хорошо, подумайте о снижении стоимости инфляции. Уменьшение количества ненужных контейнерных и структурных представлений может помочь. Рассмотрите возможность создания itemViews с использованием ConstraintLayout , что может помочь уменьшить количество структурных представлений.

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

RecyclerView: Привязка занимает слишком много времени

Привязка (то есть, onBindViewHolder(VH, int) ) должна быть простой и занимать значительно меньше одной миллисекунды для всего, кроме самых сложных элементов. Она должна брать обычные Java-объекты (POJO) из внутренних данных элементов вашего адаптера и вызывать сеттеры для представлений внутри ViewHolder . Если OnBindView в ViewHolder занимает много времени, убедитесь, что вы выполняете минимальную работу в коде привязки.

Если вы используете базовые POJO-объекты для хранения данных в вашем адаптере, вы можете полностью избежать написания кода привязки в onBindViewHolder , используя библиотеку Data Binding Library .

RecyclerView или ListView: слишком долгое время компоновки или отрисовки.

По вопросам, связанным с отрисовкой и компоновкой, см. разделы «Производительность компоновки» и «Производительность рендеринга» .

ListView: Инфляция

В ListView можно случайно отключить повторное использование элементов, если не быть осторожным. Если вы видите, что элемент постоянно увеличивается в размере при появлении на экране, убедитесь, что ваша реализация метода Adapter.getView() обрабатывает, перепривязывает и возвращает параметр convertView . Если ваша реализация getView() постоянно увеличивает размер элемента, ваше приложение не получает преимуществ повторного использования элементов в ListView . Структура вашего getView() почти всегда должна быть похожа на следующую реализацию:

Котлин

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

Производительность компоновки

Если Systrace показывает, что сегмент Layout в Choreographer#doFrame работает слишком часто или слишком интенсивно, это означает, что у вас возникли проблемы с производительностью компоновки. Производительность компоновки вашего приложения зависит от того, какая часть иерархии представлений имеет изменяющиеся параметры компоновки или входные данные.

Эффективность компоновки: Стоимость

Если сегменты длиннее нескольких миллисекунд, возможно, вы сталкиваетесь с наихудшими показателями производительности при вложенности для RelativeLayouts или weighted-LinearLayouts . Каждый из этих макетов может запускать несколько проходов измерения и компоновки своих дочерних элементов, поэтому их вложенность может привести к поведению O(n^2) по глубине вложенности.

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

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

Характеристики компоновки: Частота

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

Как правило, анимация должна запускаться при изменении свойств отрисовки объекта View , например, следующих:

Изменить все эти параметры гораздо дешевле, чем свойства компоновки, такие как отступы или поля. Как правило, также гораздо дешевле изменить свойства отрисовки представления, вызвав сеттер, который запускает invalidate() , а затем draw(Canvas) в следующем кадре. Это перезаписывает операции отрисовки для представления, которое было аннулировано, и, как правило, также намного дешевле, чем изменение компоновки.

Производительность рендеринга

Пользовательский интерфейс Android работает в два этапа:

  • Запишите вызов метода View#draw в потоке пользовательского интерфейса, который запускает draw(Canvas) для каждого недействительного представления и может вызывать методы в пользовательских представлениях или в вашем коде.
  • Функция DrawFrame выполняется в RenderThread , который работает в собственном RenderThread но функционирует на основе работы, сгенерированной на этапе Record View#draw .

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

Если вызов метода Record View#draw занимает много времени, это часто означает, что отрисовка растрового изображения происходит в потоке пользовательского интерфейса. Отрисовка растрового изображения использует рендеринг на ЦП, поэтому, как правило, следует избегать этого метода, если это возможно. Вы можете использовать трассировку методов с помощью Android CPU Profiler, чтобы определить, в чем может быть проблема.

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

Котлин

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

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

Котлин

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

Вы можете заменить это следующим:

Котлин

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

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

Если вы используете растровое изображение для рисования по другой причине — например, в качестве кэша — попробуйте рисовать на аппаратно-ускоренном Canvas , передаваемом непосредственно в View или Drawable . При необходимости также рассмотрите возможность вызова setLayerType() с LAYER_TYPE_HARDWARE для кэширования сложных результатов рендеринга и при этом использования преимуществ рендеринга на графическом процессоре.

Производительность рендеринга: RenderThread

Некоторые операции Canvas легко записываются в память, но запускают ресурсоемкие вычисления в потоке RenderThread . Systrace обычно сообщает об этом с помощью оповещений.

Анимация больших траекторий

Когда вызывается Canvas.drawPath() для аппаратно ускоренного Canvas , переданного в View , Android сначала рисует эти пути на ЦП, а затем переносит их на графический процессор. Если у вас большие пути, избегайте их редактирования от кадра к кадру, чтобы они могли быть кэшированы и эффективно отрисованы. drawPoints() , drawLines() и drawRect/Circle/Oval/RoundRect() более эффективны и лучше использовать, даже если вы используете больше вызовов отрисовки.

Canvas.clipPath

clipPath(Path) запускает ресурсоемкое действие обрезки, и его, как правило, следует избегать. По возможности, лучше рисовать фигуры, а не обрезать по непрямоугольным объектам. Это обеспечивает лучшую производительность и поддерживает сглаживание. Например, следующий вызов clipPath можно выразить по-другому:

Котлин

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

Вместо этого, представьте приведенный выше пример следующим образом:

Котлин

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
Загрузка растровых изображений

Android отображает растровые изображения как текстуры OpenGL, и при первом отображении растрового изображения в кадре оно загружается в графический процессор. Это можно увидеть в Systrace как Texture upload(id) width x height . Это может занять несколько миллисекунд, как показано на рисунке 2, но это необходимо для отображения изображения с помощью графического процессора.

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

В Android 7.0 код загрузки растровых изображений — обычно выполняемый библиотеками — может вызывать метод prepareToDraw() для запуска предварительной загрузки до того, как она потребуется. Таким образом, загрузка происходит на ранней стадии, когда RenderThread простаивает. Это можно сделать после декодирования или при привязке растрового изображения к представлению, если вам известно само растровое изображение. В идеале, это делает ваша библиотека для загрузки растровых изображений, но если вы управляете собственной библиотекой или хотите убедиться, что не сталкиваетесь с загрузками на более новых устройствах, вы можете вызвать prepareToDraw() в своем собственном коде.

Приложение тратит значительное время в кадре на загрузку большого растрового изображения.
Рисунок 2. Приложение тратит значительное время в кадре на загрузку большого растрового изображения. Либо уменьшите его размер, либо запустите этот процесс на более раннем этапе при декодировании с помощью prepareToDraw() .

Задержки планирования потоков

Планировщик потоков — это часть операционной системы Android, отвечающая за определение того, какие потоки в системе должны выполняться, когда они должны выполняться и как долго.

Иногда задержки возникают из-за того, что поток пользовательского интерфейса вашего приложения заблокирован или не запущен. Systrace использует разные цвета, как показано на рисунке 3, для обозначения того, когда поток находится в спящем режиме (серый), готов к выполнению (синий: он может выполняться, но еще не выбран планировщиком для выполнения), активно работает (зеленый) или находится в режиме непрерывного сна (красный или оранжевый). Это чрезвычайно полезно для отладки проблем с задержками, вызванных задержками планирования потоков.

Выделяет период, когда поток пользовательского интерфейса находится в спящем режиме.
Рисунок 3. Фрагмент изображения периода, когда поток пользовательского интерфейса находится в спящем режиме.

Часто вызовы binder-функций — механизма межпроцессного взаимодействия (IPC) в Android — вызывают длительные паузы в выполнении вашего приложения. В более поздних версиях Android это одна из наиболее распространенных причин остановки потока пользовательского интерфейса. Как правило, решение заключается в том, чтобы избегать вызова функций, которые выполняют вызовы binder-функций. Если это неизбежно, кэшируйте значение или перенесите работу в фоновые потоки. По мере увеличения размера кодовой базы вы можете случайно добавить вызов binder-функции, вызвав какой-либо низкоуровневый метод, если не будете осторожны. Однако вы можете найти и исправить их с помощью трассировки.

Если у вас есть транзакции Binder, вы можете получить их стеки вызовов с помощью следующих команд adb :

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

Иногда, казалось бы, безобидные вызовы, такие как getRefreshRate() , могут запускать транзакции связывания и вызывать серьезные проблемы при частом использовании. Периодическая трассировка может помочь обнаружить и исправить эти проблемы по мере их возникновения.

Показывает, что поток пользовательского интерфейса приостанавливается из-за транзакций binder в RV fling. Сосредоточьте внимание на логике binder и используйте trace-ipc для отслеживания и удаления вызовов binder.
Рисунок 4. Поток пользовательского интерфейса находится в режиме ожидания из-за транзакций связывания в RV fling. Упростите логику связывания и используйте trace-ipc для отслеживания и удаления вызовов связывания.

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

Выделение объектов и сборка мусора

С момента внедрения ART в качестве среды выполнения по умолчанию в Android 5.0 проблема выделения памяти и сборки мусора (GC) значительно уменьшилась, но эта дополнительная работа всё ещё может перегружать потоки. Выделение памяти допустимо в ответ на редкое событие, происходящее нечасто в секунду — например, нажатие пользователем кнопки, — но помните, что каждое выделение памяти имеет свою цену. Если это происходит в часто вызываемом цикле, следует избегать выделения памяти, чтобы снизить нагрузку на сборщик мусора.

Systrace показывает, часто ли запускается сборщик мусора, а Android Memory Profiler может показать, откуда происходят выделения памяти. Если избегать выделений памяти, особенно в плотных циклах, вероятность возникновения проблем снизится.

Отображает 94-миллисекундную сборку мусора в HeapTaskDaemon.
Рисунок 5. Сборка мусора длительностью 94 мс в потоке HeapTaskDaemon.

В последних версиях Android сборщик мусора обычно работает в фоновом потоке под названием HeapTaskDaemon . Значительные объемы выделения памяти могут означать увеличение ресурсов ЦП, затрачиваемых на сборку мусора, как показано на рисунке 5.

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}