Android:LiveData postValue导致数据丢失问题,及其原因

关于这个问题,网上很多,有一篇文章还详细列举了几种情况,写的非常直观:https://www.jianshu.com/p/aa24dd9123a1

我写的此文章比较多的个人想法,需要自己思考一下。

我碰到的实际情况是:

使用阿里RTC实时音视频服务,我把音视频操作和回调都写在了ViewModel中,在同一房间内,已经有人的情况下,在自己加入房间时,会触发阿里SDK事件通知回调onRemoteUserOnLineNotify,告诉我当前房间存在的人,因为回调都是在非主线程里,然后我通过LiveData.postValue通知到UI有人加入,我在recyclerView的adapter将数据add进去,有几个人回调几次此方法。在不止一人的情况下,就会几乎同时的多次调用LiveData.postValue,从而导致我只观察到了最后一个postValue。

我不想先讲这个问题的解决办法,我想先谈谈:为什么会出现这个问题?

据我猜测,碰到这个问题的大多数使用情况应该和我上面的差不多,都是获取数据,并且将数据添加到列表中。不知道猜的对不对?

那么出现这个问题的原因,是你对于LiveData的认知,是LiveData的概念问题。在你而言,LiveData是用于事件通知呢,还是一个activity的数据持有类。LiveData的正确使用方式是:

作为可以被观察的数据持有类

在MVP架构中,假如增加了功能,那么首先接口层需要增加一个方法定义,View层需要实现其方法,Presenter层调用此方法,把数据回调到UI界面上,其中需要判断activity是否被销毁。这样做能明显看出有2点不足,一是方法定义变多,改动的地方增多,而且实现接口从代码来看,不够直观、二是需要手动控制Presenter的生命周期。

那么在MVVM中,LiveData很好的解决了这个问题,我们不需要写一个接口文件,把方法提前定义好;也不需要自己判断数据更新时UI是否存在。只需要将需要的数据类型包裹在MutableLiveData中,生成它,在activity中观察:

viewModel.liveData.observe(this, new Observer<String>() {
    @Override
    public void onChanged(xxx s) {
        在这边实现数据的使用
    }
}); 

这个时候,对于我来说,概念上的偏差就来了,我是将它作为MVP中V和P之间交互的替代品,那么它就是作为一个数据通知功能。把它当成一种事件传递,数据通知的工具会出现什么问题呢?

接下来看一个简单的例子,来看看它作为界面数据持有的功能,功能很简单:界面上一个TextView,两个Button,一个按钮旋转屏幕,让activity重建,一个按钮生成String数据,并将数据设置到TextView上:

上代码,代码中使用了封装的框架,这个框架源自github上的一个项目,我拿来修改重新封装更适合自己使用,功能上大致能猜个八九不离十,之前想写这个框架博客的,但是太忙了。

首先界面 layout:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.zh.mvvmui.viewmodel.TestViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".mvvmui.activity.TestActivity">

        <TextView
            android:id="@+id/tv_test"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="15sp"
            android:textColor="@color/black"
            android:layout_marginTop="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/bt_change"/>

        <Button
            android:id="@+id/bt_change"
            android:layout_width="120dp"
            android:layout_height="40dp"
            android:text="旋转屏幕"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent" />

        <Button
            android:id="@+id/bt_set_text"
            android:layout_width="120dp"
            android:layout_height="40dp"
            android:text="设置文字"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/bt_change"
            app:layout_constraintBottom_toBottomOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

当然,LiveData也支持android DataBinding,写成android:text="@{viewModel.testLiveData}",只是为了更直观的观察它调用情况,不使用它。

ViewModel类:

public class TestViewModel extends BaseViewModel {

    public MutableLiveData<String> testLiveData = new MutableLiveData<>();

    public TestViewModel(@NonNull Application application) {
        super(application);
    }

    public void setTestString(String str) {
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                testLiveData.postValue(str);
            }
        }, 1000);
    }

    @Override
    public void onCreate() {
        Log.d("TEST--activity", "onCreate");
    }

    @Override
    public void onResume() {
        Log.d("TEST--activity", "onResume");
    }

    @Override
    public void onPause() {
        Log.d("activity", "onPause");
    }

    @Override
    public void onDestroy() {
        Log.d("TEST--activity", "onDestroy");
    }
}

