第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的创建与取消时机:

Service与Scope生命周期绑定 Service.onStartCommand 创建CoroutineScope 启动子协程任务 Service.onDestroy scope.cancel() 协程级联取消 + 清理 注意点 • 创建Scope使用SupervisorJob • 取消必须在主线程调用 • 避免在onDestroy后使用scope

2.3 标准实现流程

  1. 在Service类的onCreate()或onStartCommand()中创建CoroutineScope,推荐使用SupervisorJob() + Dispatchers.IO(或其他合适的调度器)。
  2. 在Service内启动子协程时,使用scope.launch或scope.async,确保所有任务都属于该Scope。
  3. 在onDestroy()中调用scope.cancel()取消整个Scope,并立即释放持有的资源。
  4. 避免在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绑定,避免泄露并提升应用稳定性。