ViewBinding 封装
前言
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,使用泛型 , 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.LayoutInflaterimport android.view.ViewGroupimport androidx.appcompat.app.AppCompatActivityimport androidx.fragment.app.Fragmentimport androidx.viewbinding.ViewBindingimport 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)*/@PublishedApiinternal 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)@PublishedApiinternal 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)); }} 当开启代码混淆后,其中的 bind、inflate 等方法因为混淆改变了方法名,使用反射的方式便有找不到对应方法的异常。
FATAL EXCEPTION: mainProcess: com.dafay.demo.viewbinding, PID: 309java.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)