Jetpack LiveData 是时候了解一下了

作者: 小鱼人爱编程 | 来源:发表于2022-04-02 10:51 被阅读0次

    前言

    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 易懂易学系列

    相关文章

      网友评论

        本文标题:Jetpack LiveData 是时候了解一下了

        本文链接:https://www.haomeiwen.com/subject/ketwrrtx.html