前言
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));
}
}
当开启代码混淆后,其中的 bind
、inflate
等方法因为混淆改变了方法名,使用反射的方式便有找不到对应方法的异常。
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(...);
}