Android开发[1]:协程

3 阅读5分钟

协程

今日核心目标

  • 掌握职场常用协程用法,避开实际开发踩坑点。
  • 掌握协程池设计核心逻辑,解决线程过多的性能问题。
  • 完成通用协程Scope封装,落地可复用的协程池。

掌握协程3个核心知识点

  • 协程Dispatchers优化(贴合你的开发场景)
  • Job生命周期管理(解决协程泄漏问题)
  • 协程异常处理(保障App稳定性)

协程Dispatchers优化(贴合你的开发场景)

  • Dispatchers.Main
  • Dispatchers.IO
  • Dispatchers.Default
Dispatchers.Main
  • 主线程,用于更新UI、处理UI相关操作(例:Toast提示、页面刷新等)。
  • 禁止在此线程执行耗时操作,否则会导致App卡顿、ANR。
Dispatchers.IO
  • IO线程,用于执行耗时IO操作(例:网络请求、文件读写、蓝牙连接、NDK数据解析等)。
  • 线程池可自动扩容,适合多任务并行(例:多设备蓝牙连接)。
Dispatchers.Default
  • 默认线程,用于执行CPU密集型操作(例:复杂计算)。
  • 不适合IO操作,避免与网络请求、蓝牙、NDK等IO操作混用,防止线程堵塞。

Job生命周期管理(解决协程泄漏问题)

  • CoroutineScope#launch {}
  • CoroutineScope#async {}
  • Job#cancel()
  • Job#join()
CoroutineScope#launch
  • 无返回值,返回Job对象,可通过cancel()取消任务。
  • 适合执行不需要返回结果的操作(例:文件读写、蓝牙连接、数据发送等)。
CoroutineScope#async
  • 有返回值,返回Deferred对象,通过await()获取结果。
  • 适合执行需要获取结果的操作(例:蓝牙设备信息获取、NDK数据解析等)。
Job#cancel
  • 取消机制
  • 例:蓝牙断连页面销毁后,蓝牙连接协程继续尝试连接,占用资源。此时必须调用Job.cancel(),否则协程会继续执行,导致内存泄漏。
Job#join
  • 等待机制
  • 适合多任务顺序执行,等待前一个协程执行完成,再执行下一个
    • 场景1:先登录,再获取用户信息
    • 场景2:先连接蓝牙,再获取设备信息

协程异常处理(保障App稳定性)

  • try-catch
  • CoroutineExceptionHandler
  • supervisorScope
try-catch
  • 局部异常捕获
  • 适用于单个协程的异常处理
  • 例:网络请求获取数据失败、单个蓝牙设备连接失败等
CoroutineExceptionHandler
  • 全局异常处理器
  • 统一捕获所有协程的异常,便于排查问题,避免App闪退
  • 例:网络请求、蓝牙连接、数据库操作,异常日志统一打印
supervisorScope
  • 监督作用域
    • public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R)
  • 子协程异常不会影响其他子协程,适合多任务并行场景
  • 例:多设备连接时,一个设备连接失败,不会影响其他设备连接

协程池知识点

  • 协程池设计(解决性能问题)
  • 协程泄漏进阶解决方案(规避泄漏)
  • async/await实战(适配有返回值场景)

协程池设计(解决性能问题)

  • 痛点:协程Scope默认线程池,在多任务、多设备蓝牙连接场景会自动创建大量线程,导致线程混乱、CPU占用过高、App卡顿等问题。
  • 逻辑:通过自定义Dispatcher,控制线程数量,避免线程无限扩容,适配多任务、多设备蓝牙连接并行场景,提升App性能和稳定性。
  • 关键点:设置合理线程数(3-5个即可,无需过多,避免资源浪费),确保多任务并行且不占用过多系统资源。

协程泄漏进阶解决方案(规避泄漏)

按场景分析解决方案

  • 场景1:蓝牙后台监听协程(全局协程),App退出后台后,协程仍在运行,导致资源浪费和泄露,添加后台状态监听,自动暂停/恢复协程。
  • 场景2:NDK长耗时解析协程,解析未完成时页面销毁,单纯取消Job可能无法彻底终止NDK任务,需结合NDK接口,实现协程与NDK任务的联动取消。

async/await实战(适配有返回值场景)

  • 用途:适合有返回值的耗时操作,替代Callback获取返回值,简化代码。
    • NDK数据解析,返回解析结果。
    • 蓝牙设备信息获取,返回设备名称、信号强度等。
  • 用法:async启动协程,返回Deferred对象,通过await()获取返回值,await()会挂起协程,直到获取到结果,避免阻塞线程。
  • 异常:async协程的异常可通过全局异常处理器捕获,也可通过try-catch局部获取,避免闪退。
  • 场景:多设备蓝牙连接时,同时获取多个设备的信息,用async/await实现并行获取,提升效率。

通用协程Scope封装

import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore

/**
 * 通用协程域封装,自带异常隔离+线程池
 */
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()
    }

    // 退出App,一般不用
    fun destroy() {
        parentJob.cancel()
    }
}