Android开发[3]:协程+Flow

协程+Flow小结

今日核心目标

  • 整合协程+Flow核心知识点,掌握协程+Flow在多场景下的统一封装技巧,完善全局异常处理、日志规范,形成标准化的异步处理体系、同时完成对前两节知识复盘与实战总结,为后续进阶学习铺垫。

知识点复盘(巩固基础)

  • 协程核心
  • Flow核心
  • 核心痛点

协程核心

掌握协程基础(launch/async)
  • launch:无返回值的异步任务
    • 发起异步任务,不需要返回结果,用于执行独立操作(例:网络请求、IO、蓝牙等)。
  • async:有返回值的异步任务
    • 返回Deferred,通过await()获取结果,适合并行异步任务。
协程池封装(蓝牙/IO/主线程)

Android 标准线程调度(主线程 / IO / 蓝牙),解决线程混乱问题

csharp 复制代码
/**
 * 协程调度器封装
 */
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崩溃。

  • 局部异常处理(单个任务)
kotlin 复制代码
scope.launch {
    try {
        // 耗时任务
        val data = api.request()
    } catch (e: Exception) {
        // 本任务异常,只处理自己,不影响其他协程
        e.printStackTrace()
    }
}
  • 全局异常捕获(统一处理)
scss 复制代码
// 全局异常捕获处理器
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销毁时自动取消
  • 自定义组件绑定(蓝牙、引擎类)
kotlin 复制代码
/**
 * 通用协程域封装,自带异常隔离+线程池
 */
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(网络回调、蓝牙回调、权限回调)
  • 以网络请求为例
kotlin 复制代码
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:高并发、被压安全(适合高频数据、多线程发送,自带缓冲区、不会丢数据)
scss 复制代码
/**
 * 批量获取数据
 */
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状态、列表、播放状态、全局适配
kotlin 复制代码
// 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:事件总线(多页面通信)
  • 无初始值
  • 可配置缓冲、重放、粘性
  • 适合:事件通知、全局消息、多页面同步
kotlin 复制代码
// 全局事件总线(替代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内不输入才请求
scss 复制代码
/**
 * 用于输入框联想词:输入框500ms内不输入才请求
 */
fun searchFlow(query: String) = flow { emit(query) }
    .debounce(500) // 防抖
    .filter { it.isNotEmpty() }
    .distinctUntilChanged() // 去重
    .flatMapConcat { api.search(it) }
throttleLatest节流:控制数据处理频率,在指定时间内只处理一次数据(或只取最新数据)。
  • 用于传感器、滑动、蓝牙实时数据
kotlin 复制代码
/**
 * 节流:高频数据只取最新
 */
fun <T> Flow<T>.throttleLatest(milliseconds: Long): Flow<T> = this
    .throttleLatest(milliseconds) // milliseconds毫秒取一次最新值
    .flowOn(CoroutineHelper.multiTaskDispatcher) // 绑定协程池,避免阻塞主线程
sample采样:每隔一段时间采样一次。
kotlin 复制代码
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(蓝牙 / 网络)
kotlin 复制代码
fun listenData(): Flow<Data> = callbackFlow {
    val callback = Callback { data -> trySend(data) }
    register(callback)
    awaitClose { unregister(callback) }
}
  • UI 状态 StateFlow(ViewModel)
ini 复制代码
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
  • 全局事件 SharedFlow(页面通信)
ini 复制代码
private val _events = MutableSharedFlow<Any>()
val events = _events.asSharedFlow()
  • 安全收集(必须用,防泄漏)
scss 复制代码
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        flow.collect { ... }
    }
}
  • 防抖搜索
scss 复制代码
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级别。
相关推荐
张小潇2 小时前
AOSP15 WMS/AMS系统开发 - WindowManagerService addWindow详解
android
爱吃牛肉的大老虎2 小时前
MySQL优化之系统表分析SQL
android·sql·mysql
Fate_I_C2 小时前
Kotlin数据类equals和 == 会返回true
kotlin
Fate_I_C2 小时前
实战案例:用 Kotlin 重写一个 Java Android 工具类
android·java·kotlin
Fate_I_C2 小时前
Kotlin 特有语法糖
android·开发语言·kotlin
Fate_I_C2 小时前
Kotlin 为什么是 Android 开发的首选语言
android·开发语言·kotlin
黄林晴2 小时前
Android CLI 来了!终端一键建项目、控模拟器、给 Agent 喂官方规范
android
常利兵2 小时前
Kotlin 助力 Android 启动“大提速”
android·开发语言·kotlin