前言
Jetpack AAC 系列文章:
Jetpack Lifecycle 该怎么看?还肝否?
Jetpack LiveData 是时候了解一下了
Jetpack ViewModel 抽丝剥茧
上篇分析了Lifecycle,知道了如何优雅地监听生命周期,本篇将着重分析Lifecycle 的具体应用场景之一:LiveData的原理及使用。
通过本篇文章,你将了解到:
1、为什么需要LiveData?
2、LiveData 的使用方式
3、LiveData 的原理
4、LiveData 优劣势及其解决方案
1、为什么需要LiveData?
一个异步回调的例子
某个功能需要从网络获取数据并展示在页面上,想想这个时候该怎么做呢?
很容易想到分三步:
1、请求网络接口获取数据。
2、页面调用接口并传入回调对象。
3、数据通过回调接口通知UI 更新。
典型代码如下:
object NetUtil {
//接口
lateinit var listener : InfoNotify
fun getUserInfo(notify: InfoNotify) {
listener = notify
Thread {
//模拟获取网络数据
Thread.sleep(2000)
//回调通知更新
listener?.notify(100)
}.start()
}
interface InfoNotify {
fun notify(a : Int)
}
}
编写了一个网络工具类,getUserInfo(xx) 传入回调对象,而后在线程里拿到数据后通过回调通知界面更新:
findViewById(R.id.original_callback).setOnClickListener((v)->{
NetUtil.INSTANCE.getUserInfo(new NetUtil.InfoNotify() {
@Override
public void notify(int a) {
runOnUiThread(()->{
Toast.makeText(LiveDataActivity.this, "a=" + a, Toast.LENGTH_SHORT).show();
});
}
});
});
这是获取异步信息并展示的常规做法,但却不够完善,存在三个问题:
第一个问题:
当退回到桌面后,此时网络接口返回数据,那么就会弹出Toast,如果我们想要在App退到后台后不再弹出Toast,那么需要在弹Toast前判断当前App是否在前台可见。
第二个问题:
假若在调用网络的过程中退出LiveDataActivity,当网络数据返回后再Toast,因为Activity 已经不存在了,就会发生Crash。规避的方式如下:
runOnUiThread(()->{
//如果Activity 正在销毁或者已经销毁,那就没必要Toast了
if (!LiveDataActivity.this.isFinishing() && !LiveDataActivity.this.isDestroyed()) {
Toast.makeText(LiveDataActivity.this, "a=" + a, Toast.LENGTH_SHORT).show();
}
});
第三个问题:
我们知道内部类持有外部类引用,而new NetUtil.InfoNotify() 表示构建了一个匿名内部类,这个内部类对象会被NetUtil 持有。Activity 退出时因为被匿名内部类持有,导致其无法释放,造成内存泄漏。规避方式如下:
1)在Activity onDestroy()里移除NetUtil 的InfoNotify监听。
2)在NetUtil 里使用弱引用包裹InfoNotify 对象。
可以看出,为了解决以上三个问题,需要额外多出不少代码,而这些代码又是重复性/代表性比较高,因此我们期望有一种方式来帮我们实现简单的异步/同步 通信问题,我们只需要着眼于数据,而不用管生命周期、内存泄漏等问题。
刚好LiveData 能够满足需求。
2、LiveData 的使用方式
简单同步使用方式
分为三步:
第一步:构造LiveData
public class SimpleLiveData {
//LiveData 接收泛型参数
private MutableLiveData name;
public MutableLiveData getName() {
if (name == null) {
name = new MutableLiveData<>();
}
return name;
}
}
LiveData 是抽象类,MutableLiveData 是其中的一个实现子类,上面的代码其实就是将我们感兴趣的数据包裹在MutableLiveData里,类型为String。
为了方便获取MutableLiveData 实例,再将它封装在SimpleLiveData里。
第二步:监听LiveData数据变化
有了SimpleLiveData,接下来看如何对它进行操作:
private void handleSingleLiveData() {
//构造LiveData
simpleLiveData = new SimpleLiveData();
//获取LiveData实例
simpleLiveData.getName().observe(this, (data)-> {
//监听LiveData,此处的data参数类型即是为setValue(name)时name 的类型-->String
Toast.makeText(LiveDataActivity.this, "singleLiveData name:" + data, Toast.LENGTH_SHORT).show();
});
}
第三步:主动变更LiveData数据
既然有观察者监听,那么势必需要有主动发起通知的地方。
findViewById(R.id.btn_change_name).setOnClickListener((v)->{
int a = (int)(Math.random() * 10);
//获取LiveData实例,更新LiveData
simpleLiveData.getName().setValue("singleName:" + a);
});
很简单,调用LiveData.setValue(xx)即可,LiveData数据发生变更后,就会通知第二步的观察者,观察者刷新UI(Toast)。
简单异步使用方式
你可能已经发现了,上面的数据变更是在主线程发起的,我们实际场景更多的是在子线程发起的,模拟子线程发起数据变更:
findViewById(R.id.btn_change_name).setOnClickListener((v)->{
new Thread(()->{
int a = (int)(Math.random() * 10);
//获取LiveData实例,更新LiveData
simpleLiveData.getName().postValue("singleName:" + a);
}).start();
});
开启线程,在线程里更新LiveData,此时调用的方法是postValue(xx)。
需要注意的是:许多文章在分析LiveData时,习惯性和ViewModel混在一起讲解,造成初学者理解上的困难,实际上两者是不同的东西,都可以单独使用。分别将两者分析后,再结合一起使用就会比较清楚来龙去脉。
3、LiveData 的原理
通过比对传统的回调和LiveData,发现LiveData 使用简洁,没有传统回调的那几个缺点,接下来我们带着问题分析它是如何做到规避那几个缺点的。
添加观察者
#LiveData.java
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer super T> observer) {
//该方法调用者必须在主线程
assertMainThread("observe");
//如果处在DESTROYED 状态,则没必要添加观察者
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
//包装观察者
LiveData.LifecycleBoundObserver wrapper = new LiveData.LifecycleBoundObserver(owner, observer);
//将包装结果添加到Map里
LiveData.ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
...
//监听生命周期
owner.getLifecycle().addObserver(wrapper);
}
重点看看LifecycleBoundObserver:
#LiveData.java
class LifecycleBoundObserver extends LiveData.ObserverWrapper implements LifecycleEventObserver {
@NonNull
final LifecycleOwner mOwner;
LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer super T> observer) {
super(observer);
mOwner = owner;
}
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
...
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
//=====重要1
if (currentState == DESTROYED) {
//移除观察者
removeObserver(mObserver);
//不再分发
return;
}
Lifecycle.State prevState = null;
while (prevState != currentState) {
prevState = currentState;
//通知观察者
activeStateChanged(shouldBeActive());
currentState = mOwner.getLifecycle().getCurrentState();
}
}
...
}
onStateChanged() 是LifecycleEventObserver 接口里定义的方法,而LifecycleEventObserver 继承自LifecycleObserver。
当宿主(Activity/Fragment) 生命周期发生改变时会调用onStateChanged()。
我们注意到注释里的:"重要1"
removeObserver(mObserver)
目的是将之前添加的观察者从Map 里移除。
当宿主(Activity/Fragment) 处在DESTROYED 状态时,移除LiveData的监听,避免内存泄漏。
这就解决了第三个问题:内存泄漏问题。
shouldBeActive()用来判断当前宿主是否是活跃状态,此处定义的活跃状态为:宿主的状态要>="STARTED"状态,而该状态区间为:Activity.onStart() 之后且Activity.onPause()之前。
当宿主处于活跃状态时,才会继续通知UI 数据变更了,进而刷新UI,若是处于非活跃状态,比如App 失去焦点(onPause()被调用),那么将不会刷新UI 。
通知观察者
观察者接收数据的通知有两个来源:
1、宿主的生命周期发生变化。
2、通过调用setValue()/postValue() 触发。
上面分析的是第1种情况,接下来分析第2种场景。
LiveData.setValue() 调用栈
先看方法实现:
#LiveData.java
protected void setValue(T value) {
//必须在主线程调用
assertMainThread("setValue");
//版本增加
mVersion++;
//暂存值
mData = value;
//分发到观察者
dispatchingValue(null);
}
再看dispatchingValue(xx)
#LiveData.java
void dispatchingValue(@Nullable LiveData.ObserverWrapper initiator) {
...
do {
mDispatchInvalidated = false;
if (initiator != null) {
//精准通知
considerNotify(initiator);
initiator = null;
} else {
//遍历调用所有观察者
for (Iterator, LiveData.ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}
通过搜索发现,dispatchingValue(xx) 被两个地方调用,其实就是上面所说的:观察者接收数据的通知有两个来源。
当主动调用setValue(xx)/postValue(xx)时,因为没有指定分发给哪个观察者,因此会遍历通知所有观察者。
而当生命周期发生变化时,因为每个观察者都绑定了Lifecycle,因此都独立处理了数据分发。
如图所示,最后都会调用到considerNotify(xx):
#LiveData.java
private void considerNotify(LiveData.ObserverWrapper observer) {
//非活跃状态,直接返回
if (!observer.mActive) {
return;
}
//此处再额外判断是为了防止observer.mActive 没有及时被赋值(也就是Lifecycle 没有及时通知到)
//因此,这里会主动去拿一次状态,若是非活跃状态,就返回。
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
//如果LiveData数据版本<= 观察者的数据版本,则直接返回
if (observer.mLastVersion >= mVersion) {
return;
}
//更新观察者版本
observer.mLastVersion = mVersion;
//最终通知观察者
observer.mObserver.onChanged((T) mData);
}
可以看出,不论数据通知来源于哪,最后都只会在活跃状态时才会通知观察者。
这就解决了最开始的第一个、第二个问题。
不区分活跃/非活跃
当然啦,是否活跃都是通过调用ObserverWrapper 里的方法来进行判断的,因此若是想要不区分是否活跃都能收到数据变更,则可在添加观察者时,调用如下方法:
simpleLiveData.getName().observeForever(s -> {
Toast.makeText(LiveDataActivity.this, "singleLiveData name:" + s, Toast.LENGTH_SHORT).show();
});
该方法调用时没有传入LifecycleOwner 实例,因此此时的Observer没有和Lifecycle进行关联,当然就没有所谓的活跃与非活跃的划分了。
更直观的是Observer的命名:AlwaysActiveObserver(永远活跃)。
绑定Lifecycle Observer的命名:LifecycleBoundObserver (有限制)。
LiveData.postValue() 调用栈
#LiveData.java
protected void postValue(T value) {
boolean postTask;
//子线程、主线程都需要修改mPendingData,因此需要加锁
synchronized (mDataLock) {
//mPendingData 是否还在排队等待发送出去
//mPendingData == NOT_SET 表示当前没有排队
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
//说明上次的Runnable 还没执行
//直接返回,不需要切换到主线程执行
return;
}
//切换到主线程执行Runnable
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}
这里有个明显的特点:
当调用postValue(xx)比较快时,数据都会更新为最新的存储到mPendingData里,若是上条数据变更没有发送出去,那么将不会再执行新的Runnable了。
因此观察者有可能不会收到全部的数据变更,而是只保证收到最新的更新。
切换到主线程执行Runnable:
#LiveData.java
private final Runnable mPostValueRunnable = new Runnable() {
public void run() {
Object newValue;
synchronized (mDataLock) {
newValue = mPendingData;
//重置状态
mPendingData = NOT_SET;
}
//发送数据变更
setValue((T) newValue);
}
};
postValue(xx)作用:
将数据存储到临时变量里,并切换到主线程执行setValue(xx),将数据变更分发出去。
4、LiveData 优劣势及其解决方案
优势
通过原理部分的分析,你可能已经察觉到了:LiveData 比较简单,上手也比较快。
其优势比较明显:
a、生命周期感知:
借助Lifecycle 能够感知生命周期各个阶段的状态,进而能够对不同的生命周期状态做相应的处理。
正因为可以感知生命周期,所以:
- 可以在活跃状态时再更新UI 。
- UI 保持最新数据(从非活跃到活跃状态总能收到最新数据)。
- 观察者无需手动移除,不会有内存泄漏。
- Activity/Fragment 不存活不会更新UI,避免了Crash。
- 粘性事件设计方式,新的观察者无需再次主动获取最新数据。
还有个额外的特点:稍微改造一下,LiveData 可以当做组件之间消息传递使用。
b、数据实时同步
在主线程调用时:LiveData.setValue(xx)能够直接将数据通知到观察者。
在子线程调用时:LiveData.postValue(xx)将数据暂存,并且切换到主线程调用setValue(xx),将暂存数据发出去。
因此,从数据变更--->发送通知--->观察者接收数据 这几个步骤没有明显地耗时,UI 能够实时监听到数据的变化。
劣势
a、postValue(xx) 数据丢失
postValue(xx)每次调用时将数据存储在mPendingData 变量里,因此后面的数据会覆盖前面的数据。LiveData 确保UI 能够拿到最新的数据,而此过程中的数据变化过程可能会丢失。
问题的原因是:不是每一次数据变更都会post到主线程执行。
因此想要每次都通知,则需要重新包装一下LiveData,如下:
public class LiveDataPostUtil {
private static Handler handler;
public static void postValue(MutableLiveData liveData, T data) {
if (liveData == null || data == null)
return;
if (handler == null) {
handler = new Handler(Looper.getMainLooper());
}
handler.post(new CustomRunnable<>(liveData, data));
}
static class CustomRunnable implements Runnable{
private MutableLiveData liveData;
private T data;
public CustomRunnable(MutableLiveData liveData, T data) {
this.liveData = liveData;
this.data = data;
}
@Override
public void run() {
liveData.setValue(data);
}
}
}
b、粘性事件
相信大家看到过一些博客的分析也知道了LiveData 粘性事件问题。
粘性事件:
数据变更发生后,才注册的观察者,此时观察者还能收到变更通知。
来看看什么场景下会有这种现象。
定义全局持有LiveData 的单例:
public class GlobalLiveData {
private static class Inner {
static GlobalLiveData ins = new GlobalLiveData();
}
public static GlobalLiveData getInstance() {
return Inner.ins;
}
private SimpleLiveData simpleLiveData;
private GlobalLiveData() {
simpleLiveData = new SimpleLiveData();
}
public SimpleLiveData getSimpleLiveData() {
return simpleLiveData;
}
}
在Activity.onCreate()里监听数据变化:
GlobalLiveData.getInstance().getSimpleLiveData().getName().observe(this, new Observer() {
@Override
public void onChanged(String s) {
Toast.makeText(LiveDataActivity.this, "global name:" + s, Toast.LENGTH_SHORT).show();
}
});
然后点击按钮发送数据变更:
findViewById(R.id.btn_change_name).setOnClickListener((v)->{
GlobalLiveData.getInstance().getSimpleLiveData().getName().setValue("from global");
});
数据变更发出去后,观察者收到通知并Toast,此时一切正常。
当Activity 关闭并重新打开时,此时发现还有Toast 弹出。
粘性事件现象发生了。
明明是全新注册的观察者,而且此时没有新的数据变更,却依然收到之前的数据。
这和LiveData 的实现有关,看看核心源码实现:
#LiveData.java
private void considerNotify(LiveData.ObserverWrapper observer) {
//mVersion 为LiveData 当前数据版本,当setValue/postValue 发生时,mVersion++
//通过比对LiveData 当前数据版本与观察者的数据版本,若是发现LiveData 当前数据版本 更大
//说明是之前没有通知过观察者,因此需要通知,反之则不通知。
if (observer.mLastVersion >= mVersion) {
return;
}
//将观察者数据版本保持与LiveData 版本一致,表明该观察者消费了最新的数据。
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}
再回溯一下流程:
1、初始时LiveData.mVersion= -1,ObserverWrapper.mLastVersion = -1,因此初次进入Activity时没有数据通知。
2、当点击按钮后(LiveData.setValue()),此时LiveData.mVersion = 0;因为LiveData.mVersion>ObserverWrapper.mLastVersion,因此观察者能够收到通知。
3、当退出Activity 再进来后,因为ObserverWrapper 是全新new 出来的,ObserverWrapper.mLastVersion = -1,而LiveData.mVersion =0,还是大于ObserverWrapper.mLastVersion,因此依然能够收到通知。
要解决这个问题,很直观的想法是从version字段出发,而LiveData、ObserverWrapper 并没有对外暴露方法来修改version,此时我们想到了反射。
通过反射修改ObserverWrapper.mLastVersion 的值,使得在第一次注册时候保持与LiveData.mVersion 值一致。
这也是很多博客的主流解决方法,因为要反射Map,进而反射里面的Observer拿出version,步骤有点多,这里提供一种方案,只需要拿到LiveData.mVersion即可,刚好LiveData提供了方法:
int getVersion() {
return mVersion;
}
因此我们只需要调用这个反射方法即可:
public class EasyLiveData extends LiveData {
@Override
public void observe(@NonNull @NotNull LifecycleOwner owner, @NonNull @NotNull Observer super T> observer) {
super.observe(owner, new EasyObserver<>(observer));
}
@Override
public void observeForever(@NonNull @NotNull Observer super T> observer) {
super.observeForever(new EasyObserver<>(observer));
}
@Override
protected void setValue(T value) {
super.setValue(value);
}
@Override
protected void postValue(T value) {
super.postValue(value);
}
class EasyObserver implements Observer{
private Observer observer;
private boolean shouldConsumeFirstNotify;
public EasyObserver(Observer observer) {
this.observer = observer;
shouldConsumeFirstNotify = isNewLiveData(EasyLiveData.this);
}
@Override
public void onChanged(T t) {
//第一次进来,没有发生过数据变更,则后续的变更直接通知。
if (shouldConsumeFirstNotify) {
observer.onChanged(t);
} else {
//若是LiveData 之前就有数据变更,那么这一次的变更不处理
shouldConsumeFirstNotify = true;
}
}
private boolean isNewLiveData(LiveData liveData) {
Class ldClass = LiveData.class;
try {
Method method = ldClass.getDeclaredMethod("getVersion");
method.setAccessible(true);
//获取版本
int version = (int)method.invoke(liveData);
//版本为-1,说明是初始状态,LiveData 还未发生过数据变更。
return version == -1;
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
}
如若不想要粘性事件,则使用上述的EasyLiveData 即可。
粘性事件/非粘性事件 对比如下:
可以看出,再次进入Activity时,并没有弹出Toast。
优劣势辩证看
LiveData 优势很明显,当然劣势也比较突出,虽然说是劣势,换个角度看就是仁者见仁智者见智:
个人猜测LiveData 设计的侧重点就不是在消息通知上,而是为了让UI 能够感知到最新数据,并且无需再次请求数据。
当然,为了使得LiveData 更加契合我们的应用场景,可以按上述方法进行适当改造。
如果你是用Java 开发,那么LiveData 是把利刃,如果你用Kotlin,可以考虑用Flow。
下篇将分析ViewModel,彻底厘清为啥ViewModel能够存储数据以及运用场合。
本文基于:implementation 'androidx.appcompat:appcompat:1.4.1'
LiveData 演示&工具
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android
1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易懂易学系列