3. LiveData封装进度状态:MutableLiveData使用、postValue/setValue差异、线程安全

在Android开发中,MutableLiveData是封装可变进度状态的首选工具。它继承自LiveData,对外提供setValue和postValue两个公开方法,但它们的线程模型和行为有本质区别。理解这些差异是避免UI更新异常、数据丢失和线程崩溃的关键。

3.1 MutableLiveData基本使用

创建MutableLiveData实例并观察其变化,是LiveData最基础的用法。

  • 创建:val progress = MutableLiveData()
  • 赋值:progress.value = 50(调用setValue)或progress.postValue(50)
  • 观察:progress.observe(viewLifecycleOwner) { value -> ... }
// 基本使用示例:ViewModel中封装进度
class ProgressViewModel : ViewModel() {
    val progress = MutableLiveData<Int>()

    fun startTask() {
        // 后台线程更新进度(必须使用postValue)
        thread {
            for (i in 1..100) {
                Thread.sleep(50)
                progress.postValue(i)
            }
        }
    }
}

3.2 setValue vs postValue:关键差异

特性setValuepostValue
调用线程必须在主线程(UI线程)可在任意线程
更新时机立即同步更新,并通知所有活跃观察者异步,将值post到主线程消息队列,等待后续执行
是否可被覆盖否,每个setValue都会导致一次通知可能被后续postValue覆盖(如果主线程尚未处理Runnable)
典型使用场景UI事件响应、用户交互后立即更新后台任务进度、网络回调、数据库查询结果

内部实现差异:LiveData通过mPendingData和mPostValueRunnable实现线程安全。

// LiveData源码简化示意
public void postValue(T value) {
    synchronized (mDataLock) {
        mPendingData = value;  // 覆盖旧值
        if (!mPostListeners.isEmpty()) {
            // 只发一次Runnable到主线程
            ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
        }
    }
}
@MainThread
public void setValue(T value) {
    assertMainThread("setValue");  // 非主线程直接抛异常
    mVersion++;
    mData = value;
    dispatchingValue(null);  // 立即分发通知
}
⚠️ 常见错误:在后台线程直接调用setValue()会抛出IllegalStateException: Cannot invoke setValue on a background thread。必须使用postValue()。相反,在主线程使用postValue()虽然不报错,但会引入不必要的异步延迟,应优先使用setValue()。

3.3 线程安全与内部机制

LiveData的线程安全设计保证了在多线程环境下不会产生并发数据竞争:

  • setValue:通过assertMainThread确保仅在主线程调用,直接修改mData并同步通知。
  • postValue:使用synchronized保护mPendingData写入,然后通过主线程Handler异步执行真正的赋值。多个postValue在Runnable执行前只会保留最后一个值。

3.4 封装安全更新方法

推荐抽象一个工具类或扩展函数,自动判断当前线程并选择正确的赋值方法。

// 安全更新封装
class TaskProgressLiveData : MutableLiveData<Int>() {
    fun updateProgress(value: Int) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            setValue(value)
        } else {
            postValue(value)
        }
    }
}

// 使用示例
class DownloadViewModel : ViewModel() {
    val progress = TaskProgressLiveData()

    fun downloadFile() {
        viewModelScope.launch(Dispatchers.IO) {
            for (i in 0..100 step 10) {
                delay(200)
                progress.updateProgress(i)  // 自动适配线程
            }
        }
    }
}

3.5 调试与验证技巧

在开发阶段,可以使用observeForever记录所有进度变化,帮助排查值丢失问题。

// 调试辅助:记录所有进度变化
progress.observeForever { value ->
    Log.d("Progress_Debug", "New progress: $value")
}

如果日志中只看到部分值(例如10→30→50),说明postValue发生了覆盖。若需要保留每个中间值,应改用Channel或Flow。

3.6 性能优化建议

  • 降低更新频率:每秒60次的进度更新对UI无害,但浪费CPU;建议每秒最多10次或使用节流。
  • 使用distinctUntilChanged:避免重复通知相同值。
  • 合并多个进度源:使用MediatorLiveData或Transformations.switchMap组合多个MutableLiveData。
// 使用distinctUntilChanged避免重复通知
val distinctProgress = Transformations.distinctUntilChanged(progress)

3.7 内联SVG:postValue vs setValue 线程模型对比

postValue vs setValue 线程模型对比 setValue(主线程) 主线程调用 setValue(50) 设置 mData = 50, mVersion++ 立即通知所有活跃观察者 UI 立即更新 postValue(任意线程) 子线程调用 postValue(50) 加锁设置 mPendingData = 50 向主线程 Handler 投递 Runnable 主线程处理 Runnable 内部调用 setValue(mPendingData) 注:若先后postValue(10)和postValue(50),最终只会通知50

3.8 常见陷阱与解决方案

  • 进度值丢失:在短时间内连续调用postValue,只有最后一个值会被处理。若需要完整进度序列,改用MutableSharedFlow或Channel。
  • 主线程误用postValue:虽然不抛异常,但会延迟UI更新一个消息循环周期,应改用setValue。
  • 单元测试中的线程问题:使用InstantTaskExecutorRule后setValue可在任意线程调用,但生产环境必须遵守线程规则。
  • 后台线程频繁postValue导致ANR:若更新速度超过主线程处理能力,消息队列堆积可能引发ANR。建议在后台进行降频(如每100ms更新一次)。
💡 最佳实践:在ViewModel中始终暴露不可变的LiveData给UI层(如val progress: LiveData get() = _progress),内部使用封装后的安全更新方法。对于需要收集完整进度的场景,考虑使用LiveData结合MediatorLiveData,或直接迁移到Flow。

掌握setValue和postValue的差异及其线程模型,是构建稳定、可预测的进度状态管线的关键一步。合理封装和选择更新方式,能够有效避免数据丢失和UI不刷新等经典问题。