10、RxJava与Room:RxJava基础回顾、在DAO中返回Flowable/Single、线程调度、背压处理

说实话,很多Android开发者刚接触Room时,都习惯用LiveData做数据观察。LiveData确实简单,但一旦涉及到复杂的线程切换、数据流变换、或者需要处理背压的场景,它就有点力不从心了。这时候,RxJava就派上了用场。

我个人习惯把Room + RxJava称为“黄金搭档”。Room天然支持RxJava的返回类型,你可以在DAO方法里直接返回Flowable、Single、Maybe、Completable等。配合RxJava的线程调度和操作符,数据流的处理会变得非常优雅。

这一章,我们就来聊聊怎么把RxJava和Room结合起来用。我会从基础回顾开始,然后深入到DAO中的具体用法,最后聊聊背压这个容易踩坑的点。

10.1 RxJava基础回顾

RxJava的核心思想,说白了就是“观察者模式 + 异步 + 操作符”。你有一个数据源(Observable),一个观察者(Observer),中间可以加各种操作符来变换数据、切换线程。

我记得刚开始学RxJava时,最头疼的就是各种类型的区别:Observable、Flowable、Single、Completable、Maybe。其实你只要记住一个原则:根据数据量选类型

类型 发射数据量 典型场景
Observable 0..N 普通数据流,不支持背压
Flowable 0..N 支持背压的数据流,适合大量数据
Single 1 单次查询,比如根据ID查一条记录
Maybe 0或1 可能没有结果,比如检查是否存在
Completable 0 只关心完成,比如插入、更新操作

嗯,这里要注意:Room的DAO中,最常用的是Flowable和Single。Flowable用于监听数据变化(类似LiveData),Single用于一次性查询。

10.2 在DAO中返回Flowable/Single

我们先看一个简单的例子。假设你有一个用户表,你想监听所有用户的变化。用LiveData是这样写的:

@Dao
public interface UserDao {
    @Query("SELECT * FROM users")
    LiveData<List<User>> getAllUsers();
}

换成RxJava的Flowable,只需要改一下返回类型:

@Dao
public interface UserDao {
    @Query("SELECT * FROM users")
    Flowable<List<User>> getAllUsers();
}

就这么简单。Room会自动帮你把数据库的变更事件转换成Flowable的发射事件。每次表数据有变化,Flowable就会重新查询并发射新的结果。

如果你只需要查一次,不想监听后续变化,那就用Single:

@Dao
public interface UserDao {
    @Query("SELECT * FROM users WHERE id = :userId")
    Single<User> getUserById(int userId);
}

Single只会发射一次数据,然后结束。我在项目中经常用它来做“详情页”的数据加载——用户进入页面时查一次,离开页面时自动取消订阅,非常干净。

小技巧: 如果你不确定用Flowable还是Single,可以问自己一个问题:“这个数据需要实时更新吗?” 需要就Flowable,不需要就Single。

10.3 线程调度

RxJava最强大的地方之一就是线程调度。Room默认是在后台线程执行数据库操作的,但如果你不指定调度器,订阅者(观察者)会运行在哪个线程呢?答案是:跟发射者同一个线程

这就有问题了。如果你在DAO中返回Flowable,然后直接在UI线程订阅,Room会抛异常——因为数据库操作不能在主线程执行。但Flowable的订阅回调又默认运行在发射线程。嗯,这里有点绕,我解释一下。

实际上,Room的Flowable是在后台线程发射数据的。如果你不切换线程,订阅者的onNext也会在后台线程执行。这时候你如果在onNext里更新UI,就会报错:

// 错误示例:直接在后台线程更新UI
userDao.getAllUsers()
    .subscribe(users -> {
        // 这里运行在Room的后台线程
        textView.setText(users.size()); // 崩溃!不能在子线程更新UI
    });

正确的做法是使用observeOn切换到主线程:

userDao.getAllUsers()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(users -> {
        // 现在运行在主线程,可以更新UI
        textView.setText(users.size());
    });

我个人习惯在ViewModel中做线程调度,这样Activity/Fragment只关心UI更新:

public class UserViewModel extends ViewModel {
    private final UserDao userDao;
    private final CompositeDisposable disposables = new CompositeDisposable();

    public UserViewModel(UserDao userDao) {
        this.userDao = userDao;
    }

    public Flowable<List<User>> getUsers() {
        return userDao.getAllUsers()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread());
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        disposables.clear(); // 防止内存泄漏
    }
}
警告: 千万不要忘记在Activity/Fragment销毁时取消订阅。我见过太多因为忘记调用disposable.dispose()导致的内存泄漏。建议在ViewModel中使用CompositeDisposable统一管理。

10.4 背压处理

背压(Backpressure)是RxJava中一个比较高级的概念。简单来说,就是生产者发射数据的速度 > 消费者处理数据的速度时,数据会堆积,最终导致内存溢出。

为什么会这样?你想想看,如果你的数据库表频繁变化(比如每秒插入100条记录),Flowable就会每秒发射100次新列表。如果你的UI处理速度跟不上(比如列表渲染需要200ms),数据就会在内存中堆积。

Flowable天生支持背压,而Observable不支持。这也是为什么我建议在Room的DAO中优先使用Flowable而不是Observable。

Flowable提供了多种背压策略,你可以通过onBackpressureBuffer()onBackpressureDrop()onBackpressureLatest()来控制:

策略 行为 适用场景
BUFFER 无限制缓存所有数据 数据量小,消费者处理快
DROP 丢弃无法处理的数据 只关心最新数据,旧数据可丢弃
LATEST 只保留最新的数据 实时性要求高,比如股票价格

我曾经在一个聊天应用里遇到过背压问题。用户发消息非常频繁,每次消息入库都会触发Flowable重新查询整个聊天记录列表。列表有几千条数据,渲染一次要几百毫秒。结果就是:用户发了10条消息,列表只渲染了3次,剩下的7次数据被积压在内存里。

解决方案很简单,用onBackpressureLatest()

userDao.getMessages(chatId)
    .onBackpressureLatest()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(messages -> {
        // 只处理最新的数据,中间的数据被丢弃
        adapter.submitList(messages);
    });

这样,即使数据库变化再频繁,UI也只会处理最新的那一次数据。中间的变化被自动丢弃,内存压力大大降低。

核心要点: Room + RxJava的组合,本质上是把数据库的变更事件转化为响应式数据流。Flowable负责监听变化,Single负责一次性查询,observeOn负责线程切换,背压策略负责流量控制。把这四样用好,你的数据层就会非常健壮。

10.5 知识体系总览

下面这张图总结了本章的核心逻辑,你可以把它当作一个速查表:

Room + RxJava 核心知识体系 DAO返回类型 线程调度 背压处理 Flowable Single subscribeOn observeOn BUFFER DROP/LATEST 关键说明: • Flowable:监听数据变化,支持背压,适合列表数据 • Single:一次性查询,适合详情页数据加载 • subscribeOn:指定数据库操作在IO线程执行 • observeOn:指定UI更新在主线程执行

这张图把本章的核心知识点串起来了。你从DAO返回类型出发,根据需求选择Flowable或Single,然后通过线程调度确保数据在正确的线程上流动,最后用背压策略应对高频数据变更。每一步都有对应的RxJava操作符,组合起来就是一个完整的数据管道。


公众号:蓝海资料掘金营,微信deep3321