Начиная с
Пока я писал пример макета для статьи, у меня в голове возник интересный вопрос.
В последнее время вопрос улетучился из моей головы из-за его неиспользования. Я использовал привязку данных, привязку представлений и даже 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 здесь.
Мир вне.