因为我实现了LifecycleObserver接口方法,所以可以直接重写onCreate这些方法。然后setTestString模拟网络延时数据。

Acitivity类:

public class TestActivity extends BaseActivity<ActivityTestBinding, TestViewModel> {

    @Override
    protected void initData() {

    }

    @Override
    protected void initViewObservable() {
        viewModel.testLiveData.observe(this, new Observer<String>() {
            @Override
            public void onChanged(String s) {
                Log.d("TEST--liveData", "---观察到了数据改变---");
                binding.tvTest.setText(s);
            }
        });
        binding.btSetText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //binding.tvTest.setText("直接设置文字");
                viewModel.setTestString("设置liveData数据,观察该LiveData,在其改变时,更新UI");
            }
        });
        binding.btChange.setOnClickListener(new View.OnClickListener() {
            @SuppressLint("SourceLockedOrientationActivity")
            @Override
            public void onClick(View v) {

                if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
                    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                } else {
                    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                }
            }
        });
    }

    @Override
    public TestViewModel initViewModel() {
        return new ViewModelProvider(this,
                new ViewModelProvider.AndroidViewModelFactory(getApplication()))
                .get(TestViewModel.class);
    }

    @Override
    public int initContentView() {
        return R.layout.activity_test;
    }

    @Override
    public int initVariableId() {
        return BR.viewModel;
    }
}

这个比较简单,我连Model都没放,单看initViewObservable方法,初始化了按钮的点击方法,观察了ViewModel的testLiveData,应该都比较简单。接下来看log,我的操作是,打开Activity,点击设置文字,再点击旋转屏幕

D/TEST--activity: onCreate  ①
D/TEST--activity: onResume  ②
D/TEST--liveData: ---观察到了数据改变---  ③
D/TEST--activity: onDestroy  ④
D/TEST--activity: onCreate   ⑤
D/TEST--liveData: ---观察到了数据改变---  ⑥
D/TEST--activity: onResume   ⑦


我标了个小圆圈数字,比较好说明一下,
1和2是在activity启动触发的;
3是点击了设置文字按钮后,触发了LiveData观察到的;
4和5是屏幕旋转activity被重建;
6是在重建时自动触发了LiveData观察
7就不说明了


附图:

            

可以看出来,LiveData在activity重建时,会把数据重新赋予一次,这就是它本质的功能可以被观察的数据持有类,它持有着界面上的数据,那么在界面重建时,会把数据恢复。

那么再看,假如不用LiveData呢,现在把那个设置文字按钮点击事件更换一下:

binding.btSetText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                binding.tvTest.setText("直接设置文字");
                //viewModel.setTestString("设置liveData数据,观察该LiveData,在其改变时,更新UI");
            }
        });

    

可以看到,旋转屏幕时,数据丢失了。

因此,LiveData并不是简单的用于事件通知和数据回调。假设像上面RTC例子中我没碰到数据丢失的情况,他们进房间都是一个一个进的,就不会有问题,但是RTC实时音视频界面被重建了,这个时候,LiveData恢复的数据,肯定只有最后进房间的那个人的数据。同样的道理,假如把LiveData当做比如RecyclerView加载更多的数据回调,在界面重建时,恢复的也是部分数据。

这个合理吗?我觉得是合理的,LiveData所持有的数据,就是界面上要展示的数据,最后一次postValue就是你界面上应该展示的数据,所以中间的数据都没发送出去。看看它的源码:

protected void postValue(T value) {
        boolean postTask;
        synchronized (mDataLock) {
            postTask = mPendingData == NOT_SET;
            mPendingData = value;
        }
        if (!postTask) {
            return;
        }
        ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }

在这个runnable未被执行前,多次调用postValue,mPendingData就会被多次赋值,所以只有最后一次数据被发送出去了。

解决办法:

1、使用setValue,setValue不会造成数据丢失,它每次都会调用,但是这样会有界面重建时数据丢失的隐患。(去除这个隐患的话,就getValue 然后获取到的数据,add,再setValue)

2、保证数据不会一起进来,大部分数据应该都不会同时进来的,所以碰到这种问题的比较小众。

(不能使用getValue获取列表项再add数据,然后postValue的方法,getValue获取的数据是setValue后的mData,在还没被调用到setValue时,你getValue出来的数据,都是在postValue之前的数据)

猜你喜欢

转载自blog.csdn.net/qq_27454233/article/details/107907603
今日推荐