⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
Android Jetpack 开发套件是 Google 推出的 Android 应用开发编程范式,为开发者提供了解决应用开发场景中通用的模式化问题的最佳实践,让开发者可将时间精力集中于真正重要的业务编码工作上。
这篇文章是 Android Jetpack 系列文章的第 6 篇文章,完整目录可以移步至文章末尾~
前言
大家好,我是小彭。
过去两年,我们在掘金平台上发表过一些文章,小彭也收到了大家的意见和鼓励。最近,我会陆续搬运到公众号上。
ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用于更加轻量地实现视图绑定(即视图与变量的绑定),可以理解为轻量版本的 DataBinding。 在这篇文章里,我将总结 ViewBinding 使用方法 & 原理,示例程序 AndroidFamilyDemo · KotlinDelegate 有用请记得给 Star ,给小彭一点创作的动力。
记录:2022 年 9 月 8 日修订:优化文章结构
前置知识:
- Kotlin 编程 #2 委托机制 & 原理 & 应用
- Kotlin 编程 #3 扩展函数(终于知道为什么 with 用 this,let 用 it)
- Java | 关于泛型能问的都在这里了(含Kotlin)
- Android Jetpack 开发套件 #7 AndroidX Fragment 核心原理分析
学习路线图
- 认识 ViewBinding
1.1 ViewBinding 用于解决什么问题?
ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用于更加轻量地实现视图绑定(即视图与变量的绑定),可以理解为轻量版本的 DataBinding。
1.2 ViewBinding 与其他视图绑定方案对比
在 ViewBinding 之前,业界已经有过几种视图绑定方案了,想必你也用过。那么,ViewBinding 作为后起之秀就一定比前者香吗?我从多个维度对比它们的区别:
角度 | findViewById | ButterKnife | Kotlin Synthetics | DataBinding | ViewBinding | ❓ |
---|---|---|---|---|---|---|
简洁性 | ✖ | ✖ | ✔ | ✔ | ✔ | ❓ |
编译期检查 | ✖ | ✖ | ✖ | ✔ | ✔ | ❓ |
编译速度 | ✔ | ✖ | ✔ | ✖ | ✔ | ❓ |
支持 Kotlin & Java | ✔ | ✔ | ✖ | ✔ | ✔ | ❓ |
收敛模板代码 | ✖ | ✖ | ✔ | ✖ | ✖ | ❓ |
- 1、简洁性: findViewById 和 ButterKnife 需要在代码中声明很多变量,其他几种方案代码简洁度较好;
- 2、编译检查: 编译期间主要有两个方面的检查:类型检查 + 只能访问当前布局中的 id。findViewById、ButterKnife 和 Kotlin Synthetics 在这方面表现较差;
- 3、编译速度: findViewById 的编译速度是最快的,而 ButterKnife 和 DataBinding 中存在注解处理,编译速度略逊色于 Kotlin Synthetics 和 ViewBinding;
- 4、支持 Kotlin & Java: Kotlin Synthetics 只支持 Kotlin 语言;
- 5、收敛模板代码: 基本上每种方案都带有一定量的模板代码,只有 Kotlin Synthetics 的模板代码是较少的。
可以看到,并没有一种绝对优势的方法,但越往后整体的效果是有提升的。另外,❓是什么呢?
1.3 ViewBinding 的实现原理
AGP 插件会为每个 XML 布局文件创建一个绑定类文件 xxxBinding
,绑定类中会持有布局文件中所有带 android:id
属性的 View 引用。例如,有布局文件为 fragment_test.xml
,则插件会生成绑定类 FragmentTestBinding.java
。
那么,所有 XML 布局文件都生成 Java 类,会不会导致包体积瞬间增大?不会的, 未使用的类会在混淆时被压缩。
- ViewBinding 的基本用法
这一节我们来介绍 ViewBinding 的使用方法,内容不多。
提示: ViewBinding 要求在 Android Gradle Plugin 版本在至少在 3.6 以上。
2.1 添加配置
视图绑定功能按模块级别启用,启用的模块需要在模块级 build.gralde 中添加配置。例如:
build.gradle
1 | groovy复制代码android { |
对于不需要生成绑定类的布局文件,可以在根节点声明 tools:viewBindingIgnore="true"
。例如:
1 | xml复制代码<LinearLayout |
2.2 视图绑定
绑定类中提供了 3 个视图绑定 API:
1 | kotlin复制代码// 绑定到视图 view 上 |
- 1、在 Activity 中使用
MainActivity.kt
1 | kotlin复制代码class TestActivity: AppCompatActivity(R.layout.activity_test) { |
- 2、在 Fragment 中使用
TestFragment.kt
1 | kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) { |
2.3 避免内存泄露
这里有一个隐藏的内存泄露问题,你需要理解清楚(严格来说这并不是 ViewBinding 的问题,即使你采用其它视图绑定方案也要考虑这个问题)。
问题:为什么 Fragment#onDestroyView() 里需要置空绑定类对象,而 Activity 里不需要?
答:Activity 实例和 Activity 视图的生命周期是同步的,而 Fragment 实例和 Fragment 视图的生命周期并不是完全同步的,因此需要在 Fragment 视图销毁时,手动回收绑定类对象,否则造成内存泄露。例如:detach Fragment,或者 remove Fragment 并且事务进入返回栈,此时 Fragment 视图销毁但 Fragment 实例存在。关于 Fragment 生命周期和事务在我之前的一篇文章里讨论过:Android Jetpack 开发套件 #7 AndroidX Fragment 核心原理分析
总之,在视图销毁但是控制类对象实例还存活的时机,你就需要手动回收绑定类对象,否则造成内存泄露。
2.4 ViewBinding 绑定类源码
反编译如下:
ActivityTestBinding.java
1 | java复制代码public final class ActivityTestBinding implements ViewBinding { |
- ViewBinding 与 Kotlin 委托双剑合璧
到这里,ViewBinding 的使用教程已经说完了。但是回过头看,有没有发现一些局限性呢?
- 1、创建和回收 ViewBinding 对象需要重复编写样板代码,特别是在 Fragment 中使用的案例;
- 2、binding 属性是可空的,也是可变的,使用起来不方便。
那么,有没有可优化的方案呢?我们想起了 Kotlin 属性委托,关于 Kotlin 委托机制在我之前的一篇文章里讨论过:Kotlin | 委托机制 & 原理。如果你还不太了解 Kotlin 委托,下面的内容对你会有些难度。下面,我将带你一步步封装 ViewBinding 属性委托工具。首先,我们梳理一下我们要委托的内容与需求,以及相应的解决办法:
需求 | 解决办法 |
---|---|
需要委托 ViewBinding#bind() 的调用 | 反射 |
需要委托 binding = null 的调用 | 监听 Fragment 视图生命周期 |
期望 binding 属性声明为非空不可变变量 | ReadOnlyProperty<F, V> |
3.1 ViewBinding + Kotlin 委托 1.0
我们现在较复杂的 Fragment 中尝试使用 Kotlin 委托优化:
FragmentViewBindingPropertyV1.kt
1 | kotlin复制代码private const val TAG = "ViewBindingProperty" |
使用示例:
1 | kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) { |
干净清爽!前面提出的三个需求也都实现了,现在我为你解答细节:
- 问题 1、为什么可以使用 V::class.java,不是泛型擦除了吗? 利用了 Kotlin 内敛函数 + 实化类型参数,编译后函数体整体被复制到调用处,V::class.java 其实是 FragmentTestBinding::class.java。具体分析见:Java | 关于泛型能问的都在这里了(含Kotlin)
- 问题 2、ReadOnlyProperty<F, V> 是什么? ReadOnlyProperty 是不可变属性代理,通过 getValue(…) 方法实现委托行为。第一个类型参数 F 是属性所有者,第二个参数 V 是属性类型,因为我们在 Fragment 中定义属性,属性类型为 ViewBinding,所谓定义类型参数为 <in F : Fragment, out V : ViewBinding>;
- 问题 3、解释下 getValue(…) 方法? 直接看注释:
FragmentViewBindingPropertyV1.kt
1 | kotlin复制代码@MainThread |
- 问题 4、为什么 onDestroy() 要采用 Handler#post(Message) 完成? 因为 Fragment#viewLifecycleOwner 通知生命周期事件 ON_DESTROY 的时机在 Fragment#onDestroyView 之前。如果不使用 post 的方式,那么业务方要是在 onDestroyView 中访问了 binding,则会二次执行 getValue() 这是不必要的。
3.2 ViewBinding + Kotlin 委托 2.0
V1.0 版本使用了反射,真的一定要反射吗?反射调用 bind 函数的目的就是获得一个 ViewBinding 绑定类对象,或许我们可以试试把创建对象的行为交给外部去定义,类似这样用一个 lambda 表达式实现工厂函数:
FragmentViewBindingPropertyV2.kt
1 | kotlin复制代码inline fun <F : Fragment, V : ViewBinding> viewBindingV2( |
使用示例:
1 | kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) { |
干净清爽!不使用反射也可以实现,现在我为你解答细节:
- 问题 5、(View) -> V 是什么? Kotlin 高阶函数,可以把 lambda 表达式直接作为参数传递,其中 View 是函数参数,而 T 是函数返回值。lambda 表达式本质上是 「可以作为值传递的代码块」。在老版本 Java 中,传递代码块需要使用匿名内部类实现,而使用 lambda 表达式甚至连函数声明都不需要,可以直接传递代码块作为函数值;
- 问题 6、Fragment::requireView 是什么? 把函数 requireView() 作为参数传递。Fragment#requireView() 会返回 Fragment 的根节点,但要注意在 onCreateView() 之前调用 requireView() 会抛出异常;
- 问题 7、FragmentTestBinding::bind 是什么? 把函数 bind() 作为参数传递,bind 函数的参数为 View,返回值为 ViewBinding,与函数声明 (View) -> V 匹配。
3.3 ViewBinding + Kotlin 委托最终版
V2.0 版本已经完成了针对 Fragment 的属性代理,但是实际场景中只会在 Fragment 中使用 ViewBinding 吗?显然并不是,我们还有其他一些场景:
- Activity
- Fragment
- DialogFragment
- ViewGroup
- RecyclerView.ViewHolder
所以,我们有必要将委托工具适当封装得更通用些,完整代码和演示工程你可以直接下载查看: AndroidFamilyDemo · KotlinDelegate
ViewBindingProperty.kt
1 | kotlin复制代码// ------------------------------------------------------- |
- 总结
ViewBinding 是一个轻量级的视图绑定方案,Android Gradle 插件会为每个 XML 布局文件创建一个绑定类。在 Fragment 中使用 ViewBinding 需要注意在 Fragment#onDestroyView() 里置空绑定类对象避免内存泄漏。但这会带来很多重复编写样板代码,使用属性委托可以收敛模板代码,保证调用方代码干净清爽。
角度 | findViewById | ButterKnife | Kotlin Synthetics | DataBinding | ViewBinding | ViewBindingProperty |
---|---|---|---|---|---|---|
简洁性 | ✖ | ✖ | ✔ | ✔ | ✔ | ✔ |
编译期检查 | ✖ | ✖ | ✖ | ✔ | ✔ | ✔ |
编译速度 | ✔ | ✖ | ✔ | ✖ | ✔ | ✔ |
支持 Kotlin & Java | ✔ | ✔ | ✖ | ✔ | ✔ | ✔ |
收敛模板代码 | ✖ | ✖ | ✔ | ✖ | ✖ | ✔ |
参考资料
- View Binding 视图绑定 —— 官方文档
- View Binding 与 Kotlin 委托属性的巧妙结合,告别垃圾代码! —— Kirill Rozov 著,依然范特稀西 译
- 谁才是 ButterKnife 的终结者? —— fundroid 著
- 深入研究 ViewBinding 在 include, merge, adapter, fragment, activity 中使用 —— Flywith24 著
推荐阅读
Android Jetpack 系列文章目录如下(2023/07/08 更新):
- #1 Lifecycle:生命周期感知型组件的基础
- #2 为什么 LiveData 会重放数据,怎么解决?
- #3 为什么 Activity 都重建了 ViewModel 还存在?
- #4 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
- #5 Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI
- #6 ViewBinding 与 Kotlin 委托双剑合璧
- #7 AndroidX Fragment 核心原理分析
- #8 OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势
- #9 食之无味!App Startup 可能比你想象中要简单
- #10 从 Dagger2 到 Hilt 玩转依赖注入(一)
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
本文转载自: 掘金