Cursor blinking

ViewBinding 封装

Android 基础|Android|字数 766|阅读时长≈ 2 分钟

前言

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

基本使用

在 Activity 中使用视图绑定

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

在 Fragment 中使用视图绑定

Code
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。

Code
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 以避免内存泄露。

Code
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 中使用示例

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

Fragment 中使用示例

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

使用反射

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

Code
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 定义

Code
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 定义

Code
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 文件里引入

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

基类中使用

BaseActivity 定义

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

BaseFragment 定义

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

Activity 中使用示例

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

Fragment 中使用示例

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

直接在 Activity、Fragment 里面使用

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

ViewBindingPropertyDelegate 原理简析

  • kotlin 的委托

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

Code
/*** 使用 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 呢?

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

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

Code
@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

示例

Code
// 演示内存泄露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 成员便引发了内存泄露

Code
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()        }    } }
screenshot-1718587849255.png
screenshot-1718587849255.png

解决

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

Code
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 生成的视图绑定类如下

Code
// 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 等方法因为混淆改变了方法名,使用反射的方式便有找不到对应方法的异常。

Code
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:3813at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308at android.os.Handler.dispatchMessage(Handler.java:106at android.os.Looper.loopOnce(Looper.java:201at android.os.Looper.loop(Looper.java:288at android.app.ActivityThread.main(ActivityThread.java:7898at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

解决

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

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

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

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

参考文档

视图绑定 (developer.android.com)