LiveData粘性清除与ViewBing基础
LiveData概要
-
粘性数据更新:子线程更新UI的原理(调用到了主线程的setValue并执行事件分发)
-
postValue:子线程更新UI
-
调用setValue
-
进入主线程setValue:
-
将粘性数据放到了mData 中了;
-
并且粘性数据的版本号会++(从-1增加为0)
-
-
-
粘性数据如何触发
-
在Activity中进行注册
-
封装宿主与观察者
-
在宿主的六个生命周期中都有去调用onStateChanged,此函数的最后一句(376行,当宿主存活时进入下一阶段)
-
此时宿主存活(因为activeStateChanged函数的参数为bool值,且实参是shouldBeActive返回值,当宿主存活时,返回值为true);在423行,当宿主真正存活,进行事件分发,并且传入当前的this做为dispatchingValue参数
-
dispatchingValue参数:@Nullable ObserverWrapper initiator(ObserverWrapper是LifecycleBoundObserver的父类),
this不空,进入这里
-
比较数据版本号,判断是否分发粘性数据;最开始的版本号都是-1,但是之前++了(宿主此时的粘性数据的版本号为0,与此时的观察者中的数据版本-1不相等,此时不会进入下面的if退出;会执行
observer.mLastVersion = mVersion; //noinspection unchecked observer.mObserver.onChanged((T) mData);//分发粘性数据
-
比较版本号,决定是否分发事件:分发粘性数据
-
继续调用:
-
LiveDataBus
-
概述:
-
解决粘性事件:实现可以通过开关决定是否启用粘性(依托Kotlin默认参数)
-
解决问题:
-
使用反射技术,使得在比较版本号的时候,执行 return;从而不进行事件分发
if (observer.mLastVersion >= mVersion) { return; }
-
-
-
设计思路:
-
通过反射拿到宿主(mLasetVersion),观察者(mVersion),然后实现状态对齐(mLastVersion.set(observerWraper, mVersionValue))
-
代码思路:
-
具体实现步骤
-
设计总线
-
代码
private val bus : MutableMap<String, BusMutableLiveData<Any>> by lazy { HashMap() }
-
参数解释:为什么要使用Map
-
// 封装总线类:存放订阅者,为了程序扩展性(方便用户指定操作对象,用户希望对于某个MyLiveData操作)所以搞成Map类型: Map类型: key:这条数据的名字 Any:这条数据的类型
-
key:数据的名字作为标识,就是下面那个红色框里的
-
value:BusMutableLiveData< Any >:
- 用户使用参数进行指定,决定对于何种类型数据进行操作
- 就不像之前的哪像直接写死,这样的扩展性更好
-
使用懒加载:kotlin的好特性
-
HashMap:数据结构
-
-
-
设计with函数:暴露函数,给外界注册订阅者关系
-
概述:
/* // 暴露一个函数,给外界注册 订阅者关系 //扩展性:使用泛型,with函数的实参泛型T类型一旦确定,那么其返回值的泛型T类型随机确定并且下面的BusMutableLiveData中的两个泛型T类型也随之确定 //安全性:搞个同步锁,并发环境 //第二个参数:控制变化,直接传相应的字节码文件就行了 //第三个参数:开关,是否启用数据粘性(配合kotlin中的默认参数,默认是开启数据粘性的)*/
-
代码:
@Synchronized fun <T> with(key: String, type: Class<T>, isStick: Boolean = true) : BusMutableLiveData<T> { if (!bus.containsKey(key)) {//判断是否有重复的KEY bus[key] = BusMutableLiveData(isStick)//运算符重载 } return bus[key] as BusMutableLiveData<T>//强制类型转换 }
-
-
设计BusMutableLiveData类:
-
设计hook函数:使用反射
-
目标:拿到mLastVersion(宿主),拿到mVersion(观察者的),然后对齐(直接赋值过去)就不会进行粘性的分发了
-
代码:Kotlin
private fun hook(observer: Observer<in T>) { // TODO 1.得到mLastVersion // 获取到LivData的类中的mObservers对象,::class这个是kotlin的,需要转换成java的 val liveDataClass = LiveData::class.java //拿到字段 val mObserversField: Field = liveDataClass.getDeclaredField("mObservers") mObserversField.isAccessible = true // 私有修饰也可以访问 // 获取到这个成员变量的对象 Any == Object val mObserversObject: Any = mObserversField.get(this) // 得到map对象的class对象 private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = val mObserversClass: Class<*> = mObserversObject.javaClass // 获取到mObservers对象的get方法 protected Entry<K, V> get(K k) { val get: Method = mObserversClass.getDeclaredMethod("get", Any::class.java) get.isAccessible = true // 私有修饰也可以访问 // 执行get方法 val invokeEntry: Any = get.invoke(mObserversObject, observer) // 取到entry中的value is "AAA" is String is是判断类型 as是转换类型 var observerWraper: Any? = null if (invokeEntry != null && invokeEntry is Map.Entry<*, *>) { observerWraper = invokeEntry.value } if (observerWraper == null) { throw NullPointerException("observerWraper is null") } // 得到observerWraperr的类对象 val supperClass: Class<*> = observerWraper.javaClass.superclass val mLastVersion: Field = supperClass.getDeclaredField("mLastVersion") mLastVersion.isAccessible = true // TODO 2.得到mVersion val mVersion: Field = liveDataClass.getDeclaredField("mVersion") mVersion.isAccessible = true // TODO 3.mLastVersion=mVersion,状态对齐 val mVersionValue: Any = mVersion.get(this) mLastVersion.set(observerWraper, mVersionValue) }
-
代码:java版本:
private void hook(Observer<? super T> observer) { try { // TODO 1.得到mLastVersion // 获取到LivData的类中的mObservers对象 Class<LiveData> liveDataClass = LiveData.class; Field mObserversField = liveDataClass.getDeclaredField("mObservers"); mObserversField.setAccessible(true); // 获取到这个成员变量的对象 Object mObserversObject = mObserversField.get(this); // 得到map对象的class对象 Class<?> mObserversClass = mObserversObject.getClass(); // 获取到mObservers对象的get方法 Method get = mObserversClass.getDeclaredMethod("get", Object.class); get.setAccessible(true); // 执行get方法 Object invokeEntry = get.invoke(mObserversObject, observer); // 取到entry中的value Object observerWraper = null; if (invokeEntry != null && invokeEntry instanceof Map.Entry) { observerWraper = ((Map.Entry) invokeEntry).getValue(); } if (observerWraper == null) { throw new NullPointerException("observerWraper is null"); } // 得到observerWraperr的类对象 Class<?> supperClass = observerWraper.getClass().getSuperclass(); Field mLastVersion = supperClass.getDeclaredField("mLastVersion"); mLastVersion.setAccessible(true); // TODO 2.得到mVersion Field mVersion = liveDataClass.getDeclaredField("mVersion"); mVersion.setAccessible(true); // TODO 3.mLastVersion=mVersion Object mVersionValue = mVersion.get(this); mLastVersion.set(observerWraper, mVersionValue); } catch (Exception e) { e.printStackTrace(); } }
-
-
带有粘性开关的完整代码:
package com.derry.livedatabusandviewbinding.simple1 import android.util.Log import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import java.lang.reflect.Field import java.lang.reflect.Method /** * 单例模式 设置了一个开关,通过函数参数决定是否启用黏性事件(有关闭黏性的开关) KT的版本 */ object OKLiveDataBusKT { /* // 封装总线类:存放订阅者,为了程序扩展性(方便用户指定操作对象,用户希望对于某个MyLiveData操作)所以搞成Map类型: Map类型: key:这条数据的名字 Any:这条数据的类型 //尽量使用懒加载 */ private val bus : MutableMap<String, BusMutableLiveData<Any>> by lazy { HashMap() } /* // 暴露一个函数,给外界注册 订阅者关系 //扩展性:使用泛型,with函数的实参泛型T类型一旦确定,那么其返回值的泛型T类型随机确定并且下面的BusMutableLiveData中的两个泛型T类型也随之确定 //安全性:搞个同步锁,并发环境 //第二个参数:控制变化,直接传相应的字节码文件就行了 //第三个参数:开关,是否启用数据粘性(配合kotlin中的默认参数,默认是开启数据粘性的)*/ @Synchronized fun <T> with(key: String, type: Class<T>, isStick: Boolean = true) : BusMutableLiveData<T> { //判断是否有重复的KEY if (!bus.containsKey(key)) { bus[key] = BusMutableLiveData(isStick)//运算符重载 } return bus[key] as BusMutableLiveData<T>//强制类型转换 } // 私有主构造,不让外界new,那么就要写次构造函数调用主构造进行激活 //前面那个T会影响后面那个T class BusMutableLiveData<T> private constructor() : MutableLiveData<T>() { //粘性开关 var isStick: Boolean = false // 次构造调用主构造进行激活 constructor(isStick: Boolean) : this() { this.isStick = isStick } // 对系统代码进行重载,目的就是实现带开关的粘性清除(可以开,可以关) override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { /*这句话是不能删掉的,hook操作不能破坏原有系统,只是多开了一条路而已; 并且执行下面这句代码的时候,只是起到了一个注册而已; 真正执行代码是由宿主的生命周期变化从而触发的*/ super.observe(owner, observer) // 这句会执行父类的 // 启用系统的功能 不写就破坏了 //使用参数,决定是否启用粘性 if (!isStick) { hook(observer = observer) Log.d("derry", "Kotlin版本的 不启用黏性") } else { Log.d("derry", "Kotlin版本的 启用黏性") } } //使用反射,去掉数据粘性 private fun hook(observer: Observer<in T>) { // TODO 1.得到mLastVersion // 获取到LivData的类中的mObservers对象,后面的.java将其转为java的 val liveDataClass = LiveData::class.java //拿到字段 val mObserversField: Field = liveDataClass.getDeclaredField("mObservers") mObserversField.isAccessible = true // 私有修饰也可以访问 // 获取到这个成员变量的对象 Any == Object val mObserversObject: Any = mObserversField.get(this) // 得到map对象的class对象 private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = val mObserversClass: Class<*> = mObserversObject.javaClass // 获取到mObservers对象的get方法 protected Entry<K, V> get(K k) { val get: Method = mObserversClass.getDeclaredMethod("get", Any::class.java) get.isAccessible = true // 私有修饰也可以访问 // 执行get方法 val invokeEntry: Any = get.invoke(mObserversObject, observer) // 获取entry中的value is "AAA" is String is是判断类型 as是转换类型 var observerWraper: Any? = null if (invokeEntry != null && invokeEntry is Map.Entry<*, *>) { observerWraper = invokeEntry.value } if (observerWraper == null) { throw NullPointerException("observerWraper is null") } // 得到observerWraperr的类对象 val supperClass: Class<*> = observerWraper.javaClass.superclass val mLastVersion: Field = supperClass.getDeclaredField("mLastVersion") mLastVersion.isAccessible = true // TODO 2.得到mVersion val mVersion: Field = liveDataClass.getDeclaredField("mVersion") mVersion.isAccessible = true//私有的也可以访问 // TODO 3.mLastVersion=mVersion val mVersionValue: Any = mVersion.get(this) mLastVersion.set(observerWraper, mVersionValue)//状态对齐 } } }
-
使用:调用with函数,粘性开关就是第三个参数,默认是开启粘性的
-
使用场景:
- 在做登录功能的时候,只想处理本次的数据,不需要处理之前的粘性数据(粘性数据会带来Bug)那么就需要去掉粘性了
-
实现细节:
-
粘性数据:先触发后订阅,还能收到上次的信息(触发之前的),这个数据就是粘性数据了
-
ViewBinding
-
使用场景:
- 实现简单功能,属于轻量级DataBinding;
- 性能很高,ViewBinding背后用的是Gradle技术(Gradle插件直接扫描布局,还是需要编译一下的)不是APT注解处理器(在编译期构建注解扫描)
-
DataBinding的优缺点:
- 因为DataBinding中的双向绑定是比较消耗性能(在MVVM换成MVP,就是DataBinding作为持有者损耗性能)
- 但是DataBinding功能强大,使用APT注解处理器技术全盘扫描布局,直接就干上去了
-
ViewBinding使用:
-
打开就行了,打开之后,整个项目的ViewBinding就打开了
-
不用再写findViewById了
-
同时还会出现一个非常神奇的现象:
- 系统会为MainActivity2创建一个ActivityMain2Binding
- 点击它会直接跳转到activity_main2.xml
-
MainActivity2
package com.derry.view_binding;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.derry.view_binding.databinding.ActivityMain2Binding;
import com.derry.view_binding.databinding.TestBinding;
// ViewBinding背后不是用 APT 注解处理器 使用什么? 答:gradle插件
public class MainActivity2 extends AppCompatActivity {
ActivityMain2Binding main2Binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
main2Binding = ActivityMain2Binding.inflate(getLayoutInflater());
setContentView(main2Binding.getRoot());
/* main2Binding.tv1.setText("AAA");
main2Binding.tv2.setText("BNB");
main2Binding.tv3.setText("DDDD");*/
}
}
activity_main2.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="30sp"
android:textColor="@color/teal_200"
/>
<TextView
android:id="@+id/tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="30sp"
android:textColor="@color/purple_500"
/>
<TextView
android:id="@+id/tv3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="30sp"
android:textColor="@color/black"
/>
</LinearLayout>
-
实现效果:
- 注释掉MainActivity2中的setText 语句
-
未注释掉MainActivity2中的setText语句:
Kotlin-android-extensions插件与ViewBinding技术
-
ViewBinding:
- 始终是面向布局的(始终与布局挂钩),Gradle插件扫描布局文件在项目编译后生成对应的类;
- 面向对象的设计思想,将布局文件看做一个对象;在项目编译后生成对应的类;
- 比Kotlin-android-extensions出现晚
-
Kotlin-android-extensions
-
工作原理:依托于Kotlin的绑定机制
-
这个还是Gradle插件
-
在ViewBinding之前出现并流行
-
使用时有风险:人为失误造成在当前Activity中访问另外一个xml文件中的控件:空指针异常
- 在MainActivity2中使用这个插件访问到了test.xml的控件了,就错了;
- ViewBinding不会出现这个问题:连提示都没有
-
-
代码展示:
-
环境准备:编辑build.gradle
-
导入插件:
plugins { id 'com.android.application' id 'kotlin-android' // 有绑定机制后,就不需要 findv gradle插件 id 'kotlin-android-extensions' // 启用 绑定机制 }
-
启用ViewBinding
// 启用ViewBinding viewBinding { enabled true }
-
-
工程结构展示:
-
activity_main.xml:就只有三个TextView控件
-
text.xml:这个里面放了一个Button
-
-
代码测试:验证插件弊端(在MainActivity中依托插件去访问text.xml中的控件)
-
MainActivity:这里可以看出来,是没有报错的
-
真机测试结果:
-
-
代码测试:验证ViewBinding优点
-
程序截图:可以看到是可以正常访问activity_main.xml中的三个TextView控件的
-
程序截图:使用ViewBinding是无法访问到当前布局文件(activity_main.xml)之外的控件(test.xml中的Button)的
-
-
ViewBinding原理解析
-
概述:使用Gradle插件进行布局文件扫描+动态生成类(这个类里面对findViewById做了封装而已)
-
需要熟悉Gradle原理才能弄懂其工作本质
-
源码验证:
-
打开ViewBinding开关
-
编辑一个布局文件:test.xml
-
打开路径
-
编译一下项目:
-
该路径下,生成了布局文件对应的对象,其中就有我们需要的TestBinding类
-
在生成的TestBinding类中就有布局文件的控件
-
底层还是findViewById,不过是封装的比较友好罢了
-
转载自:https://juejin.cn/post/7042561708266782727