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 发射太频繁。解决办法:用 debounce 或 distinctUntilChanged 过滤掉无意义的数据。
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 性能优化的三个方向串起来了。你可以看到,冷启动优化解决的是“重复执行”问题,数据流合并解决的是“多路开销”问题,避免重复计算解决的是“无效计算”问题。三者环环相扣,缺一不可。
24.5 实战建议:优化要分优先级
说了这么多,到底先优化哪个?我个人的经验是:先解决重复计算,再处理冷启动,最后合并数据流。为什么?因为重复计算是“无脑消耗”,改起来最简单,收益也最明显。冷启动优化需要改架构,合并数据流需要梳理业务逻辑,这两步可以慢慢来。
| 优化方向 | 优先级 | 改动成本 | 收益 | 推荐场景 |
|---|---|---|---|---|
| 避免重复计算 | 高 | 低 | 高 | 列表页、详情页、频繁刷新的页面 |
| 冷启动优化 | 中 | 中 | 高 | 首页、启动页、多 Tab 页面 |
| 数据流合并 | 低 | 高 | 中 | 复杂页面、多数据源聚合场景 |
最后提醒一句:性能优化不是一锤子买卖。上线后记得用 Profiler 或 Firebase Performance 监控一下,看看优化效果到底怎么样。我见过有人优化了半天,结果发现瓶颈在别的地方——嗯,数据说话最靠谱。