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:关键差异
| 特性 | setValue | postValue |
|---|---|---|
| 调用线程 | 必须在主线程(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 线程模型对比
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不刷新等经典问题。