Примечание: это перевод статьи рассказ ведется от лица автора, контакты, прямые ссылки и немного об авторе Вы сможете найти в конце статьи.
В последние время я работаю над повышением производительности некоторых из моих Xamarin-приложений для Android. Одной из преследуемых мною целей является улучшение GPU Overdraw. Проще говоря, речь идёт о том, сколько раз одни и те же пиксели отрисовываются за один кадр. Минимизация этого показателя улучшит продуктивность графического представления на Android, что в итоге обеспечит плавный скроллинг, быструю отрисовку отображаемых данных и в целом будет способствовать тому, чтобы Ваше предложение работало более стабильно. Следующая задача — отслеживать вложенные Layout и выравнивать их, чтобы улучшить качество размещения самого View на экране.
Сейчас действительно имеется немало возможностей для внесения усовершенствований в приложения путём уменьшения GPU Overdraw и повышения скорости отрисовки Views. В этой статье я постараюсь спустить завесу тайны с некоторых из этих возможностей.
Выравнивание Layout
Чтобы повысить скорость размещения Views на экране устройства, следует сделать кое-что очень важное: выровнять Layout. Что под этим подразумевается? Давайте обратимся к примеру!
Рассмотрим следующий макет страницы, который по большей части создан из вложенных LinearLayouts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button1" /> <Button android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button2" /> </LinearLayout> <LinearLayout android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button3" /> <Button android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button4" /> </LinearLayout> </LinearLayout> |
Конкретная задача с вложенным Layout, который представлен выше, осложняется тем количеством передач метрик, которые необходимо сделать, чтобы выровнить его. Каждый дочерний тег layout_weight должен измерять себя дважды. Другие лейауты также должны себя масштабировать. Представьте, что Вы работайте с более комплексным макетом, где присутствует множество вложенных элементов; это приведёт к чрезмерному количественному показателю передач метрик и замедлит отображение вашего Layout. Особенно в тех случаях, когда вы используете в качестве макета строчный Layout. Это довольно сильно ударит по производительности Вашего приложения и вызовет заметные спады в его работе.
Расположенный выше лейаут может быть выровнен с помощью тега RelativeLayout. Вы заметите, что layout_weight является достаточно мощным инструментом для выравнивания равноразмерных Views, так что некоторые из приёмов работы с ним следует также применять и к RelativeLayout, чтобы добиться схожих результатов. Поскольку производительность в итоге будет гораздо лучше.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/centerShimVertical" android:layout_height="match_parent" android:layout_width="0dp" android:visibility="invisible" android:layout_centerHorizontal="true"/> <View android:id="@+id/centerShimHorizontal" android:layout_height="0dp" android:layout_width="match_parent" android:visibility="invisible" android:layout_centerVertical="true"/> <Button android:id="@+id/button1" android:layout_alignParentTop="true" android:layout_alignParentStart="true" android:layout_alignEnd="@id/centerShimVertical" android:layout_above="@id/centerShimHorizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button1" /> <Button android:id="@+id/button2" android:layout_alignParentTop="true" android:layout_alignStart="@id/centerShimVertical" android:layout_alignParentEnd="true" android:layout_above="@id/centerShimHorizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button2" /> <Button android:id="@+id/button3" android:layout_alignParentBottom="true" android:layout_alignParentStart="true" android:layout_alignEnd="@id/centerShimVertical" android:layout_below="@id/centerShimHorizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button3" /> <Button android:id="@+id/button4" android:layout_alignParentBottom="true" android:layout_alignStart="@id/centerShimVertical" android:layout_alignParentEnd="true" android:layout_below="@id/centerShimHorizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button4" /> </RelativeLayout> |
Как Вы можете видеть, в представленном выше макете используется два дополнительных лейаута, которые применяются для центрирования других views. В качестве альтернативного варианта, вместо RelativeLayout для создания размеров views с процентным отношением можно использовать PercentRelativeLayout из пакета Android.Support.Percent. С его помощью вы можете задавать соотношение сторон, используя этот тег для установки процентов ширины и высоты, а также многого другого. Я рекомендую всегда сохранять ваши лейауты максимально простыми.
Если Вам просто хочется зафиксировать views на верхних частях друг друга, то Вы можете использовать тег FrameLayout, что также отлично скажется на производительности.
Вы можете узнать больше об оптимизации макетов в официальной документации Android, в которой также рассматривается техника использования Hierarchy viewer для выявления лейаутов с малым быстродействием.
GPU Overdraw
GPU Overdraw — это ещё одна проблема, с которой вы можете столкнуться. Что такое этот Overdraw? Это значение, сообщающее нам о том, сколько раз один и тот же пиксель отрисовывается в течение кадра. Почему это считается плохим? Чем большее число раз одни и те же пиксели отрисовываются, тем большее количество времени тратится понапрасну. Для того чтобы у Вашего приложения была плавная анимации и прокрутка, а также ряд других положительных качеств, оно должно обладать максимально высокой частотой кадров. Наилучшие результаты будут в том случае, если фреймрейт приложения всё время станет держаться на уровне 60 FPS (кадров в секунду). Для этого нам нужно максимально сократить время, затрачивающееся на отрисовку кадра, и сделать этот показатель меньше 16мс. Это не долго! Давайте узнаем, что может предпринять разработчик для улучшения этого показателя.
Включение функции демонстрации GPU Overdraw
Вы можете включить на устройстве или эмуляторе специальный прозрачный экран, накладывающий дополнительную информацию на основную, который будет отображать производимый Вашим приложением GPU Overdraw. Вы можете найти эту опцию в настройках в разделе «для разработчиков».
В результате активации данной функции на Вашем экране появятся забавно отображающиеся цвета. И каждый из этих цветов будет иметь конкретное значение.
Пурпурно-голубой цвет означает, что пиксели образовали Overdraw один раз, если цвет зеленый, то — дважды, когда светло-красный — трижды и если темно-красный, то — 4 раза или больше. Вы также увидите участки, отображающиеся в своем первоначальном цвете, и это означает, что данные конкретные пиксели не образовывали Overdraw. Цель состоит в том, чтобы Overdraw не было вообще. Однако, достичь этого может быть очень трудно, если у Вас на экране не будет отрисовывающегося фона.
Удаление бэкграунда из views
Самый простой способ уменьшить Overdraw — это удалить из views фон. Рассмотрим предложенный мною ранее лейаут на этот раз с полным бэкграундом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:background="@color/herp" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:background="@color/derp" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:background="@color/durr" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button1" /> <Button android:background="@color/durr" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button2" /> </LinearLayout> <LinearLayout android:background="@color/derp" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:background="@color/hurr" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button3" /> <Button android:background="@color/hurr" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent" android:text="button4" /> </LinearLayout> </LinearLayout> |
И если функция демонстрации GPU Overdraw запущена в отладочном режиме, то мы увидим следующее:
Этот Layout стал красным только потому, что мы добавили бэкграунды к нашим макетам. Удаление этих фонов значительно снизит Overdraw и, таким образом, повысит производительность Вашего приложения.
Простое удаление самого ближнего внешнего фона уменьшает Overdraw, но в этом Layout изменения фона в любом случае не будут заметны.
Два вложенных LinearLayouts используют один и тот же цвет, но что если мы выберем его в качестве темы для фона и удалим из макетов?
Итак, Overdraw снова уменьшился. Вот, как выглядит view компонент без включенного GPU Overdraw.
В этом случае, поскольку сами кнопки обладают фоном, появится некоторый Overdraw и здесь мы с ним ничего не сможем поделать. Тем не менее уже то, что лейаут перестал быть полностью красным и что его цвет сменился на зелёный или светло-синий, имеет весомое значение для вопроса производительности. Особенно в тех случаях, когда лейаут используется в ListView или RecyclerView, или в других view компонентах подобного адаптерного типа, поскольку тогда меньше времени расходуется на строчную отрисовку.
Следовательно, старайтесь избегать использования фонов, особенно если вы в принципе не можете их увидеть. Также хорошей идеей будет добавлять фон не ко всем макетам, а к Вашей теме, что совсем нетрудно.
1 | <item name="android:windowBackground">@color/hurr</item> |
С помощью тега android:background:»@null» Вы также можете эффективно удалять фон из тех views компонентов, бэкграунд которых Вас не особенно заботит.
Если Вам действительно необходимы тени, границы и тому подобные элементы, которые могут выступать в качестве фона, то воспользуйтесь в таком случае 9-patches с прозрачностью в областях, которые в любом случае остаются невидимыми. Система Android оптимизирует их отрисовку, и при этом Overdraw удастся избежать.
Уменьшение GPU Overdraw в компонентах собственных представлений
У Вас могут быть views компоненты, которые переопределяют метод OnDraw туда, где Вы отрисовываете содержимое для Canvas, которое им выполняется. Здесь Overdraw также имеет значение. Под применением OnDraw подразумевается то, что обычные views компоненты фактически прекращают выполнять при завершении самоотрисовки. Так что здесь следует быть поосторожнее.
Один из способов полностью ликвидировать Overdraw состоит в том, чтобы сперва передать отрисовку к Bitmap, а затем к canvas. Обычно, это называют двойной буферизацией. Используйте этот способ с осторожностью, ведь при этом ресурсопотребление первой отрисовки передаётся к Bitmap, а затем к canvas, и уже он выполняет отрисовку на дисплее.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private Bitmap _bufferedBitmap; private Canvas _bufferedCanvas; protected override void OnDraw(Canvas canvas) { Redraw(); canvas.DrawBitmap(_bufferedBitmap, 0, 0, null); } private void Redraw() { if (_bufferedCanvas == null) { _bufferedBitmap = Bitmap.CreateBitmap((int)_bounds.Width(), (int)_bounds.Height(), Bitmap.Config.Argb8888); _bufferedCanvas = new Canvas(_bufferedBitmap); } // draw on _bufferedCanvas } |
В приведенном выше коде демонстрируется упрощенная версия этого решения. В результате действительно получается всего 1x Overdraw, если отрисовка производится на бэкграунде. Тем не менее, если у Вас медленный код отрисовки, то вы можете столкнуться с мерцанием, когда вам будет необходимо многократно перерисовать ваш view компонент.
SurfaceView в Android позволяет осуществлять это несколько иначе. Ведь всю буферизацию отрисовки он выполняет в отдельном потоке. Такое решение является допустимым. Когда Вам потребуется отобразить буфер на экране, вызовите Invalidate() или PostInvalidate()
Техника, которую я применял, представляет собой модифицированную версию, где я задерживаю отрисовку, пока все графическое представление не будет выполнено на Bitmap. Тогда я подаю сигнал с PostInvalidate(). Это выглядит примерно так, как на следующем коде.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | private readonly object _canvasLocker = new object(); private Bitmap _bufferedBitmap; private Canvas _bufferedCanvas; private readonly Rect _textBounds = new Rect(); private bool _manualRefresh; public bool DelayedDoubleBuffer { get; set; } = true; private void Redraw(Canvas canvas) { // draw on canvas... } protected override void OnDraw(Canvas canvas) { lock (_canvasLocker) { if (DelayedDoubleBuffer) { if (_manualRefresh) { try { _manualRefresh = false; if (_redrawTokenSource == null) _redrawTokenSource = new CancellationTokenSource(); Task.Run(() => RedrawAsync(_bufferedCanvas, _redrawTokenSource.Token)); } catch (AggregateException) { } catch (TaskCanceledException) { } } } else { Redraw(_bufferedCanvas); } canvas.DrawBitmap(_bufferedBitmap, 0, 0, null); } } private CancellationTokenSource _redrawTokenSource; private readonly SemaphoreSlim _redrawSemaphore = new SemaphoreSlim(1); private async Task RedrawAsync(Canvas canvas, CancellationToken token = default(CancellationToken)) { await _redrawSemaphore.WaitAsync(token); try { token.ThrowIfCancellationRequested(); Redraw(canvas); token.ThrowIfCancellationRequested(); using (var h = new Handler(Looper.MainLooper)) h.Post(PostInvalidate); } finally { _redrawSemaphore.Release(); } } protected void Refresh() { if (_redrawTokenSource != null && !_redrawTokenSource.IsCancellationRequested) { _redrawTokenSource.Cancel(true); _redrawTokenSource.Dispose(); _redrawTokenSource = null; } _manualRefresh = true; PostInvalidate(); } |
Сейчас, по-видимому, это можно сделать немного проще. Но тем не менее мне удаётся достичь того, что всякий раз, когда я вручную подаю сигнал с Refresh(), я отменяю все текущие операции направления отрисовки в буфер, ведь я не заинтересован в том, чтобы они выполнялись, поскольку эти данные устарели… PostInvalidate() запускает метод OnDraw всякий раз, когда GPU готов. Далее, я запускаю новую задачу, которая направляет графическое представление в буфер. Когда эта задача выполнена она вызывает PostInvalidate(), сигнализируя тем самым о том, что буфер изменился и что отрисовка произведена. Это является разновидностью двойной буферизации, которая позволяет операциям отрисовки занимать много времени. В результате этого получается плавная прокрутка RecyclerView, которую я использую в этих графиках в виде строк, и Overdraw не наблюдается.
Возможно, некоторые из этих методов Вам пригодятся, и будут использоваться в Ваших приложениях. Дайте знать о том, что у Вас получилось.
В общем, сделать надо следующие:
- Выравнивать Layout для уменьшения циклов обращений
- Применять RelativeLayout вместо LinearLayout с использованием weights
- А с компонентами stacked views ещё лучше использовать FrameLayout
- Удалять бэкграунды, которые в любом случае не показываются
- Использовать темы бэкграундов там, где это приемлемо
- android:background:»@null»
- Использовать 9-patch для границ и теней
- Уменьшить Overdraw в Ваших собственных OnDraw обращениях
Автор: Tomasz Cielecki (Cheesebaron)
Источник: Официальный блог автора
Twitter: @Cheesebaron
GitHub: Cheesebaron
Немного об авторе:
Имеет статус Xamarin MVP и является членом сообщества Xamarin.
Советую посмотреть его GitHub проекты среди них есть реально полезные компоненты и примеры.
Написать ответ