不知道你有没有用烦findViewById
,尤其是项目中控件比较多的场景,密密麻麻的findViewById
看起来都头疼。谷歌也看到了这个问题,于是推出ViewBinding
来专门解决掉它。
findViewById
的工作原理熟悉安卓开发的小伙伴应该知道,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 class View { @Nullable public final <T extends View > T findViewById (@IdRes int id) { if (id == NO_ID) { return null ; } return findViewTraversal(id); } protected <T extends View > T findViewTraversal (@IdRes int id) { if (id == mID) { return (T)this ; } return null ; } } @UiThread public abstract class ViewGroup extends View { @Override protected <T extends View > T findViewTraversal (@IdRes int id) { if (id == mID) { return (T)this ; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0 ; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0 ) { 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
中使用 假设现在有一个名为MainActivity
的Activity
,它对应的布局文件为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 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.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() _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 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) { 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)); } }
代码非常清晰,大致可分为两步:
通过LayoutInflater
的inflate
方法加在布局文件。
通过内部的bind
方法将子View复制给类ActivityMainBinding
内部对应的成员变量。
为什么不用DataDinding
可能有人知道Data Binding
,会好奇问为什么不用它。这里并没有说不要用 ,其实可以理解View Binding
是Data Binding
的一部分。使用View Binding
是因为我们只希望使用这一个功,对其它功能不感兴趣,不希望用在项目中。如果除此外,还需要数据跟UI绑定的功能,可以再考虑使用它。
参考资料
https://developer.android.com/topic/libraries/view-binding
https://developer.android.com/topic/libraries/data-binding