ViewBinding 封装

1,890 阅读6分钟

前言

ViewBinding 视图绑定功能可让您更轻松地编写与视图交互的代码。在模块中启用视图绑定后,它会为该模块中显示的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。ViewBinding 优点和配置方式见于 视图绑定

基本使用

在 Activity 中使用视图绑定

private lateinit var binding: ResultProfileBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ResultProfileBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
}

在 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
}

如上代码所示,如果有很多 Fragment,每一个都要拷贝一份相同的代码,不符合 Don't repeat yourself 的原则,所以尝试对其进行封装。

几种封装方式

不使用反射

BaseActivity,使用泛型 <VB : ViewBinding>ActivityMainBinding::inflate 函数作为参数传递给 BaseActivity。

abstract class BaseActivity<VB : ViewBinding>(private val inflate: (LayoutInflater) -> VB) : AppCompatActivity() {
    lateinit var binding: VB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = inflate(layoutInflater)
        setContentView(binding.root)
    }
}

BaseFragment,onDestroyView()_binding = null 以避免内存泄露。

abstract class BaseFragment<VB : ViewBinding>(private val inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB) : Fragment() {
    private var _binding: VB? = null
    protected val binding: VB get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Activity 中使用示例

class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {}

Fragment 中使用示例

class TestFragment : BaseFragment<FragmentTestBinding>(FragmentTestBinding::inflate){}

使用反射

在基类中通过反射调用 ActivityMainBinding.java (ViewBinding 编译后生成的类)中的 inflate 方法获取视图 View。

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import java.lang.reflect.ParameterizedType

// 扩展函数,通过反射 inflate 方法获取 view
@JvmName("inflateWithGeneric")
fun <VB : ViewBinding> AppCompatActivity.inflateBindingWithGeneric(layoutInflater: LayoutInflater): VB =
    withGenericBindingClass<VB>(this) { clazz ->
        clazz.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as VB
    }

@JvmName("inflateWithGeneric")
fun <VB : ViewBinding> Fragment.inflateBindingWithGeneric(
    layoutInflater: LayoutInflater,
    parent: ViewGroup?,
    attachToParent: Boolean
): VB =
    withGenericBindingClass<VB>(this) { clazz ->
        clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
            .invoke(null, layoutInflater, parent, attachToParent) as VB
    }

private fun <VB : ViewBinding> withGenericBindingClass(any: Any, block: (Class<VB>) -> VB): VB {
    var genericSuperclass = any.javaClass.genericSuperclass
    var superclass = any.javaClass.superclass
    // 多继承时的递归处理
    while (superclass != null) {
        if (genericSuperclass is ParameterizedType) {
            try {
                return block.invoke(genericSuperclass.actualTypeArguments[0] as Class<VB>)
            } catch (e: Exception) {
                throw e
            }
        }
        genericSuperclass = superclass.genericSuperclass
        superclass = superclass.superclass
    }
    throw IllegalArgumentException("There is no generic of ViewBinding.")
}

BaseActivity 定义

abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
    lateinit var binding: VB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = inflateBindingWithGeneric(layoutInflater)
        setContentView(binding.root)
    }
}

BaseFragment 定义

abstract class BaseFragment<VB : ViewBinding> : Fragment() {
    private var _binding: VB? = null
    protected val binding: VB get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = inflateBindingWithGeneric(inflater, container, false)
        return binding.root
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }    
}

ViewBindingPropertyDelegate 框架

使用 ViewBindingPropertyDelegate 开源框架,在 module 下的 build.gradle 文件里引入

implementation 'com.github.kirich1409:viewbindingpropertydelegate:1.5.6'

基类中使用

BaseActivity 定义

abstract class BaseActivity(@LayoutRes contentLayoutId: Int) : AppCompatActivity(contentLayoutId) {
    protected abstract val binding: ViewBinding
}

BaseFragment 定义

abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) {
    protected abstract val binding: ViewBinding
}

Activity 中使用示例

class MainActivity : BaseActivity(R.layout.activity_main) {
    override val binding: ActivityMainBinding by viewBinding()
}

Fragment 中使用示例

class TestFragment : BaseFragment(R.layout.fragment_test) {
    override val binding: FragmentTestBinding by viewBinding()
}

直接在 Activity、Fragment 里面使用

class TestActivity : AppCompatActivity(R.layout.activity_test) {
    private val binding: ActivityTestBinding by viewBinding()
}

