Android面试:说一下 LiveData 的 postValue ?与SetValue有什么区别?连续调用会有什么问题?为什么?

本文深入探讨了LiveData中的postValue与setValue的区别,特别是在多线程环境下的行为差异。文章详细解析了postValue可能存在的问题及其背后的设计考量,并提出使用RxJava作为替代方案以确保数据更新的完整性和顺序性。

众所周知,程序员面试的时候,很多面试官喜欢会就一个问题不断深入追问。

例如一个小小的 LiveData 的 postValue,就可能会问出一连串问题:

postValue 与 setValue

postValuesetValue 一样都是用来更新 LiveData 数据的方法:

  • setValue 只能在主线程调用,同步更新数据
  • postValue 可在后台线程调用,其内部会切换到主线程调用 setValue
liveData.postValue("a");
liveData.setValue("b");

上面代码,a 在 b 之后才被更新。

postValue 收不到通知

postValue 使用不当,可能发生接收到数据变更的通知:

If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.

如上,源码的注释中明确记载了,当连续调用 postValue 时,有可能只会收到最后一次数据更新通知。

梳理源码可以了解其中原由:

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

mPendingData 被成功赋值 value 后,post 了一个 Runnable

mPostValueRunnable 的实现如下:

private final Runnable mPostValueRunnable = new Runnable() {
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Object newValue;
        synchronized (mDataLock) {
            newValue = mPendingData;
            mPendingData = NOT_SET;
        }
        setValue((T) newValue);
    }
};
  • postValue 将数据存入 mPendingDatamPostValueRunnable 在UI线程消费mPendingData

  • 在 Runnable 中 mPendingData 值还没有被消费之前,即使连续 postValue , 也不会 post 新的 Runnable

  • mPendingData 的生产 (赋值) 和消费(赋 NOT_SET) 需要加锁

这也就是当连续 postValue 时只会收到最后一次通知的原因。

源码梳理过了,但是为什么要这样设计呢?

为什么 Runnable 只 post 一次?

mPenddingData 中有数据不断更新时,为什么 Runnable 不是每次都 post,而是等待到最后只 post 一次?

一种理解是为了兼顾性能,UI只需显示最终状态即可,省略中间态造成的频发刷新。这或许是设计目的之一,但是一个更为合理的解释是:即使 post 多次也没有意义,所以只 post 一次即可

我们知道,对于 setValue 来说,连续调用多次,数据会依次更新:

如下,订阅方一次收到 a b 的通知

liveData.setValue("a");
liveData.setValue("b");

通过源码可知,dispatchingValue() 中同步调用 Observer#onChanged(),依次通知订阅方:

//setValue源码

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

但对于 postValue,如果当 value 变化时,我们立即post,而不进行阻塞

protected void postValue(T value) {
    mPendingData = value;
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
    public void run() {
        setValue((T) mPendingData);
    }
};
liveData.postValue("a")
liveData.postValue("b")

由于线程切换的开销,连续调用 postValue,收到通知只能是b、b,无法收到a。

因此,post 多次已无意义,一次即可。

为什么要加读写锁?

前面已经知道,是否 post 取决于对 mPendingData 的判断(是否为 NOT_SET)。因为要在多线程环境中访问 mPendingData ,不加读写锁无法保证其线程安全。

protected void postValue(T value) {
    boolean postTask = mPendingData == NOT_SET; // --1
    mPendingData = value; // --2
    if (!postTask) {
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
    public void run() {
        Object newValue = mPendingData;
        mPendingData = NOT_SET; // --3
        setValue((T) newValue);
    }
};

如上,如果在 1 和 2 之间,执行了 3,则 2 中设置的值将无法得到更新

使用RxJava替换LiveData

如何避免在多线程环境下不漏掉任何一个通知? 比较好的思路是借助 RxJava 这样的流式框架,任何数据更新都以数据流的形式发射出来,这样就不会丢失了。

fun <T> Observable<T>.toLiveData(): LiveData<T> = RxLiveData(this)

class RxLiveData<T>(
    private val observable: Observable<T>
) : LiveData<T>() {
    private var disposable: Disposable? = null

    override fun onActive() {
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                setValue(it)
            }, {
                setValue(null)
            })
    }

    override fun onInactive() {
        disposable?.dispose()
    }
}

最后

想要保证事件在线程切换过程中的顺序性和完整性,需要使用RxJava这样的流式框架。

有时候面试官会使用追问的形式来挖掘候选人的技术深度,所以大家在准备面试时要多问自己几个问什么,知其然并知其所以然。

当然,我也不赞同这种刨根问底式的拷问方式,尤其是揪着一些没有实用价值的细枝末节不放。所以本文也是提醒广大面试官,挖掘深度的同时要注意分寸,不能以将候选人难倒为目标来问问题。

好了,今天的文章就到这里,感谢阅读,喜欢的话不要忘了三连。大家的支持和认可,是我分享的最大动力。

Android高级开发系统进阶笔记、最新面试复习笔记PDF,我的GitHub

原文首发:https://juejin.cn/post/6971608728042733605

