协程基础回顾与作用域绑定

协程在 Android 中的正确使用离不开三个核心概念:CoroutineScope、Job 和 Dispatcher。理解它们并掌握作用域绑定策略,是避免内存泄漏、保证任务及时取消的关键。本章将从原理出发,逐步展示如何利用生命周期感知的协程作用域来管理权限请求等异步任务。

核心概念速览

概念职责典型示例
CoroutineScope定义协程的生存周期,管理子协程viewModelScope, lifecycleScope
Job表示一个异步任务,支持取消、等待launch { … } 返回的 Job
Dispatcher决定协程在哪个线程池运行Dispatchers.Main, IO, Default

生命周期感知:为什么需要绑定?

在 Android 组件(Activity / Fragment)中手动启动协程,若未在组件销毁时取消,协程可能持有 Activity 引用导致内存泄漏,甚至继续执行已不再需要的操作(如更新 UI)。生命周期感知 的协程作用域会自动监听组件状态,在 onDestroy 或 onStop 时取消其内部的所有协程。

推荐:使用 lifecycleScope(来自 androidx.lifecycle)或 viewModelScope 代替手动管理的自定义作用域,减少样板代码。

实现流程:创建与绑定生命周期

以下示例展示了如何在 Activity 和 Fragment 中使用生命周期感知的协程作用域,并同时设置 Job 和 Dispatcher。

// Activity 中使用 lifecycleScope
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // lifecycleScope 自动绑定到 LifecycleOwner (Activity)
        lifecycleScope.launch(Dispatchers.IO) {
            // 模拟权限请求前的耗时操作
            val result = doHeavyWork()
            withContext(Dispatchers.Main) {
                // 回到主线程更新 UI
                showPermissionResult(result)
            }
        }
    }
}

// Fragment 中使用 viewModelScope
class PermissionFragment : Fragment() {
    private val viewModel: PermissionViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // viewModelScope 与 ViewModel 同生命周期
        viewModelScope.launch(Dispatchers.Default) {
            val status = requestPermission()
            // 处理权限结果
        }
    }
}
关键点:lifecycleScope 在 onDestroy 时自动取消;viewModelScope 在 ViewModel.onCleared() 时取消。无需手动管理 Job 的取消。

异常处理与重试策略

即使在作用域内部,协程仍可能抛出异常(如权限被拒绝、网络超时)。使用 CoroutineExceptionHandler 捕获未处理异常,并结合 retry 模式实现稳定。

// 定义全局异常处理器
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    Log.e("Coroutine", "Unhandled exception: ${throwable.message}")
}

// 带重试的协程启动
fun launchWithRetry(scope: CoroutineScope, block: suspend () -> Unit) {
    scope.launch(exceptionHandler) {
        retry(3) { // 最多重试3次
            block()
        }
    }
}

// 使用示例:在生命周期感知的作用域内重试权限请求
lifecycleScope.launch(exceptionHandler) {
    retry(2) {
        val permission = requestSinglePermission(Manifest.permission.CAMERA)
        if (!permission) throw RuntimeException("Permission denied")
    }
}

协程作用域生命周期绑定流程图

下图直观展示了 Android 组件(Activity)通过生命周期回调驱动协程作用域的创建与取消,以及 Job、Dispatcher 在其中的角色。

协程作用域生命周期绑定流程 Android组件 (Activity/Fragment) onCreate / onStart / onResume 创建 CoroutineScope lifecycleScope / viewModelScope Job + Dispatcher launch / async 指定 协程执行中 权限请求 / 耗时任务 onDestroy / onStop 作用域自动 cancel 所有子 Job 被取消 CoroutineExceptionHandler 说明:被取消的协程会抛出 CancellationException,由 CoroutineExceptionHandler 捕获

常见错误与避坑指南

  • 在已销毁的组件中启动全局作用域:使用 GlobalScope 会导致协程脱离组件生命周期,即使 Activity 已销毁仍继续运行。应始终使用 lifecycleScope 或 viewModelScope。
  • 忘记取消自定义 Job:若手动创建 Job 并存入字段,必须在 onDestroy 中显式调用 job.cancel(),否则会导致泄漏。
  • 在 ViewModel 外使用 viewModelScope:viewModelScope 只能在 ViewModel 内部使用;若在 Fragment/Activity 中使用 viewModelScope 会导致编译错误。应使用 lifecycleScope。
  • 主线程阻塞:避免在 Dispatchers.Main 中执行耗时操作,应切换至 IO 或 Default。

调试与性能优化

调试:在 build.gradle 中添加以下依赖,可在 Android Studio 的 Coroutines Debugger 中查看协程状态:

debugImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"
debugImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"

通过 println("Coroutine: ${Thread.currentThread().name}") 或日志标签快速定位协程所在线程。

性能优化:

  • 对于 I/O 密集型任务(如网络、文件读写),使用 Dispatchers.IO,其线程池会根据需求动态扩展。
  • 对于 CPU 密集型计算,使用 Dispatchers.Default,核心线程数等于 CPU 核数。
  • 避免过度使用 Dispatchers.Main.immediate,只在确实需要立即恢复主线程时使用。
注意:Dispatchers.Unconfined 不推荐在 Android 中使用,因为它可能在任何线程执行,导致难以预测的内存行为。