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

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

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

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

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

Определить джанк

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

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

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

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

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

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

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

Систраце

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

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

Пример системы
Рисунок 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 мс для рендеринга.

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

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

Отслеживание нежелательной почты

FrameTimeline в Perfetto может помочь отслеживать медленные или зависшие кадры.

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

Медленные кадры, зависшие кадры и ошибки 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()
}

Ява

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)
}

Ява

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: вложенные RecyclerViews

Обычно существует вложение нескольких экземпляров 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)
    }
    ...

Ява

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 уровня 21 и более поздних версиях.

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

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

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

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

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

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

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.
    }
}

Ява

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 показывает, что сегмент макета 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# .

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

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

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

Котлин

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.
}

Ява

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)
}

Ява

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)
}

Ява

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()
}

Ява

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)

Ява

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

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

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

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

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

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

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

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

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

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

Если у вас есть транзакции связывания, вы можете захватить их стеки вызовов с помощью следующих команд 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() , могут инициировать транзакции связывания и вызывать большие проблемы при частом вызове. Периодическое отслеживание может помочь вам обнаружить и устранить эти проблемы по мере их появления.

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

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

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

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

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

Показывает сборщик мусора 94 мс на HeapTaskDaemon.
Рисунок 5. Сборщик мусора 94 мс в потоке HeapTaskDaemon.

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

{% дословно %} {% дословно %} {% дословно %} {% дословно %}