Jetpack LiveData 是时候了解一下了

1,125 阅读13分钟

前言

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<String> name;
    public MutableLiveData<String> 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<Map.Entry<Observer<? super T>, 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,因此都独立处理了数据分发。

image.png

如图所示,最后都会调用到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),将数据变更分发出去。

image.png

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 <T> void postValue(MutableLiveData<T> 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<T> implements Runnable{
        private MutableLiveData<T> liveData;
        private T data;

        public CustomRunnable(MutableLiveData<T> 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<String>() {
            @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<T> extends LiveData<T> {

    @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<T> implements Observer<T>{
        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 即可。
粘性事件/非粘性事件 对比如下:

有粘性事件

粘性事件.gif

无粘性事件

非粘性事件.gif

可以看出,再次进入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 易学易懂系列