`MutableLiveData.setValue()` 是 Android Jetpack `LiveData` 的核心方法之一,**用于在主线程直接更新数据并触发观察者的回调**。它的核心作用是提供一种**同步、线程安全**的方式更新数据,确保观察者能立即收到最新值,同时避免因线程问题导致的崩溃。 --- ## **1. `setValue()` 的核心作用** ### **(1)主线程同步更新** - **必须在主线程调用**:`setValue()` 要求当前线程是主线程(`UI Thread`),否则会抛出 `IllegalStateException`。 - **立即触发观察者**:调用后,所有活跃的观察者会立即收到 `onChanged()` 回调,并更新 UI。 ### **(2)线程安全** - 直接修改普通变量不同,`setValue()` 内部通过 `LiveData` 的机制保证数据更新的原子性,避免多线程竞争导致的脏数据。 ### **(3)粘性事件(Sticky Event)** - 新注册的观察者会立即收到 `setValue()` 设置的最新值(即使该值是在观察者注册之前设置的)。 --- ## **2. `setValue()` 的典型使用场景** ### **场景 1:主线程直接更新数据** ```kotlin class MyViewModel : ViewModel() { val counter = MutableLiveData<Int>() fun incrementCounter() { val currentValue = counter.value ?: 0 counter.setValue(currentValue + 1) // 主线程直接更新 } } // 在 Activity/Fragment 中观察 viewModel.counter.observe(this) { newCount -> binding.tvCount.text = "Count: $newCount" // 立即更新 UI } // 在按钮点击事件中调用(主线程) binding.btnIncrement.setOnClickListener { viewModel.incrementCounter() } ``` ### **场景 2: `DataBinding` 结合使用** ```kotlin class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val viewModel = MyViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = viewModel binding.lifecycleOwner = this // 确保 DataBinding 能观察到 LiveData 变化 // 主线程更新数据 viewModel.userName.setValue("Alice") // 触发 UI 更新 } } ``` --- ## **3. `setValue()` `postValue()` 的对比** | **方法** | **调用线程** | **执行时机** | **适用场景** | **是否触发粘性事件** | |-------------------|-------------------|---------------|---------------------------|---------------------| | `setValue(value)` | 仅主线程 | 同步 | 主线程直接更新数据 | 是 | | `postValue(value)`| 子线程或主线程 | 异步(主线程) | 子线程更新数据 | 是 | --- ## **4. 为什么需要 `setValue()`?** ### **(1)避免线程切换开销** - 在主线程直接调用 `setValue()` 比通过 `Handler` 切换到主线程(如 `postValue()`)更高效,适合频繁更新的场景(如计数器、实时状态)。 ### **(2)确保数据同步** - 如果数据更新和 UI 操作必须在同一帧内完成(如动画、进度条更新),`setValue()` 能保证数据和 UI 的同步性。 ### **(3)简化代码逻辑** - 在主线程中无需处理线程切换,代码更简洁直观。 --- ## **4. 注意事项** ### **(1)线程限制** - **必须在主线程调用**:若在子线程调用 `setValue()`,会抛出异常: ```kotlin thread { // 崩溃! viewModel.counter.setValue(10) }.start() ``` **正确做法**:改用 `postValue()` 或切换到主线程: ```kotlin thread { Handler(Looper.getMainLooper()).post { viewModel.counter.setValue(10) // 安全 } }.start() ``` ### **(2) `postValue()` 的选择** - **主线程更新**:优先用 `setValue()`(性能更高)。 - **子线程更新**:必须用 `postValue()`。 ### **(3)避免冗余更新** - 频繁调用 `setValue()` 可能导致不必要的 UI 刷新。可通过条件判断优化: ```kotlin fun updateData(newValue: Int) { if (data.value != newValue) { data.setValue(newValue) // 避免重复设置相同值 } } ``` --- ## **5. 常见问题解决方案** ### **Q1:为什么 `setValue()` 必须在主线程调用?** **答案**: `LiveData` 的设计初衷是安全地更新 UI,而 Android 规定 UI 操作必须在主线程执行。`setValue()` 通过线程检查强制开发者遵守这一规则,避免因子线程操作 UI 导致的崩溃。 ### **Q2:如何在子线程中安全更新 `LiveData`?** **答案**: 使用 `postValue()` 或通过 `Handler` 切换到主线程: ```kotlin thread { // 方法 1:postValue liveData.postValue("New Value") // 方法 2:Handler Handler(Looper.getMainLooper()).post { liveData.setValue("New Value") } } ``` ### **Q3:`setValue()` 会触发观察者的哪些生命周期状态?** **答案**: 只有当观察者的 `Lifecycle` 处于 `STARTED` 或 `RESUMED` 状态时,`setValue()` 才会触发回调。已销毁的 `Activity`/`Fragment` 不会收到更新。 ### **Q4:如何测试 `setValue()` 的行为?** **答案**: 使用 `InstantTaskExecutorRule` 强制同步执行: ```kotlin @Test fun testSetValue() { val liveData = MutableLiveData<String>() val result = mutableListOf<String>() liveData.observeForever(result::add) liveData.setValue("Test") // 同步触发 assertEquals(listOf("Test"), result) } ``` --- ## **5. 总结** - **作用**:`setValue()` 用于在主线程同步更新 `LiveData` 并触发观察者回调,保证数据 UI 的实时一致性。 - **关键点**:主线程限制、同步更新、粘性事件。 - **最佳实践**:主线程更新数据时优先使用 `setValue()`,子线程使用 `postValue()`。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值