Android开发获取视图组件的findViewById,kotlin-android-extensions,ViewBinding三种详解
前置条件:在一个activity的布局文件中,我们通过设置一个按钮,分别通过三种方式获取,并说明各自优缺点
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> //新增的Button元素按钮<Button android:id="@+id/button1" //定义idandroid:layout_width="match_parent" //宽度和父元素一样宽android:layout_height="wrap_content" //高度自适应android:text="Button 1" /> </LinearLayout>
1.findViewById详解
安卓开发中,activity获取xml文件中控件,最开始的方法是findViewById,那么它是如何去实现查找并绑定视图ID的。下面我们详细介绍
首先,findViewById的使用方法:
//Java代码 public class MainActivity extends AppCompatActivity {private TextView textView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 手动查找并转换类型textView = findViewById(R.id.textView);textView.setText("Hello Java!");} } //kotlin代码 class MainActivity : AppCompatActivity() {private lateinit var textView: TextViewoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)// 手动查找并转换类型textView = findViewById(R.id.textView)textView.text = "Hello Kotlin!"} }
那么其内部是如何实现
手机布局其实就像一个一个树状图,
- 树根:整个屏幕的最外层布局(如
DecorView
) - 树枝:有子布局的容器(如
LinearLayout
、RelativeLayout
等ViewGroup
) - 树叶:没有子布局的单个控件(如
TextView
、Button
等View
)
每个节点(View/ViewGroup)都有一个唯一的 ID。
总结查找过程:其就是先从树根(屏幕布局DecorView)开始查找,然后遍历树枝,检查当前树枝节点ID是否匹配,如果匹配直接返回,如果当前布局不匹配且含有子布局(即是ViewGroup),则递归检查其所有的子布局。如果没有子布局,则对比ID,匹配返回,不匹配则返回null或检查下一个。
核心方法:ViewGroup中的findViewTraversal()方法,是通过递归的方式从树根部,再到树枝-再到树叶,逐个查找,直到找到为止。
全局查找,从 DecorView 开始遍历整个视图树,时间复杂度为 O(n)(n 为视图总数)。
注意缺陷:
- 代码冗余:每个控件都需手动调用
findViewById
。- 类型不安全:若类型转换错误(如将
TextView
转为Button
),运行时会抛出ClassCastException
。- 空指针风险:若 ID 不存在,返回
null
(Java)或NullPointerException
(Kotlin)。- 性能开销:每次访问控件都需遍历视图树,影响性能。
实现代码:
// ViewGroup.java
@Override
protected View findViewTraversal(int id) {// 先检查自身if (id == mID) {return this;}// 遍历子 Viewfinal View[] children = mChildren;for (int i = 0; i < childrenCount; i++) {View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {// 递归查找子 ViewView v = child.findViewTraversal(id);if (v != null) {return v; // 找到即返回,终止递归}}}return null; // 未找到
}// View.java
public View findViewById(int id) {if (id == mID) {return this;}return null; // View 没有子 View,直接返回
}
2.kotlin-android-extensions详解(注意,已废弃)
Kotlin Android Extensions 于 2016 年随 Kotlin 1.0 版本推出,作为官方解决方案,旨在提供更简洁、高效的视图绑定方式。
其核心功能:Kotlin Android Extensions 的核心是 合成属性(Synthetic Properties),它允许开发者直接通过布局文件中的 ID 访问视图,无需显式绑定:
1.使用方法:
添加依赖:
plugins {id 'kotlin-android-extensions' }
直接引入空间ID
//kotlin代码 class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)// 直接使用布局文件中的 IDtextView.text = "Hello Extensions!"// 设置点击事件button.setOnClickListener { /* ... */ }} }
那么它是如何实现的呢?下面我们详细介绍
详解:这个插件呢其实是一个编译时插件,它的工作流程如下:
- 编译时处理:在编译阶段,插件分析布局文件(
.xml
),生成对应的访问器代码。- 合成属性生成:为每个布局文件生成一个 Kotlin 文件(如
R.layout.activity_main
对应生成ActivityMainKt
),其中包含布局中所有视图 ID 的属性访问器。- 运行时优化:访问合成属性时,插件会缓存首次查找的视图实例,避免重复调用 findViewById,提升性能。
整体来说,其不仅简化代码,高效快捷,合成属性的类型由视图文件自动推断还能保证类型安全,那么为什么废弃呢。主要是其缺陷
- 性能隐患:虽然缓存机制避免了重复 findViewById,但首次访问仍需反射查找,在频繁创建视图的场景下(如 RecyclerView)可能影响性能。
- 布局依赖:合成属性与布局文件强绑定,若布局文件修改但代码未同步更新,可能导致运行时异常。
- IDE 支持有限:在复杂场景下(如动态加载布局),IDE 可能无法正确提示合成属性。
3.ViewBinding详解
Android 团队于 2019 年 Android Gradle Plugin 3.6 版本引入了 View Binding。主要是这些方案均存在不同程度的缺陷,尤其是在大型项目中维护成本高。为解决这些问题,引入ViewBinding
使用方法:
1.在
build.gradle
中启用 View Bindingandroid {viewBinding {enabled = true} }
若需排除特定布局文件,可在布局文件中添加
tools:viewBindingIgnore="true"
:<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"tools:viewBindingIgnore="true"><!-- ... --> </LinearLayout>
2.在Activity中使用
class MainActivity : AppCompatActivity() {private lateinit var binding: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)binding.button.setOnClickListener { /* ... */ }} }
3.在fragment中使用
class MyFragment : Fragment(R.layout.fragment_my) {private var _binding: FragmentMyBinding? = nullprivate val binding get() = _binding!!override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)_binding = FragmentMyBinding.bind(view)binding.textView.text = "Hello from Fragment"}override fun onDestroyView() {super.onDestroyView()_binding = null // 防止内存泄漏} }
4.RecyclerView Adapter使用
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {class ViewHolder(private val binding: ItemMyBinding) : RecyclerView.ViewHolder(binding.root) {fun bind(item: MyItem) {binding.textView.text = item.name}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {val binding = ItemMyBinding.inflate(LayoutInflater.from(parent.context), parent, false)return ViewHolder(binding)}override fun onBindViewHolder(holder: ViewHolder, position: Int) {holder.bind(items[position])} }
在设计ViewBinding时,它的初衷就是
- 类型安全:编译时验证视图 ID 是否存在,避免运行时
NullPointerException
。 - 性能优化:直接访问视图引用,无需反射或缓存,提升运行效率。
- 零配置:无需编写注解或额外代码,自动生成绑定类。
- 兼容性:支持与 Data Binding、Kotlin Android Extensions 共存。
那么其工作原理:通过 编译时生成绑定类 实现视图访问。对于每个布局文件(如 activity_main.xml
),系统会自动生成对应的绑定类(如 ActivityMainBinding
)。该类包含:布局中所有具有 ID 的视图的直接引用。根视图的引用。创建绑定实例的静态方法
与 Kotlin Android Extensions 的对比
特性 | Kotlin Android Extensions | View Binding |
---|---|---|
类型安全 | 部分支持(布局修改可能导致运行时崩溃) | 完全支持(编译时验证) |
性能 | 首次访问需反射,后续缓存 | 直接访问,无反射 |
布局依赖 | 强依赖(布局修改需同步代码) | 弱依赖(绑定类自动更新) |
空安全 | 不支持(可能返回 null) | 支持(非空类型或可空类型) |
IDE 支持 | 有限(动态布局提示不足) | 完善(自动补全、类型提示) |
数据绑定支持 | 不支持 | 支持(与 Data Binding 集成) |
Fragment/RecyclerView | 需要额外处理生命周期 | 内置生命周期管理 |