自打学习Android开发以来,findViewById就是我使用最频繁的函数之一。findViewById可为灵活至极,但是又让人爱恨交加。那findViewById究竟有哪些优缺点,他又有哪些替代方案呢?今天就来做一个总结
findViewById
findViewById的原理
findViewById的原理非常简单,我们都知道Android中的View结构是一个树形结构,findViewById就是自树的根节点,依次遍历其子节点,知道找到目标的id。
findViewById的优点
还是先夸夸findViewById的优点吧,
- 兼容性好,下面各种替代方案,都有其适用的场景,但是findViewById适用所有的场景,当你不知道用哪种方案的时候,那就用findViewById吧,肯定没有错。
- 非常灵活,适合动态加载layout文件。比如一个Activity,需要在不同的业务中,加载两个不同的layout文件,但是两个layout文件只有部分间距不同,其他各个元素都是相同的。这个时候,用findViewId就可以完美适用。
findViewById的缺点
- 性能略差不好,findViewById是基于树形结构的查找,理论上会带来性能的额外开销,但是实际项目中,因为控件的个数也不会非常非常多,所以可以忽略不计。;
- Fragment中使用容易犯错。从原理图可以看到,在Activity中调用findViewById,实际上是调用Window中的findViewById,但是Fragment中并没有单独的Window,Fragment中调用findViewById的效果和Activity中调用的效果一模一样。所以如果一个Activity中有多个Fragment,Fragment中的控件名称又有重复的,那直接findViewById会出错的;
- 增加代码的耦合度,findViewById随时实地都可以调用,在子view中,在Activity中等等,如果滥用起来,会让代码耦合的一塌糊涂,后面查找bug起来,非常麻烦,因为不知道View的属性在哪个类中被改变了;
- 容易引发空指针,一个大型项目中,控件的id经常会重复,xml中删除了一个控件,但是对应的Activity中没有删除这个控件的相关引用,编译时并不会报错,但是运营室时会报出空指针;
- 代码可读性不好,findViewById往往在Activity的onCreate方法中被引用,我们不能方便的将xml中的控件与代码中的控件结合起来,特别是如果xml的命名与代码中的命名又不规范,代码阅读起来简直就是噩梦。
ButterKnife
有人推出了ButterKnife框架,详细的用法不啰嗦了,简单看下使用方法
class ExampleActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
额。。。
从上面的代码可以看到,ButterKnife基本上只是对findViewById的一个取代而已,增加了代码的可读性,findViewById的各种缺点依然存在。
copy一份网上找来的优点吧(笔者不是很认同)
1、强大的View绑定和Click事件处理功能,简化代码,提升开发效率
2、方便的处理Adapter里的ViewHolder绑定问题
3、运行时不会影响APP效率,使用配置方便
4、代码清晰,可读性强
5、…
笔者认为,ButterKnife虽然没有解决findViewById的绝大部分问题,但是他依然是一个非常优秀的开源框架。作为一个第三方的开源框架,他已经做出了最大努力。
kotlin-android-extensions
如果使用kotlin,则可以用kotlin-android-extensions来替代findViewById,用法如下
module的build.gradle文件加入以下代码
plugins {
id 'kotlin-android-extensions'
}
然后在代码中就可以引用了
package com.cmri.findviewbyid
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv_hello.setOnClickListener {
}
}
}
对生成的apk进行反编译下,看下他的原理是什么
/* access modifiers changed from: protected */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView((int) R.layout.activity_main);
((TextView) findViewById(R.id.tv_hello)).setOnClickListener(MainActivity$$ExternalSyntheticLambda0.INSTANCE);
}
可以看到,他本质上依然是使用findViewById进行实现的,那findViewById的缺点他依然存在。
databinding
databinding与viewbinding的诞生,为优化findViewById的难题提出了另外一种思路,就是生成一个databinding文件,文件中记录了各个控件的引用。用法如下
module的build.gradle中加入以下代码
android {
dataBinding {
enabled true
}
}
然后将布局文件用layout标签包裹起来
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
activity代码如下
class MainActivity : AppCompatActivity() {
lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvHello.setOnClickListener {
}
}
}
可以看到,databinding会根据动态生成一个ActivityMainBinding文件,在执行ActivityMainBinding.inflate的时候,会自动生成控件的引用(mapBindings方法),这里对布局tree执行一趟遍历查找就可以生成所有的引用。
综上,我们归纳中他的优缺点。
优点
- 规避了控件空指针错误,如果有引用错误,则会在编译阶段发现;
- 效率比findViewById要高,一趟遍历可以生成所有控件的引用,而findViewById是每次执行时都需要遍历一遍
- 代码的可读性要高,得益于Android Studio的强大功能,我们很轻易的将java(kotlin)代码中的控件引用与xml的定义结合起来
缺点
- 灵活性不高,如果需要动态选取引用的布局文件,binding就无法适用了。
- 改造成本较大,布局文件只有加上layout标签才可以使用。
viewbinding
google在推出databinding的同时,推出了viewbinding。
先看下viewbinding的用法
module的build.gradle中增加以下代码
android {
viewBinding {
enabled true
}
}
viewbinding会默认为所有的布局文件生成对应的binding文件,如果不想生成,可以在布局文件中加上以下代码
tools:viewBindingIgnore="true"
然后在activity中直接使用即可,用法与databinding的用法相同。
但是viewbinding与databinding的最大不同是,databinding的控件引用通过mapbinding来实现的,一趟查找就能生成所有的控件引用,但是viewbinding是通过findViewById来生成所有的控件引用,所有viewbinding理论上效率比databinding要差一点,但是更加轻量级。
viewbinding生成控件的引用
public static ActivityMainBinding bind(View rootView2) {
int id = R.id.ll_layout;
LinearLayout llLayout2 = (LinearLayout) ViewBindings.findChildViewById(rootView2, R.id.ll_layout);
if (llLayout2 != null) {
id = R.id.tv_hello;
TextView tvHello2 = (TextView) ViewBindings.findChildViewById(rootView2, R.id.tv_hello);
if (tvHello2 != null) {
id = R.id.tv_hello1;
TextView tvHello12 = (TextView) ViewBindings.findChildViewById(rootView2, R.id.tv_hello1);
if (tvHello12 != null) {
return new ActivityMainBinding((ConstraintLayout) rootView2, llLayout2, tvHello2, tvHello12);
}
}
}
throw new NullPointerException("Missing required view with ID: ".concat(rootView2.getResources().getResourceName(id)));
}
综上,我们总结下viewbinding的优缺点
优点
- 规避了引用控件时的空指针错误问题,编译时生成控件的引用
- 较databinding,更加轻量级
- 代码可读性高,同databinding一样
缺点 - 灵活性不高,如果需要动态选取引用的布局文件,binding就无法适用了。
- 默认会为所有的xml生成binding文件,会生成很多冗余的文件。