LiveData原理面试一问还不懂?

最近公司凉了,准备面试一家公司的车控岗位,想着对车控信号的封装应该会用到LiveData吧。学一下他的原理吧,出了很久的技术了。LiveData源码并不复杂,看过源码懂得都懂,网上也有一定的介绍文章了。本文主要看缘分给安卓小白用于扫盲。

先抛出问题,如果以下内容全清楚那么可以右上角点×了。

  1. LiveData更新数据的方法
  2. LiveData如何感知生命周期
  3. LiveData是否具有粘性,以及原因
  4. 是否遇到过LiveData丢失数据的情况
  5. Fragment使用LiveData需要 注意生什么
  6. 想要一直监听LiveData,无论是否活跃怎么办
  7. LiveData怎么转换数据类型

LiveData是啥有啥用,为啥要使用LiveData?

这个如果不知道建议看一下来至于快手大佬的这篇文章 juejin.cn/post/684490...

1. LiveData更新数据的方法

首先我们需要创建一个LiveData对象,然后调用他的observe方法,传入一个生命周期的owner对象和一个我们自定义的观察者(他实现了onChanged接口)如下。

看一下LiveData源码中的observe方法

less 复制代码
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {

    //检测非主线程报错。
    assertMainThread("observe");
    
    //检测owner生命周期
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        // ignore
        return;
    }
    
    //将我们传入的生命周期owner对象和我们自定义的观察者对象observer 包装一下 。
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    
    //将包装后的对象,以我们自定义的观察者作为key,存入一个集合mObservers(具体是啥后面介绍)
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    
    //报错检测机制先不关心
    if (existing != null && !existing.isAttachedTo(owner)) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    
    //包装后的对象给到LifecycleRegistry,记住他,这里在介绍第二个问题的时候细讲
    owner.getLifecycle().addObserver(wrapper);
}

源码中LiveData有一个mVersion 属性和mObservers集合。

csharp 复制代码
public abstract class LiveData<T> {

//每一个LiveData对象有一个mVersion属性,记住这个属性很关键。
private int mVersion;

//一个支持迭代时更新数据的Iterable
private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =
        new SafeIterableMap<>();
}

小结,看到这里你需要知道。

每一个LiveData对象中都有一个mObservers集合。mObservers中可以存储该LiveData对象的所有观察者。即一个LiveData对象可以有多个观察者。

LiveData类中更新数据的方法1。setValue简单易懂。

typescript 复制代码
@MainThread
protected void setValue(T value) {
    //检查是否在主线程使用,非主线程报错
    assertMainThread("setValue");
    
    //每一次更新数据将版本属性++
    mVersion++;
    
    //更新数据
    mData = value;
    
    //分发数据
    dispatchingValue(null);
}

LiveData类中更新数据的方法2。postValue 一般在子线程调用,他与上面setValue 的区别,没有mVersion++

ini 复制代码
protected void postValue(T value) {
    boolean postTask;
    
    //锁保护线程安全,即支持主线程调用
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    
    //切换到主线程run一个runnnable
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

震惊为啥posetValue 不用更新mVersion 呢?看一下他的mPostValueRunnable你会发现

less 复制代码
private final Runnable mPostValueRunnable = new Runnable() {
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Object newValue;
        synchronized (mDataLock) {
            newValue = mPendingData;
            mPendingData = NOT_SET;
        }
        
        //原来postValue 在切换线程以后又调用了一次 setValue,
        //那么mVersion,自然也是通过setValue更新的
        setValue((T) newValue);
    }
};

再看一下setValue 中分发的方法dispatchingValue ,内部遍历调用了considerNotify ,下面的mObservers前文介绍过是一个支持边迭代,边读写Iterable

less 复制代码
void dispatchingValue(@Nullable ObserverWrapper initiator) {
    、、省略其他代码
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
        
        //遍历调用 considerNotify
    considerNotify(iterator.next().getValue());
    if (mDispatchInvalidated) {
        break;
    }
}
 、、省略其他代码
}

considerNotify 中对observer 的状态进行过滤,以及版本进行判断,全部通过后,使用mObserverdeonChanged进行数据回调。

kotlin 复制代码
private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    //**********记住这里后面会提到***********
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}

