请点赞关注,你的反对对我意义重大。

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,关注公众号 [彭旭锐] 带你建设外围竞争力。

前言

ViewBinding 是 Android Gradle Plugin 3.6 中新增的个性,用于更加轻量地实现视图绑定(即视图与变量的绑定),能够了解为轻量版本的 DataBinding。 在这篇文章里,我将总结 ViewBinding 应用办法 & 原理,示例程序 AndroidFamilyDemo · KotlinDelegate 有用请记得给 Star ,给小彭一点创作的能源。


前置常识:

  • Kotlin | 委托机制 & 原理 & 利用
  • Kotlin | 扩大函数(终于晓得为什么 with 用 this,let 用 it)
  • Java | 对于泛型能问的都在这里了(含Kotlin)
  • Android | Fragment 外围原理 & 面试题 (AndroidX 版本)

学习路线图


1. 意识 ViewBinding

1.1 ViewBinding 用于解决什么问题?

ViewBinding 是 Android Gradle Plugin 3.6 中新增的个性,用于更加轻量地实现视图绑定(即视图与变量的绑定),能够了解为轻量版本的 DataBinding。

1.2 ViewBinding 与其余视图绑定计划比照

在 ViewBinding 之前,业界曾经有过几种视图绑定计划了,想必你也用过。那么,ViewBinding 作为后起之秀就肯定比前者香吗?我从多个维度比照它们的区别:

角度findViewByIdButterKnifeKotlin SyntheticsDataBindingViewBinding
简洁性
编译期查看
编译速度
反对 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 类,会不会导致包体积霎时增大?不会的, 未应用的类会在混同时被压缩。


2. ViewBinding 的根本用法

这一节咱们来介绍 ViewBinding 的应用办法,内容不多。

提醒: ViewBinding 要求在 Android Gradle Plugin 版本在至多在 3.6 以上。

2.1 增加配置

视图绑定性能按模块级别启用,启用的模块须要在模块级 build.gralde 中增加配置。例如:

build.gradle

android {    ...    viewBinding {        enabled = true    }}

对于不须要生成绑定类的布局文件,能够在根节点申明 tools:viewBindingIgnore="true" 。例如:

<LinearLayout    ...    tools:viewBindingIgnore="true" >    ...</LinearLayout>

2.2 视图绑定

绑定类中提供了 3 个视图绑定 API:

// 绑定到视图 view 上fun <T> bind(view : View) : T// 应用 inflater 解析布局,再绑定到 View 上fun <T> inflate(inflater : LayoutInflater) : T// 应用 inflater 解析布局,再绑定到 View 上fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T
  • 1、在 Activity 中应用

MainActivity.kt

class TestActivity: AppCompatActivity(R.layout.activity_test) {    private lateinit var binding: ActivityTestBinding    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        binding = ActivityTestBinding.inflate(layoutInflater)        setContentView(binding.root)        binding.tvDisplay.text = "Hello World."    }}
  • 2、在 Fragment 中应用

TestFragment.kt

class TestFragment : Fragment(R.layout.fragment_test) {    private var _binding: FragmentTestBinding? = null    private val binding get() = _binding!!    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {        _binding = FragmentTestBinding.bind(root)        binding.tvDisplay.text = "Hello World."    }    override fun onDestroyView() {        super.onDestroyView()        // 置空        _binding = null    }}

2.3 防止内存泄露

这里有一个暗藏的内存泄露问题,你须要了解分明(严格来说这并不是 ViewBinding 的问题,即便你采纳其它视图绑定计划也要思考这个问题)。

问题:为什么 Fragment#onDestroyView() 里须要置空绑定类对象,而 Activity 里不须要?
答:Activity 实例和 Activity 视图的生命周期是同步的,而 Fragment 实例和 Fragment 视图的生命周期并不是齐全同步的,因而须要在 Fragment 视图销毁时,手动回收绑定类对象,否则造成内存泄露。例如:detach Fragment,或者 remove Fragment 并且事务进入返回栈,此时 Fragment 视图销毁但 Fragment 实例存在。对于 Fragment 生命周期和事务在我之前的一篇文章里探讨过:Android | Fragment 外围原理 & 面试题 (AndroidX 版本)

总之,在视图销毁然而管制类对象实例还存活的机会,你就须要手动回收绑定类对象,否则造成内存泄露。

2.4 ViewBinding 绑定类源码

反编译如下:

ActivityTestBinding.java

public final class ActivityTestBinding implements ViewBinding {    private final ConstraintLayout rootView;    public final TextView tvDisplay;    private ActivityTestBinding (ConstraintLayout paramConstraintLayout1, TextView paramTextView)        this.rootView = paramConstraintLayout1;        this.tvDisplay = paramTextView;    }    public static ActivityTestBinding bind(View paramView) {        TextView localTextView = (TextView)paramView.findViewById(2131165363);        if (localTextView != null) {            return new ActivityMainBinding((ConstraintLayout)paramView, localTextView);        }else {          paramView = "tvDisplay";        }        throw new NullPointerException("Missing required view with ID: ".concat(paramView));    }    public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater) {        return inflate(paramLayoutInflater, null, false);    }    public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, boolean paramBoolean) {        paramLayoutInflater = paramLayoutInflater.inflate(2131361821, paramViewGroup, false);        if (paramBoolean) {            paramViewGroup.addView(paramLayoutInflater);        }        return bind(paramLayoutInflater);    }    public ConstraintLayout getRoot() {        return this.rootView;    }}

