第2章:Service中Scope生命周期绑定
2.1 原理:为什么需要生命周期绑定?
协程Scope的生命周期必须与Service的生命周期对齐,否则会产生两个严重问题:
- Scope泄露:Service销毁后,其内部的协程任务依然存活,继续消耗线程、内存等资源,导致内存泄漏或后台任务失控。
- 任务悬空:Service已停止,但协程仍尝试访问已释放的依赖,引发异常或错误状态。
正确的做法是:在Service创建时初始化一个CoroutineScope,在Service销毁时调用scope.cancel()取消所有子协程。这遵循结构化并发原则,确保协程的取消范围与组件生命周期一致。
核心概念:每个Service实例应当拥有一个独立的CoroutineScope,该Scope的Job作为所有协程的根Job。取消根Job会级联取消所有子协程。
2.2 生命周期绑定流程
下图展示了Service从启动到销毁过程中,Scope的创建与取消时机:
2.3 标准实现流程
- 在Service类的
onCreate()或onStartCommand()中创建CoroutineScope,推荐使用SupervisorJob()+Dispatchers.IO(或其他合适的调度器)。 - 在Service内启动子协程时,使用
scope.launch或scope.async,确保所有任务都属于该Scope。 - 在
onDestroy()中调用scope.cancel()取消整个Scope,并立即释放持有的资源。 - 避免在
onDestroy()之后继续持有Scope引用或在协程中执行回调。
最佳实践:将Scope声明为
private lateinit var scope: CoroutineScope,确保仅在Service实例内可见,不被外部误用。
2.4 代码示例:Android Service中的Scope管理
class MyService : Service() {
private lateinit var scope: CoroutineScope
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 创建Scope(如果尚未创建)
if (!::scope.isInitialized) {
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
// 启动后台任务
scope.launch {
val result = fetchDataFromNetwork()
// 更新UI或保存结果(注意主线程转换)
withContext(Dispatchers.Main) {
updateUI(result)
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
if (::scope.isInitialized) {
scope.cancel() // 取消所有子协程
}
}
}
2.5 调试:检测Scope泄露
使用以下方法判断Scope是否被正确取消:
- Android Studio Coroutine Debugger:在
onDestroy()中断点,查看Scope中活跃协程数量。若不为0,说明存在未取消的子协程。 - 日志标记:在协程
finally块中打印日志,确认所有协程都执行了取消逻辑。 - LeakCanary:如果Service实例被意外保留,LeakCanary会提示内存泄漏,排查是否为Scope引用导致。
// 调试日志示例
scope.launch {
try {
// 任务逻辑
} finally {
Log.d("CoroutineLifecycle", "协程已取消,Service: ${this@MyService}")
}
}
2.6 常见错误与规避
| 错误场景 | 后果 | 正确做法 |
|---|---|---|
使用GlobalScope.launch启动任务 | Scope与Service解耦,Service销毁后任务仍在运行,造成泄露 | 始终使用自定义Scope |
未在onDestroy()中调用scope.cancel() | Service销毁后协程持续运行,导致内存泄漏或异常 | 在onDestroy()中显式取消 |
使用Job()而非SupervisorJob() | 子协程失败导致父Job和兄弟协程被取消 | 使用SupervisorJob()隔离失败 |
| 在协程中持有Activity/Context引用 | Service销毁后Context可能无效,访问时抛异常 | 协程内使用WeakReference或检查生命周期状态 |
危险操作:避免在
onDestroy()之后仍然调用scope.cancel(),会导致CancellationException被吞没但资源未释放。正确的做法是在onDestroy()内部一次取消即可。
2.7 性能与稳定性优化
- 使用SupervisorJob:每个子协程的失败不会影响其他兄弟协程,适合Service中独立的任务(如多个API请求互不依赖)。
- 取消策略:在
scope.cancel()后,可以调用scope.coroutineContext[Job]?.children?.forEach { it.cancel() }实现更细粒度的取消,但通常不用,因为直接取消父Job即可。 - 避免重复创建Scope:在
onStartCommand中判断scope是否已初始化,防止多次创建导致Scope泄露(旧Scope未取消)。 - 清理非协程资源:在取消Scope后,手动释放如数据库连接、Socket等资源,可在Service的
stopSelf()之前统一处理。
// 优化后的Scope创建(附带错误处理)
private fun ensureScope(): CoroutineScope {
if (!::scope.isInitialized || scope.isActive.not()) {
// 如果已有Scope但已取消,则重新创建
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
}
return scope
}
通过以上步骤,可以确保Service中的协程Scope生命周期完全与Service绑定,避免泄露并提升应用稳定性。