扩展 :不知道你看完会不会有这么一个疑问。mVersion 版本信息用int 很合理吧。比long 和 double 都节省内存,很常规的用法。但是这个setValuemVersion 一直++,极端的情况会不会有超过int最大值 的情况,那再++ 超过了最大值会不会报错了?看代码里好像没有对超过int最大值的判断,难道是这个点默认不在框架设计的范围内吗?

百度一下你会得到这样的答案

在Java中,int 类型的变量是有固定范围的,其取值范围是从 -2,147,483,648 到 2,147,483,647。当你尝试将一个 int 类型的变量增加到超出这个范围时,会发生整数溢出(integer overflow),而不是抛出异常。整数溢出会导致变量的值回绕到 int 类型的最小值。

例如,如果你有一个 int 类型的变量,其值为 Integer.MAX_VALUE(即2,147,483,647),然后你执行 int++,这个变量的值会变为 Integer.MIN_VALUE(即-2,147,483,648),而不是抛出一个错误。

这里有一个简单的例子:不信的可以去试一下,反正我信。

java 复制代码
int value = Integer.MAX_VALUE;
value++; // 这会导致整数溢出,value 的值现在变成了 Integer.MIN_VALUE
System.out.println(value); // 输出 -2147483648

基础薄弱的作者此时的内心独白:卧焯妙啊。

(不过捏,虽然不会报错,但是mVersion如果是int最大值的话,再++,会导致mLastVersion > mVersion,从而触发一次return)

2. LiveData如何感知生命周期

LiveData 本身是不具备感知生命周期能力的。liveData 之所以能够感知生命周期,完全归功于lifecycle 。 在官方对于lifecycle的介绍中提到了一个类LifecycleOwner ,说你可以自己建一个类,实现了LifecycleOwner接口即可。 developer.android.google.cn/reference/k...

一般LiveData我们是在Activity里进行观察。liveData.observe(),的第一个参数我们会传入this

在Activity的super,super,super...中你会发现ComponentActivity ,并且他实现了LifecycleOwner 我们可以参考它实现一个自己的LifecycleOwner

那为啥要自己实现一个呢?,面试别人问我,我就说我只要实现lifecyce提供的LifecycleOwner接口就可以。

那这时候如果面试官继续问你,那具体该实现啥呢,有啥常规的功能或者技巧。我们是不是就懵逼了。为了让自己的回答更有底气,我们看一下如何自定义一个LifecycleOwner

然后在Activity里把liveData.observe的第一个参数,传入我们自定义的TestLifecycleOwner 运行一下 ,看一下日志。那么不出意外的话,意外就发生了。日志中只有funCreate()funOnCreateCallback() 中的日志输出。而没有Livedata的数据变化日志。 沃德发 ,为啥呢。给data.value 加上断点(如果是java那也就是,data.setValue("111"))。Debug 一下你会发现卡在了这里被return了。 这个shouldBeActive 会检测生命周期的状态,至少为STARTED,否则会return不继续执行

scss 复制代码
@Override
boolean shouldBeActive() {
    return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}

那么我们给TestLifecycleOwner 加两方法。更新它的生命周期到OnStarted 再改一下我们的Activity代码。让他3s后更新TestLifecycleOwner生命周期到OnStart

ini 复制代码
Handler().postDelayed({
    owner.funStarted()
    data.value = "222"
},3000)

再看日志,一切正常3s后生命周期变为OnStart,liveData的数据变化也可以观察到 疑问 :在OnCreate状态下 ,set的数据 111,由于生命周期的关系导致没收到。后面生命周期更新为OnStart后,收到了之前的数据111。

*这个是不是表示liveData 数据具有粘性啊? 是的,那么我们继续下个议题。

3. LiveData是否具有粘性,以及原因

还是上面的栗子,debug一下funStarted()我们会发现。(就是java中的setCurrentState)

kotlin 复制代码
fun funStarted(){
    mRegistry.currentState = Lifecycle.State.STARTED
    Log.i(TAG, "主动触发 funStarted: 生命周期更新为 ${mRegistry.currentState}")
}

会先到LifecycleRegistry.setCurrentState更新生命周期状态

接着会到LifecycleRegistry中moveToState方法的Sync() 接着到Sync的forwardPass

forwardPass中会进行observerEvent的分发

dispatchEvent 中进行mLifecycleObserver.onStateChanged回调

上面会回调到。LiveData中LifecycleBoundObserveronStateChanged 更新生命周期。满足条件后,继续分发(注意下面的this是LiveData) 后面就到了我们熟悉的流程。

