findViewById后浪们

findViewById

相信大家对这个方法已经烂熟于心,我们刚开始学习Android就接触到了,顾名思义,根据布局文件所写的id拿到View对象,然后就可以对这个View对象进行操作啦。但是一但布局复杂起来,便少不了很多声明变量+findViewById绑定的模板代码,首先会增加代码行数,其次就是浪费时间。懒是进步的动力源,为了方便和提高效率,findViewById的后浪们来了~

ButterKnife

JakeWharton大神的一项力作,也是我对findViewById说拜拜的开始,在一定程度上减少了模板代码,增强了代码的可读性。ButterKnife让我们在编码时通过注解@BindView来替代findViewById绑定的操作,原理简单来说就是在编译期间处理注解生成View绑定类并在ButterKnife.bind时使用,自动完成相关操作,帮我们屏蔽了繁琐的细节。此外,它还提供了@OnClick等其他注解减少我们的代码量,具体使用可以去官网查看。部分示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ExampleActivity extends Activity {
@BindView(R.id.user) EditText username;
@BindView(R.id.pass) EditText password;

@BindString(R.string.login_error) String loginErrorMessage;

@OnClick(R.id.submit) void submit() {
// TODO call server...
}

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}

kotlin-android-extensions

2017 Google I/O 大会上,谷歌宣布:官方正式支持将Kotlin作为Android开发的First-Class(一等公民)语言。作为Android开发者,Kotlin也逐渐变成了我们的主力开发语言,而且其安全性和简洁性等特性,也让我们直呼真香,特别是还带了一大波语法糖和拓展插件,其中就有我们接下来要说到的由JetBrain推出的kotlin-android-extensions插件。在Android Studio 4.1之前,我们新建Kotlin项目时,IDE会自动给我们引入这个插件:

1
apply plugin: 'kotlin-android-extensions'

然后在Activity、Fragment或其他需要的地方引入:

1
import kotlinx.android.synthetic.main.<布局>.*

完成简单的以上步骤,我们就可以直接使用布局文件中的id引用View对象,例如引用idtvContent的TextView:

1
2
3
4
5
6
7
8
9
10
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

tvContent.text = "Hello, World!"
}
}

好家伙,这下直接砍掉了声明变量+findViewById绑定的操作,比ButterKnife还要省时省力。至于其原理,我们将生成的字节码反编译成Java代码,简化后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class MainActivity extends AppCompatActivity {
private HashMap _$_findViewCache;

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(24654246);
TextView var10000 = (TextView)this._$_findCachedViewById(id.tvContent);
var10000.setText((CharSequence)"Hello, World!");
}

public View _$_findCachedViewById(int var1) {
if (this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(var1);
if (var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(var1, var2);
}
return var2;
}
}

可以看到,插件帮我们生成了一个_$_findCachedViewById方法,并在类中维护了一个HashMap,当我们通过id去获取View对象时,首先会去HashMap中尝试拿到缓存,如果没有就findViewById一下拿到对象并写入HashMap中,下次就可以直接取用了。

后浪推前浪

现在的ButterKnife官网写道:

This tool is now deprecated. Please switch to view binding. Existing versions will continue to work, obviously, but only critical bug fixes for integration with AGP will be considered. Feature development and general bug fixes have stopped.

大致意思就是框架已废弃,不再进行功能性维护,只考虑关键性错误修复,并推荐了一波ViewBinding

Android Studio 4.1及以后,kotlin-android-extensions也悄然离场,而且使用新版本Gradle构建项目时会抛出如下警告:

The ‘kotlin-android-extensions’ Gradle plugin is deprecated. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the
‘kotlin-parcelize’ plugin.

emmm,废弃,并推荐了一波ViewBinding

ButterKnife和kotlin-android-extensions为什么被废弃?

ButterKnife从2013年兴起并被广泛应用,一直到模块化开发的流行,Google对R类进行了改造,在Android项目的library模块中,生成R类中的成员变量改为了非final修饰,那么在注解中就不能使用R.id了,尽管JakeWharton搞出了R2类的骚操作来解决这个问题,但终究是背道而驰,最后还是妥协,并对该项目进行了废弃。

至于kotlin-android-extensions,维护一个HashMap会增加内存开支,而且尽管HashMap时间复杂度为O(1),实际调用肯定也没有直接访问来得快,无形中降低了运行效率,还有就是只支持Kotlin,更严重的是可能会找到错误布局资源下的id,并且对空安全没有做好处理。

不管怎么说,Google才是Android界的老大,随着亲儿子ViewBinding的大力推广,其他的框架也只能退位让贤,甚至还要给它打打广告,但是ViewBinding也确实撑得起这个业务,是一个更好的解决方案。

ViewBinding

ViewBinding就是一个轻量且纯粹的findViewById的替代方案,在我看来,它既弥补了ButterKnife在模块化开发方面的不足,也几乎达到了kotlin-android-extensions的易用程度,并保证了类型安全和空安全。官网是这样描述的:

通过视图绑定功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。

在大多数情况下,视图绑定会替代 findViewById

视图绑定具有一些很显著的优点:

  • Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。
  • 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。

这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。

启用也很简单,在build.gradle文件中添加:

1
2
3
4
5
6
android {
...
viewBinding {
enabled = true
}
}

如果某个布局文件不想生成绑定类,在布局文件根View中添加tools:viewBindingIgnore="true"即可。生成的绑定类命名会将xml文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。如activity_main.xml会生成ActivityMainBinding类。使用时通过绑定类的inflate方法在Activity、Fragment或其他需要的地方创建实例,然后通过getRoot方法拿到根View传递给setContentView或其他创建View的方法,使其展示在屏幕上。通过绑定类实例可以直接引用View对象,如binding.tvContent.text = "Hello, World!",更具体的使用可以去官网查看。

Google还给出了一个ViewBindingButterKnifekotlin-android-extensions(KAE)的对比:

ViewBinding ButterKnife KAE
总是空安全 🤷‍♀️
只引用当前布局中的id
支持Kotlin和Java
需要的代码量 有些重复

总结

毫无疑问,findViewById后浪中ViewBinding现在独领风骚,但是对于其他方案我们也同样瑞思拜。哪有什么岁月静好,只是有人在为你负重前行。一个看起来并不大的开发痛点,为了解决它却也经历了一波三折,希望我们在用的同时也有所得吧。

作者

EIong

发布于

2022-08-21

更新于

2022-08-21

许可协议