1. 概述
KAE(kotlin-android-extensions)插件在Kotlin 1.4.20版本开始被废弃,视图绑定(ViewBinding)是其迁移方案。
更多内容可参照谷歌开发者公众号在2020-12-04的如下推送:
https://mp.weixin.qq.com/s/pa1YOFA1snTMYhrjnWqIgg
2. 视图绑定是什么
ViewBiding,视图绑定,就是将日常开发工作中的通过xml来inflate
成View对象和对视图元素进行findViewById
进行对象获取的过程,封装到了具体的视图绑定类当中。同时,这个类的生成,交于Android Studio中完成,无需开发者手动参与。
更具体点,就是Android Studio会根据布局xml生成一个对应命名的Binding类,类内成员属性时带有xml根元素和每个带有id的视图元素(除非显式设置tools:viewBindingIgnore="true"
),并其中带有对应xml的静态inflate
和bind
方法用以构造视图绑定对象。
一言以概之,视图绑定类就是对xml视图元素的包裹类!
所以,只要开发者会写视图的inflate
和findViewById
方法,那么便无学习成本地会用视图绑定!
3. 视图绑定的理解视角
概述如下:
视图绑定类对象是对xml视图元素的包裹类对象;
视图绑定的静态
bind
方法就是通过对根视图逐一findViewById
获取元素对象引用,进而构造视图绑定类对象;视图绑定的静态
inflate
方法是从xml布局创建视图对象方法并同时调用bind
函数获得视图绑定对象的封装;
所以,视图绑定并没有带来新的学习内容,只是新壶(视图绑定类)装旧酒(inflate、findViewById),而且这个”装酒“的过程交给Android Studio去完成,进而减少了开发者的重复性工作。
所以,视图绑定可以说是几乎无学习成本的!
如果对概述的内容有所疑惑,请继续往下看:
假定有以下Java代码:
View view = LayoutInflater.from(context).inflate(R.layout.view_test, parent, false);
TextView nameTv = view.findViewById(R.id.name_tv);
TextView mailTv = view.findViewById(R.id.mail_tv);
这类代码应该是不采用ButterKnife、KAE、ViewBinding等任何方案时,非常常见的一种代码方式。
如果用视图绑定,这里就缩减成如下方式:
ViewTestBinding binding = ViewTestBinding.inflate(LayoutInflater.from(context), parent, false);
拿到视图绑定引用后,就可以非常方便地任意访问xml里的对应元素了:
binding.getRoot() // xml根视图
binding.nameTv; // xml中的name_tv
binding.mailTv; // xml中的mail_tv
这样,即使xml里有再多的视图元素要访问,再也不用手动一个个地去写findViewById
了,而是转而通过视图绑定对象去统一访问。
所以,视图绑定类就是对xml视图元素的包裹类,仅此而已。
而视图绑定的类,由Android Studio自行根据xml实际情况生成并修改,并不用开发者手动维护。
那么,视图绑定是何时给xml里的视图元素对应找到视图对象引用的呢?答案就在视图绑定类的静态方法里。
每个视图绑定类都必定包含下面三个静态方法:
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater)
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent)
@NonNull
public static ViewTestBinding bind(@NonNull View rootView)
这也是初看视图绑定时比较困惑的地方,因为各类文档中用了不同的静态方法去获取视图对象实例。
但是,这三者其实是统一的,一个参数的inflate
方法调用了三个参数的inflate方法,三个参数的inflate
方法其实调用的是bind
方法。
也就是说,无论用的是哪个静态方法去获取视图对象对象,其实最终都是用的都是bind
!
而对于inflate
方法,其实也不过是LayoutInflater#inflate(int, ViewGroup, boolean)
的简单封装:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
目的,是为了封装对应的xml布局到inflate
方法内部(视图绑定类命名本身已经与对应的xml布局文件所一一对应起来)。
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.view_test, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
而bind
方法里,则是最后根据对于的根视图来对所有视图元素进行findViewById
的过程(枯燥乏味的过程,这里不贴代码了,况且每个xml里会有不同的id和数量);
想明白了上面的过程,其实视图绑定还可以这么用
View view = LayoutInflater.from(context).inflate(R.layout.view_test, parent, false);
ViewTestBinding binding = ViewTestBinding.bind(view);
这样出来的binding引用和直接用inflate
获取的引用其实是一样的效果,只不过这种写法,有两点重复的内容了:
- 视图绑定类本身即是与layout的文件名匹配生成的,两者出现其一就足够了,这里两者都出现了;
- 这里的view引用和通过
binding.getRoot()
获得的引用会是同一个(但类型不同),同一个引用可以通过两种方式访问了;
这两点影响较小,不过各处文档中都没有出现这种写法,个人猜测,可能是考虑到啰嗦与重复了吧,毕竟一个函数调用能做完的事情,为什么要分两步?
4. 视图绑定在Activity/Fragment中的用法
这节来结合官方开发文档中视图绑定的用法实例来看下,
注,下面示例代码来自:https://developer.android.google.cn/topic/libraries/view-binding#kotlin
4.1 在Activity中使用视图绑定
官方样例代码:
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
看到这个代码,怎么还能说视图绑定是无学习成本的呢?不是还要看视图绑定的inflate
方法怎么用吗?
但是呢,有没觉得,这个inflate
方法很眼熟?其实吧,这个写法,是精简写法了,不信?一步步来拆解一下:
没有视图绑定之前,其实可以这样去给Activity去setContentView
:
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val view = layoutInflater.inflate(R.layout.result_profile, null, false)
setContentView(view)
}
只是,普遍写法是:setContentView(R.layout.result_profile)
普遍写法不代表只有这种方式,而视图绑定方式,只是将上面layoutInflater.inflate
的方式进行了一层封装并同时构造了视图绑定对象。
其实吧,如果不想改变原来的setContentView(R.layout.xxx)
的方式,又想获取视图绑定实例,也是有办法的,如下:
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.result_profile)
binding = ActivityMainBinding.bind(findViewById<ViewGroup>(android.R.id.content).getChildAt(0))
}
效果和官方示例其实是一样的,只是,麻烦和啰嗦了一些。这里仅为说明视图绑定的情况,不代表个人建议如上写法。
4.2 在Fragment中使用视图绑定
官方样例代码:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
可能有一部分开发者会疑惑,为什么这里要对视图绑定属性进行置空呢?上面Activity使用过程中却不需要?
概述性的描述为:不置空会有可能的内存泄漏风险。
但是,这个并不是视图绑定所带来的风险,更直接地说,这个锅,视图绑定不接。
为讲清楚这个,先从Java代码中说起,在没有使用视图绑定之前,有下面代码:
private View mRootView;
@Override
public View onCreateView (LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.result_profile, container, false);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mRootView = null;
}
如果在onDestroyView
没有对mRootView
进行置空,那么原理上同样地存在内存泄漏风险。
事实上,上述mRootView
的写法即使没有在onDestroyView中进行相应的置空,大多情况下也不会内存泄漏(LeakCanary等内存泄漏检测结果),这又是为什么呢?
这是因为大多使用Fragment的过程中,很少见有FragmentTransaction#attach
和FragmentTransaction#detach
的使用,又或者不常用FragmentPagerAdapter
(里面对于Fragment的切换是用attach/detach
方法),所以Fragment与View的生命周期几乎等同进而不会有内存泄漏问题。
回过来,视图绑定类就是对xml视图元素的包裹类,对于视图绑定在Fragment中的内存泄漏风险,并不是视图绑定的锅,而是对Fragment和View之间的设计以及内存泄漏原理理解不足够产生的“偏见”,如果能处理好Fragment与View之间的关系,自然也能在Fragment中用好视图绑定。
题外话,其实在业务Fragment中保存mRootView
其实是多余的,在onCreateView
执行后并且在onDestroyView
执行后不久的这段生命周期内,通过Fragment#getView
方法拿出来的即是onCreateView
中的返回值,有兴趣的可以从源码上查看Fragment中的生命周期与视图View之间的详细设计。
5. 关于视图绑定需要说明的
5.1 视图绑定跟模块相关
模块(module)下的build.gradle
的中,增加:
android {
...
buildFeatures {
viewBinding = true
}
...
}
只有增加了该内容的module才会开启视图绑定功能,为module下的layout中的xml生成对应的视图绑定类,没有该部分内容的module则不会
由于视图绑定类是代码文件,可以引用别的module中的视图绑定类来用。
5.2 关于tools:viewBindingIgnore="true"
放入xml根布局,则不生成对应的视图绑定类。
放入视图id元素声明统计,则不生成该id对应的视图属性。
属于优化生成的视图绑定类代码内容的优化项。
5.3 视图绑定对于xml中的include处理
视图绑定类的是以xml布局文件作为维度的,所以仅会对当前xml中的视图id元素进行属性生成,而xml中include另一个xml的内容相关不会进行相应属性生成。
如果对第4节的内容理解清楚,那其实include的内容就是通过视图绑定的静态bind
方法就可以生成相应的视图绑定对象来使用。
当然,在include标签里新声明id且被include的xml里没有使用merge标签时也可以一步到位,但是吧,如果被include的xml根布局也有id,情况就有点多了。
这里不继续讨论,因为不同人不同场景可能对这部分有不同的选择,更多场景应该是findViewById和视图id之间的关联和理解,而非视图绑定本身需要关注的内容。
6. 关于对视图绑定的进一步封装
封装是一种避免犯错规范使用的一种好方案,但是视图绑定从本身设计上和使用上就足够简单,为了更简洁和更安全,再进行一层一层的封装,一定程度加大了学习理解和后续维护拓展的成本。
与其理解使用对于视图绑定与各项其他内容的封装以及维护相应的逻辑,不如把重点放在视图绑定和视图框架本身的理解和使用上。
目前,网上对于视图绑定有各种八仙过海式的封装使用,如反射、基类封装、Kotlin的具化泛型、Kotlin属性委托方式等等,封装的角度和考虑往往相当深入,其中个人看到的属性委托方式封装视图绑定的技术内容更是将Kotlin的属性委托、Jetpack中的LifeCycle组件、内存泄漏问题研究得妥妥帖帖。
如果从学习和深入的角度去看待各种封装思想和用处,这是非常好的一件事,但是反过来,视图绑定本身使用上就不复杂不难用,看到各路封装容易让不明所以的人容易对视图绑定有个不容易用的错觉。
即使不进行任何封装,本身视图绑定的使用就是一行或者几行代码的事情,至于对于内存泄漏,本身的解决方案也仅是在恰当的位置置空也并不复杂,这里更多需要注意的是对于Fragment/View/内存泄漏的理解,治标不如治本。
7. 为什么要从KAE换用视图绑定
无法否认的一点是,视图绑定在使用上,并没有KAE中直接通过id访问这么方便,那么为什么要从KAE换用视图绑定方案呢?
个人角度的一些想法:
- KAE仅支持Kotlin代码,视图支持Kotlin和Java;
另一种观点:现在主推Kotlin开发代码了,Java是否能用影响不大。
-
KAE在每个使用id访问视图的地方使用
HashMap
并开发代码中嵌入了相应代码,造成了运行时内存额外占用和更大的代码打包体积;
视图绑定也会产生相应的代码生成,不过多处使用的视图绑定都是同一个所以代码打包增加量是个常数,包装类也有内存占用成本,但是成本也小于HashMap
的使用。
另一种观点:HashMap
这一点点的内存占用成本和代码打包体积在整体工程项目下往往可以忽略不计。
- KAE并不是空安全的,而视图绑定对空安全的支持更好;
因为一个视图id即使不在当前的视图布局内,KAE使用上在代码开发阶段也可以通过id去访问,只有在运行时才会出现异常,而视图绑定则因为通过视图绑定对象限定了视图布局的范围更安全;再者在横竖屏的xml有不同元素时,视图绑定会标注视图是否可空,而KAE有空安全风险。
杠:也不是什么问题,运行到了时候出现异常再检查修改,分分钟的事。
答:万一这个视图访问的逻辑业务层次很深,不容易触发该部分业务逻辑的话,存在一定风险。
杠:那就是代码逻辑性和测试的问题了,规范严谨的开发者不会犯这样的错误的。
答:。。。。。。
最后,也是最重要的一点,那就是KAE已经被官方废!弃!了!,替代方案是视图绑定。
8. 总结
其实视图绑定处于一个挺尴尬的时期,前有KAE更方便的使用,后有Jetpack Compose这种声明式UI架构正在路上。KAE被废弃了,开发者应避免使用被废弃的内容;Jetpack Compose是声明式UI,对于现存的命令式UI开发框架是个较大的冲击,加上官方在逐步优化Compose性能、新框架的学习迁移成本等因素,这个应该仍需一定的时间和过程。
好在,视图绑定内容本身几乎没有学习成本,使用上也并不麻烦,所以,在现有的命令式UI开发框架下,仍可一战!