24、Flow与性能优化:Flow的冷启动优化、数据流合并优化、避免重复计算

性能优化这件事,说白了就是跟资源较劲。CPU、内存、线程,哪样都不是白给的。Flow 虽然好用,但用不好照样能把 App 拖垮。我见过不少项目,Flow 满天飞,结果冷启动慢得像蜗牛,数据流重复计算搞得界面卡顿。今天咱们就来聊聊怎么给 Flow 做“瘦身”。

核心要点:Flow 的性能优化主要围绕三个方向——减少不必要的启动开销、合并冗余的数据流、避免重复计算。这三板斧砍下去,性能至少提升 30%。

24.1 冷启动优化:别让 Flow 一上来就“全速跑”

Flow 是冷数据流,这个大家都知道。但冷启动的问题在于:每次订阅都会从头开始执行。如果你的 Flow 里包了个网络请求或者数据库查询,那每次 collect 都会触发一次完整操作。这在列表页、详情页来回切换时特别明显。

我个人习惯的做法是:用 stateIn 或 shareIn 把冷流转成热流。这样多个订阅者共享同一个上游,不会重复执行。

// 冷启动优化前:每次 collect 都重新请求
val userFlow: Flow<User> = flow {
    emit(fetchUserFromNetwork()) // 每次订阅都执行
}

// 优化后:共享数据,只请求一次
val userFlow: Flow<User> = flow {
    emit(fetchUserFromNetwork())
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000), // 5秒内无订阅者才停止
    initialValue = User.EMPTY
)

这里有个坑,我曾经踩过:SharingStarted 的参数选不对,反而更耗性能。Eagerly 模式一启动就执行,哪怕没人订阅;Lazily 模式等第一个订阅者来了才执行,但之后一直保持活跃。我个人推荐 WhileSubscribed,配合一个合理的超时时间(比如 5 秒),既能避免重复请求,又不会长期占用资源。

小技巧:如果你的 Flow 只在界面可见时需要,可以用 SharingStarted.WhileSubscribed(0),这样界面一消失就停止,特别省电。

24.2 数据流合并优化:别让多个 Flow 各自为政

实际项目中,一个页面往往依赖多个数据源。比如用户信息、商品列表、购物车数量。如果每个都单独 collect,不仅代码难看,线程开销也大。

你想想看,三个 Flow 各自跑在三个协程里,每个都做线程切换、数据发射。合并之后,一个协程搞定,省下的线程切换开销可不是一星半点。

// 优化前:三个独立的 collect
viewModelScope.launch {
    userFlow.collect { user -> updateUserUI(user) }
}
viewModelScope.launch {
    productFlow.collect { products -> updateProductUI(products) }
}
viewModelScope.launch {
    cartFlow.collect { count -> updateCartBadge(count) }
}

// 优化后:合并成一个 Flow
viewModelScope.launch {
    combine(userFlow, productFlow, cartFlow) { user, products, count ->
        Triple(user, products, count)
    }.collect { (user, products, count) ->
        updateUserUI(user)
        updateProductUI(products)
        updateCartBadge(count)
    }
}

这里要注意:combine 是“合并”,不是“拼接”。它会在每个 Flow 有新数据时,把最新的值组合起来。而 zip 是“配对”,必须等所有 Flow 都发射一次才触发。我建议大部分场景用 combine,除非你明确需要一一对应。

避坑指南:我曾经在项目中用 combine 合并了 6 个 Flow,结果每次更新都触发 6 次重组。后来发现是某个 Flow 发射太频繁。解决办法:用 debouncedistinctUntilChanged 过滤掉无意义的数据。

24.3 避免重复计算:用 distinctUntilChanged 和缓存

重复计算是性能杀手。最常见的情况:数据没变,但 UI 重新计算了。比如用户滑动列表,每次滚动都触发一次数据流发射,但数据内容根本没变。

Flow 自带的 distinctUntilChanged 就是干这个的。它会比较前后两次发射的值,如果相同就跳过。嗯,这里要注意:默认比较用的是 equals,如果你的数据类没重写 equals,那比较的就是引用地址,基本每次都不同,等于没优化。

// 数据类必须重写 equals 和 hashCode
data class Product(
    val id: String,
    val name: String,
    val price: Double
)

// 使用 distinctUntilChanged 过滤重复数据
val productFlow: Flow<List<Product>> = repository.getProducts()
    .distinctUntilChanged()
    .map { products ->
        // 只有数据真正变化时才执行这里的计算
        products.filter { it.price > 100 }
    }

除了 distinctUntilChanged,缓存也是避免重复计算的好办法。我习惯在 Repository 层做内存缓存,Flow 发射前先检查缓存有没有更新。如果缓存没变,直接返回旧数据,省掉网络请求和数据库查询。

class ProductRepository {
    private var cache: List<Product>? = null

    fun getProducts(): Flow<List<Product>> = flow {
        // 先发射缓存
        cache?.let { emit(it) }
        
        // 再从网络获取最新数据
        val fresh = fetchFromNetwork()
        cache = fresh
        emit(fresh)
    }.distinctUntilChanged()
}

核心原则:能不计算就不计算,能少计算就少计算。distinctUntilChanged 是“过滤”,缓存是“复用”,两者结合效果最佳。

24.4 知识体系总览

下面这张图把 Flow 性能优化的三个方向串起来了。你可以看到,冷启动优化解决的是“重复执行”问题,数据流合并解决的是“多路开销”问题,避免重复计算解决的是“无效计算”问题。三者环环相扣,缺一不可。

Flow 性能优化三大方向 Flow 性能优化 冷启动优化 stateIn / shareIn 转热流 WhileSubscribed 超时策略 避免重复订阅执行 数据流合并优化 combine 合并多路 Flow 减少协程数量 debounce 防抖过滤 避免重复计算 distinctUntilChanged 过滤 内存缓存复用结果 数据类重写 equals

24.5 实战建议:优化要分优先级

说了这么多,到底先优化哪个?我个人的经验是:先解决重复计算,再处理冷启动,最后合并数据流。为什么?因为重复计算是“无脑消耗”,改起来最简单,收益也最明显。冷启动优化需要改架构,合并数据流需要梳理业务逻辑,这两步可以慢慢来。

优化方向 优先级 改动成本 收益 推荐场景
避免重复计算 列表页、详情页、频繁刷新的页面
冷启动优化 首页、启动页、多 Tab 页面
数据流合并 复杂页面、多数据源聚合场景

最后提醒一句:性能优化不是一锤子买卖。上线后记得用 Profiler 或 Firebase Performance 监控一下,看看优化效果到底怎么样。我见过有人优化了半天,结果发现瓶颈在别的地方——嗯,数据说话最靠谱。

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