检测生命周期,检查LiveData的mVersion和观察者的version是否相等 。触发观察者回调(这里就是我们在Activity中实现的onChanged方法) 结束

kotlin 复制代码
val owner = TestLifecycleOwner()
data.observe(owner,object : Observer<String> {
    override fun onChanged(t: String?) {
        Log.i(TAG, "onChanged: $t")
    }
}

4. 是否遇到过LiveData丢失数据的情况

如果有在子线程 或者多线性 频繁调用liveData.postValue 的话。那应该会遇到过这种问题。 这个其实人家官方是不建议你这样去使用liveData的。

非要用liveData处理,要么单线程每次更新加延时,要么频繁切换线程到主线程setValue去更新,也是可以的代价就是浪费性能。

业务上如果不是必须展示每次数据的变化,那我们就不处理。如果业务上一定要处理那就改用其他的方式,不使用liveData,比如Rxjava或者Flow。

如果使用Flow可以参考来自于字节大佬的这篇文章。 juejin.cn/post/700107...

5. Fragment使用LiveData需要 注意生什么

首先这个问题为什么是Fragment 而不是Activity

前文我们了解到 liveData通过observe设置生命周期的Owner。 在LiveData 类中的LifecycleBoundObserver 有这样一段代码,在LiveData 绑定的LifecycleOwner 的生命周期变化为OnDestory 时会移除对应的观察者。所以将Activity 作为LifecycleOwner 时可以自动 的处理LiveData解注册问题

less 复制代码
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
        @NonNull Lifecycle.Event event) {
    Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
    //注意看这里*******onDestroy的时候移除了Observer*************
    if (currentState == DESTROYED) {
        removeObserver(mObserver);
        return;
    }
    Lifecycle.State prevState = null;
    while (prevState != currentState) {
        prevState = currentState;
        activeStateChanged(shouldBeActive());
        currentState = mOwner.getLifecycle().getCurrentState();
    }
}

那么Fragment 有什么不一样。Fragment 的加载方式有addreplace两种。

  1. 使用add 添加新的Fragment ,那么原本旧的Fragment 不会被移除栈,会继续保留在栈内,旧的Fragment 也就不会触发OnDestroy,LiveData也就不会去进行解注册操作。
  2. 而使用replace 添加新的Fragment ,会使旧的Fragment 进行出栈,导致旧的Fragment 触发OnDestroy 如果将Fragment 作为LiveDataLifecycleOwner

所以是说我们使用Fragment作为LiveData的lifecycleOwner的时候要,使用replace不使用add吗

不是的,这样做太局限了,某些业务的场景下,可能存在就是要使用add的情况。 那这个时候我们可以用viewLifecycleOwner ,其实就是FragmentViewLifecycleOwner ,它在package androidx.fragment.app中。官方推荐使用viewLifecycleOwner,在Fragment视图不显示的时候,会触发OnDestroy帮助我们进行LiveData的解注册。

6. 想要一直监听LiveData,无论是否活跃怎么办

这个问题就有点偏了,看业务经验。常规业务可能不太容易遇到。

可以使用LiveData的observeForever 函数 这里查看一下源码,你会发现observeForever ,是没有传lifecycleOwner的只有观察者一个参数。它连Owner都没有,就也没有相应的Owner生命周期状态了。它会默认一直是活跃的。所以在我们使用的时候,也需要自己进行解注册的处理。

observeForever 中会把每次传入的observer 包装成一个新的wrapper ,再调用每个新的warpper 的activeStateChanged(true)。这里的AlwaysActiveObserverObserverWrapper 的子类,并且本身没有activeStateChanged 方法,所以其实是调用父类的方法。

也就是说,一个LiveData 支持多个observeForever 。即使这样写,传入的是相同的观察者,它也会给你每次调用时包装成一个新的wrapper

7. LiveData怎么转换数据类型

这个感觉是锦上添花的东西。比如你使用一个接口的回调结果展示获取用户的详细信息。

ini 复制代码
val data = MutableLiveData<UserInfo>()

后面,还有个其他的业务,需要监听用户等级的变化,展示其他的业务。使用Transformations可以实现这一操作。

kotlin 复制代码
val userLv: LiveData<Int> = Transformations.map(data) { it.userLv }

除了map,还有一个switchMap,switchMap每次会返回一个新的LiveData对象。

kotlin 复制代码
val userLv: LiveData<Int> = Transformations.switchMap(data) {
    MutableLiveData(it.userLv)
}