ViewBindingPropertyDelegate 原理简析

  • kotlin 的委托

本质上使用了 kotlin 的委托,关于 kotlin 委托,这篇 一文彻底搞懂Kotlin中的委托 博客写的很好。拿对 Fragment 处理举例

/**
* 使用 ReadOnlyProperty 属性委托,
* 添加 fragment 声明周期的监听,以便 onDestoryView 时 viewBinding 置空
* 通过反射调用 xxxBinding.java 里面的 bind、inflate 方法(混淆时要 keep)
*/
@PublishedApi
internal class FragmentViewBindingProperty<T : ViewBinding>(
    private val viewBinder: ViewBinder<T>
) : ReadOnlyProperty<Fragment, T> {
    internal var viewBinding: T? = null
    private val lifecycleObserver = BindingLifecycleObserver()

    @MainThread
    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        checkIsMainThread()
        this.viewBinding?.let { return it }
        val view = thisRef.requireView()
        thisRef.viewLifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        return viewBinder.bind(view).also { vb -> this.viewBinding = vb }
    }

    private inner class BindingLifecycleObserver : DefaultLifecycleObserver {
        private val mainHandler = Handler(Looper.getMainLooper())

        @MainThread
        override fun onDestroy(owner: LifecycleOwner) {
            owner.lifecycle.removeObserver(this)
            // Fragment.viewLifecycleOwner call LifecycleObserver.onDestroy() before Fragment.onDestroyView().
            // That's why we need to postpone reset of the viewBinding
            mainHandler.post {
                viewBinding = null
            }
        }
    }
}

/**
 * Create new [ViewBinding] associated with the [Fragment][this]
 */
@Suppress("unused")
inline fun <reified T : ViewBinding> Fragment.viewBinding(): ReadOnlyProperty<Fragment, T> {
    return FragmentViewBindingProperty(DefaultViewBinder(T::class.java))
}

/**
 * Create new [ViewBinding] associated with the [Fragment][this] and allow customize how
 * a [View] will be bounded to the view binding.
 */
@Suppress("unused")
inline fun <T : ViewBinding> Fragment.viewBinding(
    crossinline bindView: (View) -> T
): ReadOnlyProperty<Fragment, T> {
    return FragmentViewBindingProperty(viewBinder(bindView))
}

@RestrictTo(RestrictTo.Scope.LIBRARY)
@PublishedApi
internal class DefaultViewBinder<T : ViewBinding>(
    private val viewBindingClass: Class<T>
) : ViewBinder<T> {

    /**
     * Cache static method `ViewBinding.bind(View)`
     */
    private val bindViewMethod by lazy(LazyThreadSafetyMode.NONE) {
        viewBindingClass.getMethod("bind", View::class.java)
    }

    /**
     * Create new [ViewBinding] instance
     */
    @Suppress("UNCHECKED_CAST")
    override fun bind(view: View): T {
        return bindViewMethod(null, view) as T
    }
}

/**
 * Create instance of [ViewBinding] from a [View]
 */
interface ViewBinder<T : ViewBinding> {
    fun bind(view: View): T
}

@PublishedApi
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal inline fun <T : ViewBinding> viewBinder(crossinline bindView: (View) -> T): ViewBinder<T> {
    return object : ViewBinder<T> {
        override fun bind(view: View) = bindView(view)
    }
}
  • 布局与 Binding 对象建立关联

    • CreateMethod.BIND

      例如下面这种使用方式,viewBinding() 默认通过反射 ActivityMainBinding.java 的 bind 方法绑定到 View,怎么获取这个 View 呢?

      class TestActivity : AppCompatActivity(R.layout.activity_test) {
          private val binding: ActivityTestBinding by viewBinding()
      }
      

      Activity 中获取 setContentView() 所对应的这个 View 如下

      @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
      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")
          }
      }
      

      Fragment 中绑定的 View 是直接 Fragment::getView() 获取。

    • CreateMethod.INFLATE

      这种方式跟上面 使用反射 小节一样。

问题

内存泄露

Fragment 中常见的一种内存泄露,例如下面这个例子,回退栈持有 Fragment 的引用,Fragment 生命周期 onDestoryView() 调用后,view 已经被移除,而且对应的 binding 对象还被 Fragment 持有引用,因此造成内存泄露。引用链条为 BackStack→Fragment→binding

示例

// 演示内存泄露
abstract class BaseFragment<VB : ViewBinding>(private val inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB) : Fragment() {
    protected lateinit var binding: VB

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
    }
}

