协程+Flow小结
今日核心目标
- 整合协程+Flow核心知识点,掌握协程+Flow在多场景下的统一封装技巧,完善全局异常处理、日志规范,形成标准化的异步处理体系、同时完成对前两节知识复盘与实战总结,为后续进阶学习铺垫。
知识点复盘(巩固基础)
- 协程核心
- Flow核心
- 核心痛点
协程核心
掌握协程基础(launch/async)
- launch:无返回值的异步任务
- 发起异步任务,不需要返回结果,用于执行独立操作(例:网络请求、IO、蓝牙等)。
- async:有返回值的异步任务
- 返回Deferred,通过await()获取结果,适合并行异步任务。
协程池封装(蓝牙/IO/主线程)
Android 标准线程调度(主线程 / IO / 蓝牙),解决线程混乱问题
/**
* 协程调度器封装
*/
object CoroutineDispatchers {
// 主线程:UI操作
val main: MainCoroutineDispatcher
get() = Dispatchers.Main
// IO线程:网络、数据库、文件
val io: CoroutineDispatcher
get() = Dispatchers.IO
// 专用线程池,避免阻塞IO线程:蓝牙、硬件通信
val singleTask: CoroutineDispatcher
get() = Dispatchers.Default.limitedParallelism(1) // 单线程串行,蓝牙必须串行
// 多线程池
val multiTask: CoroutineDispatcher
get() = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
}
异常处理:全局+局部(解决崩溃、异常丢失)
协程异常默认向上传播,不处理会直接导致App崩溃。
- 局部异常处理(单个任务)
scope.launch {
try {
// 耗时任务
val data = api.request()
} catch (e: Exception) {
// 本任务异常,只处理自己,不影响其他协程
e.printStackTrace()
}
}
- 全局异常捕获(统一处理)
// 全局异常捕获处理器
val coroutineHandler = CoroutineExceptionHandler { _, throwable ->
// 全局统一处理:日志上报、弹窗提示、异常恢复
throwable.printStackTrace()
}
// 使用
// 方式1:直接加到协程域
val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Main + coroutineHandler)
// 方式2:处理异常
coroutineHandler.handleException(Dispatchers.IO, throwable)
- 异常处理规则
- SupervisorJob:子协程异常互不干扰
- try-catch:局部精准处理
- CoroutineExceptionHandler:全局兜底
- async必须包裹try-catch:否则await()会崩溃
生命周期绑定(解决内存泄漏)
- Activity/Fragment绑定:lifecycleScope.launch { }
- 自动跟随生命周期销毁,不会内存泄漏
- ViewModel绑定:viewModelScope.launch { }
- ViewModel销毁时自动取消
- 自定义组件绑定(蓝牙、引擎类)
/**
* 通用协程域封装,自带异常隔离+线程池
*/
open class BaseCoroutineScope {
// 父Job:一个子协程崩溃,不会干扰其他子协程
private val parentJob = SupervisorJob()
// 全局异常捕获处理器
private val coroutineHandler = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTrace()
}
/* ----- 绑定默认调度器 ----- */
// IO线程:网络、数据库、文件
protected val ioScope =
CoroutineScope(parentJob + Dispatchers.IO + coroutineHandler)
// 主线程:UI操作
protected val mainScope =
CoroutineScope(parentJob + Dispatchers.Main + coroutineHandler)
// 专用线程池,避免阻塞IO线程:蓝牙、硬件通信
protected val singleTaskScope =
CoroutineScope(parentJob + Dispatchers.Default.limitedParallelism(1) + coroutineHandler)
/**
* 并发任务
*
* @param permits 最大并发数
*/
fun multiTaskLaunch(
permits: Int,
block: suspend () -> Unit
) {
val semaphore = Semaphore(permits)
CoroutineScope(parentJob + Dispatchers.Default + coroutineHandler).launch {
semaphore.acquire()
try {
block()
} finally {
semaphore.release()
}
}
}
// 生命周期销毁时取消所有协程
fun cancelAll() {
parentJob.cancelChildren()
}
}
// 蓝牙任务:串行执行任务
class BLEManager : BaseCoroutineScope() {
// 蓝牙任务:运行在专用单任务线程
fun connectDevice() {
singleTaskScope.launch {
// 蓝牙连接逻辑(串行执行)
}
}
// 页面退出时调用
fun onDestroy() {
cancelAll() // 取消所有蓝牙协程
}
}
问题解决
| 问题 | 解决方案 |
|---|---|
| 异步任务阻塞 | 用launch/async+Dispatchers.IO,挂起不阻塞 |
| 线程混乱 | 封装固定调度器:main/io/singleTask/multiTask,禁止随意切换线程 |
| 内存泄漏 | 绑定lifecycleScope/viewModelScope,页面销毁自动取消 |
| 异常崩溃 | SupervisorJob+局部try-catch+全局异常处理器 |
小结
- 基础:launch 无返回值,async 有返回值
- 封装:固定 4 种调度器(主线程 / IO / 单任务协程池 / 多任务协程池),统一管理线程
- 异常:SupervisorJob 隔离 + 局部捕获 + 全局兜底
- 生命周期:lifecycleScope 自动绑定,彻底杜绝内存泄漏
Flow核心
Flow自带生命周期安全,配合lifecycle.repeatOnLifecycle完全无泄漏
掌握冷流(callbackFlow/channelFlow)
- 冷流:无人订阅就不执行(网络、蓝牙、IO、单次任务)
- flow、callbackFlow、channelFlow
- 把传统回调转换成Flow,消灭回调地狱
callbackFlow:常用于回调转Flow(网络回调、蓝牙回调、权限回调)
- 以网络请求为例
interface ApiService {
// 挂起函数,无需callbackFlow
@GET("{page}")
suspend fun getTests(@Path("page") page: Int): Tests
// 传统Call方式,用来转callbackFlow
@GET("{page}")
fun getTestsFlow(@Path("page") page: Int): Call<Tests>
}
class FlowRepository {
private val api = ServiceCreator.createService(ApiService::class.java)
fun getTestsFlow(): Flow<Tests?> = callbackFlow {
val call = api.getTestsFlow(1)
call.enqueue(object : Callback<Tests> {
override fun onResponse(
call: Call<Tests?>,
response: Response<Tests?>
) {
if (response.isSuccessful) {
val data = response.body() ?: null
trySend(data) // 发送数据
} else {
close(Exception("请求失败"))
}
// 关闭流
close()
}
override fun onFailure(p0: Call<Tests?>, throwable: Throwable) {
// 异常关闭
close(throwable)
}
})
// 协程取消时自动中断请求(防泄漏)
awaitClose {
// 取消网络请求
call.cancel()
}
}
}
// 使用
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
flowRepository.getTestsFlow()
.catch { e ->
LogTool.e("MainActivity", "callbackFlow error: ${e.message}")
}
.collect { tests ->
LogTool.i("MainActivity", "callbackFlow tests: $tests")
}
}
}
channelFlow:高并发、被压安全(适合高频数据、多线程发送,自带缓冲区、不会丢数据)
/**
* 批量获取数据
*/
fun getTestsChannelFlow(): Flow<Tests> = channelFlow {
// 开启10个协程批量请求
val first = 1
repeat(10) { index ->
launch {
// 网络请求
val tests = api.getTests(first + index)
// 发送数据
send(tests)
}
}
}
// 使用
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
flowRepository.getTestsChannelFlow()
.collect { tests ->
// 逐个接收结果
LogTool.i("MainActivity", "callbackFlow tests: $tests")
}
}
}
热流(StateFlow/SharedFlow)
- 热流:不管有没有人订阅都在跑(页面状态、实时数据)
- StateFlow、SharedFlow
StateFlow:页面状态管理(ViewModel必备)
- 必须有初始值
- 始终持有最新状态
- 粘性数据(新订阅立刻收到最后一条数据)
- 适合:UI状态、列表、播放状态、全局适配
// ViewModel内标准使用方式
class MyViewModel : ViewModel() {
// 私有:可读写
private val _uiState = MutableStateFlow(UiState())
// 公开:只读(外部只能订阅,不能修改)
val uiState: StateFlow<UiState> = _uiState
// 更新状态
fun updateData(data: String) {
_uiState.value = _uiState.value.copy(data = data)
}
}
// 页面安全订阅
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
// 更新UI
}
}
}
SharedFlow:事件总线(多页面通信)
- 无初始值
- 可配置缓冲、重放、粘性
- 适合:事件通知、全局消息、多页面同步
// 全局事件总线(替代EventBus)
object EventBus {
private val _events = MutableSharedFlow<Any>(
replay = 0, // 不粘滞(新订阅不收历史事件)
extraBufferCapacity = 10 // 缓冲
)
val events = _events.asSharedFlow()
// 发送事件
suspend fun post(event: Any) {
_events.emit(event)
}
}
// 使用
// 页面A发送
EventBus.post(LoginEvent(true))
// 页面B接收
lifecycleScope.launch {
EventBus.events.collect { event ->
if (event is LoginEvent) {
// 处理登录
}
}
}
操作符:节流防抖(解决高频数据卡顿)
- 专门解决:搜索框、滚动、点击、传感器、蓝牙高频数据导致的UI卡顿
debounce防抖:等待数据稳定后再处理,若在指定时间内有新数据发射,则重新计时。
- 输入框最常用:输入框500ms内不输入才请求
/**
* 用于输入框联想词:输入框500ms内不输入才请求
*/
fun searchFlow(query: String) = flow { emit(query) }
.debounce(500) // 防抖
.filter { it.isNotEmpty() }
.distinctUntilChanged() // 去重
.flatMapConcat { api.search(it) }
throttleLatest节流:控制数据处理频率,在指定时间内只处理一次数据(或只取最新数据)。
- 用于传感器、滑动、蓝牙实时数据
/**
* 节流:高频数据只取最新
*/
fun <T> Flow<T>.throttleLatest(milliseconds: Long): Flow<T> = this
.throttleLatest(milliseconds) // milliseconds毫秒取一次最新值
.flowOn(CoroutineHelper.multiTaskDispatcher) // 绑定协程池,避免阻塞主线程
sample采样:每隔一段时间采样一次。
fun <T> Flow<T>.sample(milliseconds: Long): Flow<T> = this
.sample(milliseconds) // 每milliseconds毫秒拿一个数据
.flowOn(CoroutineHelper.multiTaskDispatcher) // 绑定协程池,避免阻塞主线程
问题解决
| 问题 | 解决方案 |
|---|---|
| 回调嵌套、回调地狱 | callbackFlow把回调变线性Flow |
| 高频数据卡顿 | debounce/throttleLatest防抖节流 |
| 多页面数据不同步 | StateFlow/SharedFlow全局共享 |
| 内存泄漏 | repeatOnLifecycle生命周期安全 |
| 异步数据不统一 | 冷流+热流标准化 |
小结
- 冷流:没人订阅不执行 → 网络 / IO / 蓝牙回调 → callbackFlow
- 热流:始终持有数据 → 状态 / 事件 / 共享 → StateFlow/SharedFlow
- 防抖节流:debounce 搜索,throttleLatest 高频数据
- 多页面共享:单例 StateFlow/SharedFlow
- 安全订阅:必须用 repeatOnLifecycle
使用示例
- 回调转 Flow(蓝牙 / 网络)
fun listenData(): Flow<Data> = callbackFlow {
val callback = Callback { data -> trySend(data) }
register(callback)
awaitClose { unregister(callback) }
}
- UI 状态 StateFlow(ViewModel)
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
- 全局事件 SharedFlow(页面通信)
private val _events = MutableSharedFlow<Any>()
val events = _events.asSharedFlow()
- 安全收集(必须用,防泄漏)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
flow.collect { ... }
}
}
- 防抖搜索
searchQueryFlow
.debounce(500)
.distinctUntilChanged()
.flatMapLatest { api.search(it) }
应用
统一封装逻辑
- 统一线程管理:所有异步任务复用协程池,避免线程切换混乱,统一线程配置。
- 统一异常处理:所有Flow数据流均结合全局异常处理器,捕获各类异常,统一打印日志,避免App闪退,形成异常处理闭环。
- 统一状态管理
- 用StateFlow管理所有场景的状态(蓝牙连接状态、网络请求状态、数据库操作状态)。
- 用SharedFlow分发所有场景的数据流(蓝牙数据、网络数据、数据库数据)。
- 实现多场景状态同步、数据共享。
- 统一工具类:Coroutine、Flow、网络、数据库等工具方法整合,形成统一异步工具体系,提升开发效率。
Room数据库:协程+Flow(数据持久化)
Room原生支持Flow,可实现数据库数据变化时自动通知UI更新,无需手动监听,结合协程实现数据库操作异步化,避免阻塞主线程。
用法
- Room Dao层方法返回Flow<List>,当数据库数据发生变化时,Flow自动发射新数据。
- 用协程IO线程执行数据库增删改查操作,用StateFlow管理数据库操作状态,用Flow操作符(filter/map)处理数据库查询结果。
- 实现网络数据与本地缓存的协同(先查本地缓存,再请求网络更新)。
坑点复盘
坑点1:协程Scope滥用导致内存泄漏
- 现象:页面销毁后,协程仍在执行网络请求/数据库操作引发内存泄漏。
- 原因:未正确绑定页面/ViewModel生命周期,滥用全局协程Scope。
- 解决:严格区分页面级、ViewModel级、全局级Scope,页面销毁时主动取消协程,完善Flow生命周期绑定方法,新增协程取消监听。
坑点2:Flow取消不及时导致数据错乱
- 现象:页面切换后,前页面Flow仍在发射数据,导致UI错乱。
- 原因:Flow未与页面生命周期绑定,未在页面销毁时取消收集。
- 解决:优化bindLifecycle(),新增Flow取消回调,在页面onDestroy()时主动取消Flow收集,避免无效数据发射。
坑点3:数据库操作未判空导致空指针
- 现象:Room数据库查询结果为null时,未作判空处理,引发空指针异常。
- 原因:Dao层返回Flow<T?>时,收集时未判空。
- 解决:增加判空处理,在工具类中默认值返回空对象/空列表,避免空指针,完善异常捕获机制。
坑点4:网络请求未处理离线场景
- 现象:离线状态下,网络请求直接返回失败,未优先加载本地缓存。
- 原因:请求逻辑中缓存查询逻辑不完善,未处理离线异常。
- 解决:优化协同逻辑,新增离线判断,离线时返回本地缓存,网络恢复时自动更新
坑点5:线程池配置不合理导致卡顿
- 现象:高频网络请求、蓝牙连接过多时,出现UI卡顿。
- 原因:IO协程池、蓝牙协程池核心线程数配置不合理,线程切换频繁。
- 解决:优化协程池配置,动态调整核心线程数,避免线程阻塞,新增线程池状态监控日志。
坑点6:日志规范不统一导致排查困难
- 现象:不同场景日志打印格式混乱,无场景标识,异常排查效率低。
- 原因:未统一日志打印规范,未区分场景、级别。
- 解决:完善全局日志工具,统一日志格式,新增场景标识(网络、数据库、蓝牙等),区分DEBUG、INFO、WARN、ERROR级别。