属性委托

4 阅读3分钟
fun <VB : ViewBinding> AppCompatActivity.viewBinding(inflate: (LayoutInflater) -> VB) = lazy {
    inflate(layoutInflater).also { binding ->
        setContentView(binding.root)
        if (binding is ViewDataBinding) binding.lifecycleOwner = this
    }
}

//将函数当做参数传递
fun <VB : ViewBinding> Fragment.binding(bind: (View) -> VB) = FragmentBindingDelegate(bind)

//属性委托
class FragmentBindingDelegate<VB : ViewBinding>(private val bind: (View) -> VB) :
    ReadOnlyProperty<Fragment, VB> {

    private var binding: VB? = null

    override fun getValue(thisRef: Fragment, property: KProperty<*>): VB {
        binding = try {
            thisRef.requireView().getBinding(bind).also { binding ->
                if (binding is ViewDataBinding) binding.lifecycleOwner = thisRef.viewLifecycleOwner
            }
        } catch (e: IllegalStateException) {
            throw IllegalStateException("The property of ${property.name} has been destroyed.")
        }
        /*thisRef.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) {
                //fragment销毁时置为null,避免内存泄露
                binding = null
            }
        })*/
        thisRef.viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_DESTROY) {
                binding = null
            }
        })

        return binding!!
    }
}

属性委托(Property Delegation)是 Kotlin 的一个强大特性,它允许你将属性的 gettersetter 逻辑委托给另一个对象处理,而不是在类中直接编写这些逻辑。

1. 核心概念

当你声明一个委托属性时:

val binding by viewBinding(ActivityMainBinding::inflate)

编译器不会生成普通的字段,而是生成一个隐藏的委托对象。当你访问 binding 时,实际上是在调用委托对象的 getValue 方法。

2. 代码中的具体实现

FragmentBindingDelegate 类实现了属性委托:

  • 接口实现:它实现了 ReadOnlyProperty<Fragment, VB> 接口。这意味着它是一个只读属性委托,专门用于 Fragment,返回类型是 VB (ViewBinding)
  • getValue 方法:
    1. 这是委托的核心。当你在 Fragment 中第一次访问 binding 时,这个方法会被调用。
    2. 它通过 thisRef.requireView().getBinding(bind) 完成实际的 ViewBinding 初始化。
    3. 上述代码中还处理了生命周期观察,确保在 Fragment 销毁时将 binding 置为 null,防止内存泄漏。
3. 为什么要用属性委托?

Android 开发中,属性委托非常有用,原因如下:

  1. 简化代码:你不需要在每个 Fragment 中都手动写 private var _binding: ... val binding get() = _binding!! 这种样板代码。

  2. 统一管理逻辑:如上述代码所示,你可以把“如何初始化”、“如何处理生命周期”、“如何防止空指针”等复杂逻辑全部封装在 FragmentBindingDelegate 里。

  3. 延迟初始化:像上述代码的 viewBinding 函数使用了 lazy(Kotlin 内置的委托),确保 ViewBinding 只有在第一次使用时才创建,避免在 onCreateView 之前访问导致崩溃。

4. 委托对象的生命周期

当你在 Fragment 中写:

private val binding by binding(FragmentTestBinding::bind)

这里的 binding(...) 函数会创建一个新的 FragmentBindingDelegate 实例。这个委托实例是作为 Fragment 的一个成员变量存在的。

5. binding 变量的本质

在委托模式下,Fragment 中的 binding 并不是直接持有 ViewBinding 对象,它只是一个“代理人”。

  • 当你调用 binding.root 时,Kotlin 实际上是在调用委托对象的 getValue(this, property) 方法。
  • ViewBinding 的真实引用其实存储在 FragmentBindingDelegate 类内部的 private var binding: VB? = null里。
6.销毁过程是如何发生的?
  • 添加观察者:在第一次访问 binding 时,委托对象会向 viewLifecycleOwner 添加一个生命周期观察者。
  • 监听销毁:当 Fragment 的视图销毁(onDestroy)时,观察者被触发。
  • 置空操作:执行 binding = null。注意: 这里置空的是委托对象内部的那个 binding 变量,而不是 Fragment 里的 binding 属性。
  • 断开引用:一旦委托对象内部的 binding 变为 null,它就放弃了对 ViewBinding 以及它所持有的所有 View 的引用。

虽然 Fragment 实例可能还持有着那个 FragmentBindingDelegate 委托对象,但委托对象内部已经不再持有沉重的 View 引用了。

所以:

  • Fragment 中的 binding 属性:依然存在(因为它是 val),但它现在只是一个指向“空壳”委托对象的指针。
  • 真正的 ViewBinding 对象:因为委托对象内部的引用被切断,且没有其他强引用指向它,它会被 GC 回收。
  • 内存泄露:不会发生,因为最占内存的 View 层级结构已经被释放了。
潜在的小风险

如果 Fragment 本身发生了内存泄露(例如被某个单例或长生命周期对象引用),那么这个 FragmentBindingDelegate 也会跟着泄露。但这个委托模式至少确保了只要 Fragment 进入销毁流程,View 资源就会被立即释放,这是符合 Android 开发规范的。