Test01Fragment、Test02Fragment 是 BaseFragment 子类,这样在切换后,Test01Fragment 的 binding 成员便引发了内存泄露

class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {

    override fun bindListener() {
        binding.btnReplace1.setOnClickListener {
            supportFragmentManager.beginTransaction()
                .replace(R.id.fl_content, Test01Fragment())
                .addToBackStack(null) // fragment 对象不会被释放
                .commit()
        }

        binding.btnReplace2.setOnClickListener {
            supportFragmentManager.beginTransaction()
                .replace(R.id.fl_content, Test02Fragment())
                .addToBackStack(null)
                .commit()
        }
    }

}

解决

onDestoryView() 时将 _binding = null 变量置空

abstract class BaseFragment<VB : ViewBinding>(private val inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB) : Fragment() {
    private var _binding: VB? = null
    protected val binding: VB get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

代码混淆

ViewBinding 基于 .xml 生成的视图绑定类如下

// Generated by view binder compiler. Do not edit!
package com.dafay.demo.viewbinding.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import androidx.viewbinding.ViewBindings;
import com.dafay.demo.viewbinding.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final FrameLayout rootView;

  @NonNull
  public final Button btnReplace1;

  @NonNull
  public final Button btnReplace2;

  @NonNull
  public final FrameLayout flContent;

  private ActivityMainBinding(@NonNull FrameLayout rootView, @NonNull Button btnReplace1,
      @NonNull Button btnReplace2, @NonNull FrameLayout flContent) {
    this.rootView = rootView;
    this.btnReplace1 = btnReplace1;
    this.btnReplace2 = btnReplace2;
    this.flContent = flContent;
  }

  @Override
  @NonNull
  public FrameLayout 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) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.btn_replace_1;
      Button btnReplace1 = ViewBindings.findChildViewById(rootView, id);
      if (btnReplace1 == null) {
        break missingId;
      }

      id = R.id.btn_replace_2;
      Button btnReplace2 = ViewBindings.findChildViewById(rootView, id);
      if (btnReplace2 == null) {
        break missingId;
      }

      id = R.id.fl_content;
      FrameLayout flContent = ViewBindings.findChildViewById(rootView, id);
      if (flContent == null) {
        break missingId;
      }

      return new ActivityMainBinding((FrameLayout) rootView, btnReplace1, btnReplace2, flContent);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

当开启代码混淆后,其中的 bindinflate 等方法因为混淆改变了方法名,使用反射的方式便有找不到对应方法的异常。

FATAL EXCEPTION: main
Process: com.dafay.demo.viewbinding, PID: 309
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.dafay.demo.viewbinding/com.dafay.demo.viewbinding.MainActivity}: java.lang.IllegalArgumentException: There is no generic of ViewBinding.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3676)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3813)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7898)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: java.lang.IllegalArgumentException: There is no generic of ViewBinding.
at com.dafay.demo.viewbinding.utils.ViewBindingUtilsKt.genericViewBindingClass(SourceFile:42)
at com.dafay.demo.viewbinding.utils.ViewBindingUtilsKt.inflateWithGeneric(SourceFile:13)
at com.example.demo.lab.base.base.BaseActivity.onCreate(SourceFile:15)
at android.app.Activity.performCreate(Activity.java:8290)
at android.app.Activity.performCreate(Activity.java:8269)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1384)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3657)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3813) 
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101) 
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308) 
at android.os.Handler.dispatchMessage(Handler.java:106) 
at android.os.Looper.loopOnce(Looper.java:201) 
at android.os.Looper.loop(Looper.java:288) 
at android.app.ActivityThread.main(ActivityThread.java:7898) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

解决

在混淆 proguard-rules.pro 配置文件里面,keep 对应的类

# keep 所有生成的视图绑定类
-keep class * implements androidx.viewbinding.ViewBinding { *; }
-keepclassmembers class * { public <init>(android.view.View); }

更细粒度的配置,keep 对应的方法

-keep,allowoptimization class * implements androidx.viewbinding.ViewBinding {
    public static *** bind(android.view.View);
    public static *** inflate(...);
}

参考文档

原文链接

视图绑定 (developer.android.com)

View Binding 与Kotlin委托属性的巧妙结合,告别垃圾代码!

一文彻底搞懂Kotlin中的委托(深入简出,博客结构很好)

ViewBindingPropertyDelegate