Начиная с

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

В последнее время вопрос улетучился из моей головы из-за его неиспользования. Я использовал привязку данных, привязку представлений и даже Jetpack Compose в своих недавних приключениях по разработке приложений. Но в моем примере проекта уравнения были другими, так как я не хотел загружать его вещами, которые ему не нужны.

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

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

Что такое findViewById?

Версия findViewById, о которой мы здесь говорим, определена в AppCompatActivity и возвращает экземпляр представления, допускающего значение NULL, для переданного вами идентификатора представления. Она может возвращать значение null, если переданный вами идентификатор представления не ссылается на представление в текущей иерархии представлений.

Существует также отвратительный метод под названием requireViewById, который возвращает непустое представление. Это может дать вам ощущение безопасности в отношении типа возвращаемого значения, но будьте осторожны, в отличие от findViewById, этот метод выдаст IllegalArgumentException, если представление не может быть найдено.

Существует также отвратительный метод, называемый requireViewById, который возвращает непустое представление. Это может дать вам чувство безопасности в отношении типа возвращаемого значения, но будьте осторожны, в отличие от findViewById, этот метод вызовет исключение IllegalArgumentException, если представление не может быть найдено.

Копаем

Чтобы узнать, что происходит, когда мы вызываем findViewById, все, что нам нужно сделать, это использовать функцию перехода к определению и проследить цепочку до конца. Мы также обнаружим некоторое интересное поведение по пути.

AppCompatActivity

Когда мы переходим к определению findViewById из нашей SampleActivity, мы достигаем того же самого определения в AppCompatActivity.

Как мы видим здесь, AppCompatActivity просто делегирует этот вызов делегату. Поэтому двинемся дальше и посетим AppCompatDelegate

AppCompatDelegate

Когда мы достигаем AppCompatDelegate, мы обнаруживаем, что findViewById — это просто абстрактное объявление. С ним не связано никакой реализации. Чтобы продолжить нашу цепочку вызовов, мы найдем единственную реализацию этого класса, которая точно названа AppCompatDelegateImpl.

AppCompatDelegateImpl

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

  • Устанавливает функции темы AppCompat, такие как windowNoTitle и actionBar, в окно.
  • Решает и отправляет вкладыши потомкам.
  • Показывает прогресс панели инструментов в ОС до леденцов.
  • Обеспечивает установку окна.

После возвращения ensureSubDecor мы продвигаемся дальше по цепочке вызовов findViewById. Следующая остановка — класс Window.

Окно

Даже со всеми комментариями о том, как эта функция будет работать, и предупреждением о том, что она вызовет getDecorView внутри. Хорошо, что когда AppCompatDelegateImpl звонит. Он уже заботится о том, чтобы вид декора был установлен. От класса окна мы переходим к классу View.

Вид

В отличие от комментария, который мы видели в классе Window, этот комментарий говорит нам нечто очень важное. Он объясняет точную природу функций, которые реализует findViewById. Сначала представление проверяет себя на запрошенный id, если само представление совпадает, поиск прекращается, в противном случае поиск продолжается до потомков этого представления.

Метод findViewTraversal — это то место, где находится эта реализация. Давайте посмотрим на это.

Как и в упомянутом выше комментарии, он проверяет, совпадает ли запрошенный идентификатор с самим собой, если да, возвращает экземпляр self, в противном случае возвращает null?

Странно, разве в комментарии не сказано, что поиск продолжится до его потомка, если id не совпадет с самим собой. Что может быть причиной этого?

Оказывается, ответ довольно прост, и он очевиден в иерархии представлений Android. Давайте взглянем.

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

В типичной иерархии представлений только определенный подкласс представления и его потомки могут иметь дочерние элементы, этот конкретный подкласс является ViewGroup, и именно здесь мы далее рассмотрим реализацию, зависящую от потомка, findViewTraversal

ViewGroup

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

И на этом наше путешествие по поиску окончательной реализации findViewById подходит к концу.

Важная информация:findViewTraversal для ViewGroup использует метод обхода дерева/графа, который называется DFS или поиск в глубину. Вы можете прочитать больше об этом здесь".

Еда на вынос

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

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

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

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

Вопрос к читателю: как вы думаете, почему findViewTraversal реализован в ViewGroup с использованием DFS, а не BFS (поиск в ширину)? Дайте мне знать ваши мысли либо в комментариях, либо в Твиттере

Редактировать. Во-первых, большое спасибо за то, что сообщили мне об отсутствии тестов с комментарием о том, что вызовы findViewById могут быть очень дорогими. Я изменил его, чтобы он был более контекстным. Звонок может быть очень дорогим теоретически. Этот вывод не принимает во внимание современные устройства и реальные показатели производительности. Один из немногих способов, где вы, возможно, увидите это влияние на производительность, - это если у вас был глубоко вложенный макет и вам приходилось многократно вызывать findViewById (например, в представлении переработчика onBindViewHolder), и это легко сводится на нет, следуя простым методам кодирования, таким как сохранение представления вместо того, чтобы найти его снова снова.

Ресурсы

Весь код, использованный в этой статье, вы можете найти здесь.

Вы можете найти меня в Твиттере здесь. Пожалуйста, не стесняйтесь обращаться ко мне, если у вас есть какие-либо сомнения, предложения или вопросы. Я рад помочь.

Если вы хотите решить интересные задачи по программированию чего-либо (в основном для Android), вы можете найти меня на LinkedIn здесь.

Мир вне.