3. 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

private const val TAG = "ViewBindingProperty"public inline fun <reified V : ViewBinding> viewBindingV1() = viewBindingV1(V::class.java)public inline fun <reified T : ViewBinding> viewBindingV1(clazz: Class<T>): FragmentViewBindingPropertyV1<Fragment, T> {    val bindMethod = clazz.getMethod("bind", View::class.java)    return FragmentViewBindingPropertyV1 {        bindMethod(null, it.requireView()) as T    }}/** * @param viewBinder 创立绑定类对象 */class FragmentViewBindingPropertyV1<in F : Fragment, out V : ViewBinding>(    private val viewBinder: (F) -> V) : ReadOnlyProperty<F, V> {    private var viewBinding: V? = null    @MainThread    override fun getValue(thisRef: F, property: KProperty<*>): V {        // 曾经绑定,间接返回        viewBinding?.let { return it }        // Use viewLifecycleOwner.lifecycle other than lifecycle        val lifecycle = thisRef.viewLifecycleOwner.lifecycle        val viewBinding = viewBinder(thisRef)        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {            Log.w(                TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +                        "The instance of viewBinding will be not cached."            )            // We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak        } else {            lifecycle.addObserver(ClearOnDestroyLifecycleObserver())            this.viewBinding = viewBinding        }        return viewBinding    }    @MainThread    fun clear() {        viewBinding = null    }    private inner class ClearOnDestroyLifecycleObserver : LifecycleObserver {        private val mainHandler = Handler(Looper.getMainLooper())        @MainThread        @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)        fun onDestroy(owner: LifecycleOwner) {            owner.lifecycle.removeObserver(this)            mainHandler.post { clear() }        }    }}

应用示例:

class TestFragment : Fragment(R.layout.fragment_test) {    private val binding : FragmentTestBinding by viewBindingV1()    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {        binding.tvDisplay.text = "Hello World."    }}

洁净清新!后面提出的三个需要也都实现了,当初我为你解答细节:

  • 问题 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

@MainThreadoverride fun getValue(thisRef: F, property: KProperty<*>): V {    // 1、viewBinding 不为空阐明曾经绑定,间接返回    viewBinding?.let { return it }    // 2、Fragment 视图的生命周期    val lifecycle = thisRef.viewLifecycleOwner.lifecycle    // 3、实例化绑定类对象    val viewBinding = viewBinder(thisRef)    if (lifecycle.currentState == Lifecycle.State.DESTROYED) {        // 4.1 如果视图生命周期为 DESTROYED,阐明视图被销毁,此时不缓存绑定类对象(防止内存透露)    } else {        // 4.2 定义视图生命周期监听者        lifecycle.addObserver(ClearOnDestroyLifecycleObserver())        // 4.3 缓存绑定类对象        this.viewBinding = viewBinding    }    return viewBinding}
  • 问题 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

inline fun <F : Fragment, V : ViewBinding> viewBindingV2(    crossinline viewBinder: (View) -> V,    // 相似于创立工厂    crossinline viewProvider: (F) -> View = Fragment::requireView) = FragmentViewBindingPropertyV2 { fragment: F ->    viewBinder(viewProvider(fragment))}class FragmentViewBindingPropertyV2<in F : Fragment, out V : ViewBinding>(    private val viewBinder: (F) -> V) : ReadOnlyProperty<F, V> {    // 以下源码雷同 ...}

应用示例:

class TestFragment : Fragment(R.layout.fragment_test) {    private val binding by viewBindingV2(FragmentTestBinding::bind)    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {        binding.tvDisplay.text = "Hello World."    }}

洁净清新!不应用反射也能够实现,当初我为你解答细节:

  • 问题 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

// -------------------------------------------------------// ViewBindingProperty for Activity// -------------------------------------------------------@JvmName("viewBindingActivity")inline fun <V : ViewBinding> ComponentActivity.viewBinding(    crossinline viewBinder: (View) -> V,    crossinline viewProvider: (ComponentActivity) -> View = ::findRootView): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->    viewBinder(viewProvider(activity))}@JvmName("viewBindingActivity")inline fun <V : ViewBinding> ComponentActivity.viewBinding(    crossinline viewBinder: (View) -> V,    @IdRes viewBindingRootId: Int): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->    viewBinder(activity.requireViewByIdCompat(viewBindingRootId))}// -------------------------------------------------------// ViewBindingProperty for Fragment / DialogFragment// -------------------------------------------------------@Suppress("UNCHECKED_CAST")@JvmName("viewBindingFragment")inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(    crossinline viewBinder: (View) -> V,    crossinline viewProvider: (F) -> View = Fragment::requireView): ViewBindingProperty<F, V> = when (this) {    is DialogFragment -> DialogFragmentViewBindingProperty { fragment: F ->        viewBinder(viewProvider(fragment))    } as ViewBindingProperty<F, V>    else -> FragmentViewBindingProperty { fragment: F ->        viewBinder(viewProvider(fragment))    }}@Suppress("UNCHECKED_CAST")@JvmName("viewBindingFragment")inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(    crossinline viewBinder: (View) -> V,    @IdRes viewBindingRootId: Int): ViewBindingProperty<F, V> = when (this) {    is DialogFragment -> viewBinding(viewBinder) { fragment: DialogFragment ->        fragment.getRootView(viewBindingRootId)    } as ViewBindingProperty<F, V>    else -> viewBinding(viewBinder) { fragment: F ->        fragment.requireView().requireViewByIdCompat(viewBindingRootId)    }}// -------------------------------------------------------// ViewBindingProperty for ViewGroup// -------------------------------------------------------@JvmName("viewBindingViewGroup")inline fun <V : ViewBinding> ViewGroup.viewBinding(    crossinline viewBinder: (View) -> V,    crossinline viewProvider: (ViewGroup) -> View = { this }): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->    viewBinder(viewProvider(viewGroup))}@JvmName("viewBindingViewGroup")inline fun <V : ViewBinding> ViewGroup.viewBinding(    crossinline viewBinder: (View) -> V,    @IdRes viewBindingRootId: Int): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->    viewBinder(viewGroup.requireViewByIdCompat(viewBindingRootId))}// -------------------------------------------------------// ViewBindingProperty for RecyclerView#ViewHolder// -------------------------------------------------------@JvmName("viewBindingViewHolder")inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(    crossinline viewBinder: (View) -> V,    crossinline viewProvider: (RecyclerView.ViewHolder) -> View = RecyclerView.ViewHolder::itemView): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->    viewBinder(viewProvider(holder))}@JvmName("viewBindingViewHolder")inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(    crossinline viewBinder: (View) -> V,    @IdRes viewBindingRootId: Int): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->    viewBinder(holder.itemView.requireViewByIdCompat(viewBindingRootId))}// -------------------------------------------------------// ViewBindingProperty// -------------------------------------------------------private const val TAG = "ViewBindingProperty"interface ViewBindingProperty<in R : Any, out V : ViewBinding> : ReadOnlyProperty<R, V> {    @MainThread    fun clear()}class LazyViewBindingProperty<in R : Any, out V : ViewBinding>(    private val viewBinder: (R) -> V) : ViewBindingProperty<R, V> {    private var viewBinding: V? = null    @Suppress("UNCHECKED_CAST")    @MainThread    override fun getValue(thisRef: R, property: KProperty<*>): V {        // Already bound        viewBinding?.let { return it }        return viewBinder(thisRef).also {            this.viewBinding = it        }    }    @MainThread    override fun clear() {        viewBinding = null    }}abstract class LifecycleViewBindingProperty<in R : Any, out V : ViewBinding>(    private val viewBinder: (R) -> V) : ViewBindingProperty<R, V> {    private var viewBinding: V? = null    protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner    @MainThread    override fun getValue(thisRef: R, property: KProperty<*>): V {        // Already bound        viewBinding?.let { return it }        val lifecycle = getLifecycleOwner(thisRef).lifecycle        val viewBinding = viewBinder(thisRef)        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {            Log.w(                TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +                        "The instance of viewBinding will be not cached."            )            // We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak        } else {            lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this))            this.viewBinding = viewBinding        }        return viewBinding    }    @MainThread    override fun clear() {        viewBinding = null    }    private class ClearOnDestroyLifecycleObserver(        private val property: LifecycleViewBindingProperty<*, *>    ) : LifecycleObserver {        private companion object {            private val mainHandler = Handler(Looper.getMainLooper())        }        @MainThread        @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)        fun onDestroy(owner: LifecycleOwner) {            mainHandler.post { property.clear() }        }    }}class FragmentViewBindingProperty<in F : Fragment, out V : ViewBinding>(    viewBinder: (F) -> V) : LifecycleViewBindingProperty<F, V>(viewBinder) {    override fun getLifecycleOwner(thisRef: F): LifecycleOwner {        try {            return thisRef.viewLifecycleOwner        } catch (ignored: IllegalStateException) {            error("Fragment doesn't have view associated with it or the view has been destroyed")        }    }}class DialogFragmentViewBindingProperty<in F : DialogFragment, out V : ViewBinding>(    viewBinder: (F) -> V) : LifecycleViewBindingProperty<F, V>(viewBinder) {    override fun getLifecycleOwner(thisRef: F): LifecycleOwner {        return if (thisRef.showsDialog) {            thisRef        } else {            try {                thisRef.viewLifecycleOwner            } catch (ignored: IllegalStateException) {                error("Fragment doesn't have view associated with it or the view has been destroyed")            }        }    }}// -------------------------------------------------------// Utils// -------------------------------------------------------@RestrictTo(RestrictTo.Scope.LIBRARY)class ActivityViewBindingProperty<in A : ComponentActivity, out V : ViewBinding>(    viewBinder: (A) -> V) : LifecycleViewBindingProperty<A, V>(viewBinder) {    override fun getLifecycleOwner(thisRef: A): LifecycleOwner {        return thisRef    }}fun <V : View> View.requireViewByIdCompat(@IdRes id: Int): V {    return ViewCompat.requireViewById(this, id)}fun <V : View> Activity.requireViewByIdCompat(@IdRes id: Int): V {    return ActivityCompat.requireViewById(this, id)}/** * Utility to find root view for ViewBinding in Activity */fun findRootView(activity: Activity): View {    val contentView = activity.findViewById<ViewGroup>(android.R.id.content)    checkNotNull(contentView) { "Activity has no content view" }    return when (contentView.childCount) {        1 -> contentView.getChildAt(0)        0 -> error("Content view has no children. Provide root view explicitly")        else -> error("More than one child view found in Activity content view")    }}fun DialogFragment.getRootView(viewBindingRootId: Int): View {    val dialog = checkNotNull(dialog) {        "DialogFragment doesn't have dialog. Use viewBinding delegate after onCreateDialog"    }    val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" }    return with(window.decorView) {        if (viewBindingRootId != 0) requireViewByIdCompat(            viewBindingRootId        ) else this    }}

4. 总结

ViewBinding 是一个轻量级的视图绑定计划,Android Gradle 插件会为每个 XML 布局文件创建一个绑定类。在 Fragment 中应用 ViewBinding 须要留神在 Fragment#onDestroyView() 里置空绑定类对象防止内存透露。但这会带来很多反复编写样板代码,应用属性委托能够收敛模板代码,保障调用方代码洁净清新。

角度findViewByIdButterKnifeKotlin SyntheticsDataBindingViewBindingViewBindingProperty
简洁性
编译期查看
编译速度
反对 Kotlin & Java
收敛模板代码

参考资料

  • View Binding 视图绑定 —— 官网文档
  • View Binding 与 Kotlin 委托属性的奇妙联合,辞别垃圾代码! —— Kirill Rozov 著,仍然范特稀西 译
  • 谁才是 ButterKnife 的终结者? —— fundroid 著
  • 深入研究 ViewBinding 在 include, merge, adapter, fragment, activity 中应用 —— Flywith24 著