Site icon AppTractor

Отладка и исправление проблемы с производительностью Jetpack Compose в моем приложении для решения судоку

В “свободное время” я получаю степень магистра в области искусственного интеллекта. Одной из самых забавных курсовых работ, на которую я к сожалению не смог потратить достаточно времени, было создание Решателя судоку на Python. Решив, что есть много вещей, которые я бы мог сделать более интересными, я поместил его в приложение для Android, используя при этом Jetpack Compose.

Не вдаваясь в подробности самого решателя, моя реализация на Python хранила список «возможных значений» для каждой ячейки, и каждый раз, когда новое значение вводилось в сетку судоку, выполнялся набор правил для уменьшения (насколько насколько это возможно) остальных возможных значений по всей сетке. Я представил это в виде сетки из 9 text view для каждой ячейки (да, почти наверняка есть лучший пользовательский способ сделать это, но помните: прогресс важнее совершенства!).

Вот, например, число 1 вставляется в сетку, а остальные значения для других ячеек обновляются в ответ (т.е. 1 удаляется из других ячеек в том же квадрате 3×3, в том же столбце и в той же строке):

 

После просмотра отличного доклада Аиды Исаевой на Droidcon New York, в котором приводились примеры распространенных ошибок и способов отладки, я решил попробовать новую функцию Android Studio Electric Eel, которая показывает количество рекомпозиций рядом с composable объектами в Layout Inspector.

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

И тогда я понял, что каждый отдельный Text composable для оставшихся значений (всего 729!) перекомпоновывался не только при вставке нового значения, но даже когда я нажимал на ячейку, и она выделялась фиолетовым состоянием.

Визуализация перекомпоновки Android Studio показывает, как малейшее изменение вызывало перекомпоновку более 700 текстовых полей.

Так почему же это произошло?

Stable против unstable. Почему composable всегда перекомпоновывается.

Очень рекомендую посмотреть выступление Аиды, когда оно появится в сети, но тем не менее, вот как я разобрался с проблемой.

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

Как выглядит мой код, когда я не ожидаю, что кто-то его проверит. Я собирался привести его в порядок… обещаю!

В приведенном выше у нас есть один параметр. Список целых чисел — и он immutable (неизменяем), верно? (Нет, это не так, но мы вернемся к этому позже). Таким образом, имея очень базовое понимание Jetpack Compose, можно было бы ожидать, что перекомпоновка произойдет только в случае изменения значения, верно?

Неверно. Compose пропускает только те рекомпозиции, которые совершенно точно не нужно перекомпоновывать, чего и следовало ожидать. А чтобы определить те, которые можно пропустить, компилятор проверяет, есть ли в списке параметров все те, которые были помечены как stable или unstable. Я не буду здесь вдаваться в подробности, скажу только, что если параметр помечен как stable, Compose абсолютно уверен, что он будет проинформирован, если это значение изменится, и может с определенной степенью уверенности пропустить рекомпозицию.

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

И вот тут начинается сюрприз.

MutableList не означает, что список неизменен

Список не является immutable, он доступен только для чтения. Это можно показать на примере, о котором вы почти наверняка знаете, но, вероятно, забыли (как и я). В вашей ViewModel у вас есть такие вещи:

Список read-only изменится, если изменится базовый mutable список. Поэтому он не является неизменным. Он доступен только для чтения, потому что вам запрещен прямой доступ к записи в переменную List, но вы не можете быть уверены, что значения в этой переменной List не изменятся. Compose тоже не может быть уверен, поэтому он решает, что просто перекомпонует любой компонуемый объект, который принимает список (или Set, или Map).

Делаем списки стабильными

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

На самом деле, учитывая, что List, Set и Map всегда приводят к перекомпоновке, я лично считаю, что они должны выдавать предупреждение компилятору. Но тем не менее, вот как я это исправил. Существует библиотека kotlinx-collections-immutable, которая позволяет вам явно указать, что Composable должен принимать неизменяемый список. Это изменяет сигнатуру составной функции на…

ImmutableList означает отсутствие ненужных рекомпозиций. Ура!

Но хотите знать самое худшее?

Я был хорошим Compose мальчиком (в основном)… разбивая вещи на деревья повторно используемых компонентов. Но поскольку этот Composable находился в самом конце ветки и всегда нуждался в перекомпоновке, это означало, что все его родительские Composable тоже должны были перекомпоновываться.

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

Выводы

  1. Используйте функцию проверки рекомпозиций в Android Studio EE, чтобы увидеть, насколько сильно изменяются ваши Composable.
  2. Если элемент каждый раз перекомпоновывается, ищите нестабильные параметры, такие как vars и List/Set/Map.
  3. Используйте библиотеку и никогда не позволяйте вашему Composable принимать изменяемый список.

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

Источник

Exit mobile version