在安卓中使用ViewBinding

不知道你有没有用烦findViewById,尤其是项目中控件比较多的场景,密密麻麻的findViewById看起来都头疼。谷歌也看到了这个问题,于是推出ViewBinding来专门解决掉它。

findViewById的工作原理

熟悉安卓开发的小伙伴应该知道,Android的View体系是一个树状的结构。

Android View的树状结构示意图

当我们通过findViewById去获取对应的View实例时,实际上是在这个树上进行遍历,找到id相符的View对象。关键源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* file: frameworks/base/core/java/android/view/View.java
*/
class View {
/**
* 在Activity中调用findViewById,最终会走到这里。
* 可以看到,这里实际是调用findViewTraversal进行递归检索View对象
*/
@Nullable
public final <T extends View> T findViewById(@IdRes int id) {

if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}

/**
* 通常最开始是在ViewGroup上调用findViewTraversal,所以应当看ViewGroup的
* findViewTraversal方法。
*/
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T)this;
}
return null;
}
}

/**
* file: frameworks/base/core/java/android/view/ViewGroup.java
*/
@UiThread
public abstract class ViewGroup extends View {
@Override
protected <T extends View> T findViewTraversal(@IdRes int id) {
// 如果查询的id跟当前的ViewGroup匹配,则返回当前ViewGroup对象
if (id == mID) {
return (T)this;
}

// 缓存该ViewGroup的子View
final View[] where = mChildren;
final int len = mChildrenCount;

// 开始线性搜索,匹配到目标View对象
for (int i = 0; i < len; i++) {
View v = where[i];

if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
// 调用子View的方法,其实就是为了判断id是否跟View的mID相等
v = v.findViewById(id);

if (v != null) {
return (T)v;
}
}
}

return null;
}
}

从源码不难理解,findViewById时间复杂度为$O(n)$,且它没有做任何优化——当对同一id多次调用findViewById时,每次的时间复杂度都是$O(n)$。

总结下findViewById存在的问题:

  • 编写复杂。当布局中控件较多时,需要很多次findViewById
  • 大多数情况,需要对获取到的View对象进行强制类型转换成对应的目标View类型,才能使用。
  • 每次调用的时间复杂度都是$O(n)$,没有优化。

ViewBinding

谷歌针对上述问题,给出的方案是使用ViewBinding。先看下如何使用

1、在build.gradle中开启该feature

如果需要在模块中启用view binding,只需要修改模块的build.gradle

1
2
3
4
5
android {
buildFeatures {
viewBinding true
}
}

2、加载并使用布局

2.1、在Activity中使用

假设现在有一个名为MainActivityActivity,它对应的布局文件为activity_main.xml,并且有两个子View:id为info_tv的TextView和id为click_btn的Button。那么启用View Binding后,会自动生成一个ActivityMainBinding的类。它位于模块build目录下的generated/data_binding_base_class_source_out/debug/out文件夹中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MainActivity: AppCompatActivity() {
private var _binding: ActivityMainBinding? = null
// 只能在onCreate()和onDestroy()之间使用
private val mBinding get() = _binding!!

override fun onCreate(savedInstanceState: Bundle?) {
// 加在布局文件
_binding = ActivityMainBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
// 注意这里传入的值
setContentView(mBinding.root)
}

private fun initView() {
initViewListener()
// 使用直接通过mBinding访问对象
// 注意:子View对象在ActivityMainBinding的名字为它们的id按驼峰式命名拼接,且首字母小写
mBinding.clickBtn.apply {
text = "Clicked me!"
isAllCaps = false
}
mBinding.infoTv.apply {
text = "Hi, I'm TextView"
}
}

private fun initViewListener() {
mBinding.clickBtn.apply {
setOnClickListener {
Toast.makeText(context, "You clicked me!", Toast.LENGTH_SHORT).show()
}
}
}

override fun onDestroy() {
super.onDestroy()
// 为了防止内存泄露,在onDestroy中要置为null
_binding = null
}
}

2.2、在Fragment中使用

同理,假设存在一个MainFragment,对应布局文件为fragment_main.xml,则会生成一个类FragmentMainBinding。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainFragment: Fragment() {
private var _binding: FragmentMainBinding? = null

private val mBinding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMainBinding.inflate(inflater, container, false)
return mBinding.root
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

Activity和Fragment使用View Binding大同小异,记住都要在onDestroyView释放View对象的引用,防止内存泄露。

ViewBinding的工作原理

现在来看看它的原理,打开ActivityMainBinding.java文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// Generated by view binder compiler. Do not edit!
package me.ljh.app.viewbindingexample.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import androidx.viewbinding.ViewBindings;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;
import me.ljh.app.viewbindingexample.R;

public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;

@NonNull
public final Button clickBtn;

@NonNull
public final TextView infoTv;

private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull Button clickBtn,
@NonNull TextView infoTv) {
this.rootView = rootView;
this.clickBtn = clickBtn;
this.infoTv = infoTv;
}

@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}

@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}

@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}

@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.click_btn;
Button clickBtn = ViewBindings.findChildViewById(rootView, id);
if (clickBtn == null) {
break missingId;
}

id = R.id.info_tv;
TextView infoTv = ViewBindings.findChildViewById(rootView, id);
if (infoTv == null) {
break missingId;
}

return new ActivityMainBinding((LinearLayout) rootView, clickBtn, infoTv);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}

代码非常清晰,大致可分为两步:

  1. 通过LayoutInflaterinflate方法加在布局文件。
  2. 通过内部的bind方法将子View复制给类ActivityMainBinding内部对应的成员变量。

为什么不用DataDinding

可能有人知道Data Binding,会好奇问为什么不用它。这里并没有说不要用,其实可以理解View BindingData Binding的一部分。使用View Binding是因为我们只希望使用这一个功,对其它功能不感兴趣,不希望用在项目中。如果除此外,还需要数据跟UI绑定的功能,可以再考虑使用它。

参考资料

  1. https://developer.android.com/topic/libraries/view-binding
  2. https://developer.android.com/topic/libraries/data-binding