普通的类型转换,我们使用map就可以了。那什么场景下会使用到switchMap呢。 假设我们的ViewModel中暴露一个itemsLiveData用于给用户观察。 我们通过用户的意图(提供一个带有参数的getItemsLiveData方法),用户可以选择获取数据库中旧的数据,或者网络请求新的是数据。 那么我们可以通过以下代码实现。

kotlin 复制代码
// 这是本地数据源
private val localDataSource = MutableLiveData<List<Item>>()
// 这是网络数据源
private val networkDataSource = MutableLiveData<List<Item>>()
// 暴露给用户的liveData
val itemsLiveData = MutableLiveData<List<Item>>()

// 用于获取最终数据流的方法
fun getItems(dataSourceType: DataSourceType): LiveData<List<Item>> {
    // 使用switchMap根据dataSourceType的值切换数据源
    Transformations.switchMap(dataSourceType) { type ->
        when (type) {
            DataSourceType.LOCAL -> {
                // 如果选择本地数据源,返回本地数据的LiveData
                localDataSource
            }

            DataSourceType.NETWORK -> {
                // 如果选择网络数据源,返回网络数据的LiveData
                networkDataSource
            }
        }
    }
}

我们知道SwitchMap每次都会创建一个新的LiveData,很明显他不适合频繁调用的情况,必定浪费性能,那怎么优化一下呢。我们可以使用MediatorLiveData

kotlin 复制代码
// 这是本地数据源
private val localDataSource = MutableLiveData<List<Item>>()
// 这是网络数据源
private val networkDataSource = MutableLiveData<List<Item>>()
// 暴露给用户的liveData
val mediatorLiveData = MediatorLiveData<List<Item>>()

// 用于获取最终数据流的方法
fun getItems(dataSourceType: DataSourceType): LiveData<List<Item>> {
    // 使用switchMap根据dataSourceType的值切换数据源
        when (dataSourceType) {
            DataSourceType.LOCAL -> {
                // 如果选择本地数据源,返回本地数据的LiveData
                localDataSource.value = loadLocalData()
                mediatorLiveData.addSource(localDataSource) {
                    mediatorLiveData.value = localDataSource.value
                }
                return localDataSource
            }
            else ->{
                // 如果选择网络数据源,返回网络数据的LiveData
                networkDataSource.value = getNetWorkData()
                mediatorLiveData.addSource(localDataSource) {
                    mediatorLiveData.value = localDataSource.value
                }
                return networkDataSource
            }
        }
    }

疑问 :那你说,我不用这个MediatorLiveData ,我直接定义一个LiveData ,数据来了,无论是网络,还是本地,还是别人给我传的,我酷酷一顿setValue做不了吗?

这样搞他兴许也能做,只不过有点违背常理。人家LiveData本身就是给你用来观察一个数据的变化用的。网络数据,本地数据,明显两个数据源。非要把他定义成一个数据去观察,代码也能写就是不优雅,不符合常理。

文章如有错误,请康概指出 thanks

参考

juejin.cn/post/708503... juejin.cn/post/697568... zhuanlan.zhihu.com/p/504082214 blog.csdn.net/shenshizhon...

相关推荐
mingzhi614 小时前
网安面试会问到的:http的长连接和短连接
http·面试·职场和发展
极客先躯4 小时前
高级java每日一道面试题-2024年9月16日-框架篇-Spring MVC和Struts的区别是什么?
java·spring·面试·mvc·struts2·框架篇·高级java
GISer_Jing8 小时前
前端面试CSS常见题目
前端·css·面试
八了个戒9 小时前
【TypeScript入坑】什么是TypeScript?
开发语言·前端·javascript·面试·typescript
xilu012 小时前
英语: "简历" 的两种表达,CV 和 Resume 探讨
面试
mingzhi6114 小时前
应届生必看 | 毕业第一份工作干销售好不好?
网络·web安全·面试
八了个戒15 小时前
【TypeScript入坑】TypeScript 的复杂类型「Interface 接口、class类、Enum枚举、Generics泛型、类型断言」
开发语言·前端·javascript·面试·typescript
Pandaconda17 小时前
【计算机网络 - 基础问题】每日 3 题(十)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
测试老哥19 小时前
功能测试干了三年,快要废了。。。
自动化测试·软件测试·python·功能测试·面试·职场和发展·压力测试