从 Job 树开始彻底讲透 Kotlin 协程的生命周期管理机制上一篇我们讲了《CoroutineContext 到底是什么为什么 Job 和 Dispatcher 可以直接相加》最终得出了一个重要结论CoroutineContext本质上是一个特殊的Map而Dispatcher Job CoroutineName ExceptionHandler都只是 Context 中不同的配置项。其中最重要的一个配置Job很多同学天天写job.cancel() viewModelScope.launch { } SupervisorJob()但从来没认真思考过协程为什么能取消父协程为什么能取消子协程SupervisorJob为什么不会连坐结构化并发到底是什么这一篇 我们就彻底讲透 Kotlin 协程最核心的设计之一Job一、先看一个最简单的例子val job CoroutineScope( Dispatchers.IO ).launch { delay(10000) Log.d(TAG, 执行完成) }然后job.cancel()结果协程停止执行很多人的理解停留在cancel()取消协程但问题来了它凭什么取消线程都不能随便停。为什么协程可以二、协程为什么能取消答案其实很简单协程本身不会被强杀很多人第一次听到都会愣一下。实际上cancel() 并不会杀死协程它只是发送取消信号例如job.cancel()本质Job状态 ↓ Cancelled协程自己看到我已经被取消了然后主动结束。三、Job 本质上是什么很多人以为Job是协程对象其实不是。更准确地说Job 协程生命周期管理器负责记录是否启动 是否完成 是否取消 父子关系四、Job 有哪些状态官方状态图比较复杂。实际项目理解这几个就够了。New ↓ Active ↓ Completing ↓ Completed或者New ↓ Active ↓ Cancelling ↓ Cancelled例如launch { }启动后Active调用cancel()变成Cancelling然后Cancelled五、协程为什么要有 Job如果没有 Job协程启动了你怎么知道还活着怎么知道完成了怎么知道取消了根本没法管理。所以Job 负责管理协程生命周期六、真正牛逼的地方来了Kotlin 协程最经典的设计Job树例如viewModelScope.launch { launch { } launch { } launch { } }结构Parent Job │ ├── Child Job 1 │ ├── Child Job 2 │ └── Child Job 3七、父协程为什么能取消子协程因为父Job 持有 子Job所以parentJob.cancel()相当于通知所有孩子 收工了于是Child1 Cancel Child2 Cancel Child3 Cancel全部结束。八、为什么 ViewModel 被销毁后协程自动结束因为viewModelScope内部SupervisorJob()作为根节点。结构ViewModel │ └── SupervisorJob │ ├── launch1 │ ├── launch2 │ └── launch3当onCleared()执行scope.cancel()时整棵树全部取消这就是生命周期感知的来源。九、问题来了为什么 Google 不允许这样GlobalScope.launch { }因为没有父Job结构GlobalScope │ └── launch彻底脱离生命周期。于是页面退出 协程还在跑容易内存泄漏所以Google一直不推荐GlobalScope十、一个孩子崩了会发生什么例如CoroutineScope(Job()).launch { launch { throw RuntimeException() } launch { delay(10000) Log.d(TAG, 我还能执行吗) } }答案不能因为一个孩子异常 ↓ 父Job取消 ↓ 所有孩子取消结构Parent │ ├── Child1 崩溃 │ └── Child2 连坐十一、这就是普通 Job普通Job()遵循一荣俱荣 一损俱损例如一个孩子挂了 全家陪葬十二、SupervisorJob 为什么出现Google发现很多场景不合理。例如网络请求A 网络请求B 网络请求CA失败为什么B和C也要死不合理。于是SupervisorJob()出现了。十三、SupervisorJob 的核心思想结构Parent │ ├── Child1 崩溃 │ ├── Child2 正常 │ └── Child3 正常特点孩子之间互不影响例如A失败 B继续 C继续这也是viewModelScope选择SupervisorJob的原因。十四、终于理解结构化并发很多教程喜欢说Structured Concurrency然后大家一脸懵。其实一句话协程必须有父亲例如Activity ↓ viewModelScope ↓ launch ↓ launch ↓ launch形成Job树这样页面退出 整棵树一起结束不会出现孤儿协程十五、为什么 repeatOnLifecycle 这么安全因为repeatOnLifecycle()每次STARTED创建新的子Job离开STOPPED取消这个子Job整个过程都挂在LifecycleScope下面。这也是Flow收集安全的根本原因。十六、最终总结如果让我一句话解释Job我会这样说Job不是协程。 Job是协程生命周期管理器。负责启动 完成 取消 父子关系普通Job()特点一个孩子挂 全家陪葬而SupervisorJob()特点一个孩子挂 其它继续工作而所谓结构化并发本质上就是用Job树管理协程生命周期让协程永远有归属。下篇预告既然Job 负责生命周期那么Dispatchers 到底负责什么 为什么IO和Default要分开 launch(IO) withContext(IO) 真的只是切线程吗下一篇我们继续《Kotlin 协程设计思想三Dispatchers 到底是什么切线程真的只是切线程吗》从线程池开始彻底讲透 Dispatchers.IO、Dispatchers.Default、Dispatchers.Main 的设计思想。
Kotlin 协程设计思想(二):Job 到底是什么?为什么协程能被取消?
从 Job 树开始彻底讲透 Kotlin 协程的生命周期管理机制上一篇我们讲了《CoroutineContext 到底是什么为什么 Job 和 Dispatcher 可以直接相加》最终得出了一个重要结论CoroutineContext本质上是一个特殊的Map而Dispatcher Job CoroutineName ExceptionHandler都只是 Context 中不同的配置项。其中最重要的一个配置Job很多同学天天写job.cancel() viewModelScope.launch { } SupervisorJob()但从来没认真思考过协程为什么能取消父协程为什么能取消子协程SupervisorJob为什么不会连坐结构化并发到底是什么这一篇 我们就彻底讲透 Kotlin 协程最核心的设计之一Job一、先看一个最简单的例子val job CoroutineScope( Dispatchers.IO ).launch { delay(10000) Log.d(TAG, 执行完成) }然后job.cancel()结果协程停止执行很多人的理解停留在cancel()取消协程但问题来了它凭什么取消线程都不能随便停。为什么协程可以二、协程为什么能取消答案其实很简单协程本身不会被强杀很多人第一次听到都会愣一下。实际上cancel() 并不会杀死协程它只是发送取消信号例如job.cancel()本质Job状态 ↓ Cancelled协程自己看到我已经被取消了然后主动结束。三、Job 本质上是什么很多人以为Job是协程对象其实不是。更准确地说Job 协程生命周期管理器负责记录是否启动 是否完成 是否取消 父子关系四、Job 有哪些状态官方状态图比较复杂。实际项目理解这几个就够了。New ↓ Active ↓ Completing ↓ Completed或者New ↓ Active ↓ Cancelling ↓ Cancelled例如launch { }启动后Active调用cancel()变成Cancelling然后Cancelled五、协程为什么要有 Job如果没有 Job协程启动了你怎么知道还活着怎么知道完成了怎么知道取消了根本没法管理。所以Job 负责管理协程生命周期六、真正牛逼的地方来了Kotlin 协程最经典的设计Job树例如viewModelScope.launch { launch { } launch { } launch { } }结构Parent Job │ ├── Child Job 1 │ ├── Child Job 2 │ └── Child Job 3七、父协程为什么能取消子协程因为父Job 持有 子Job所以parentJob.cancel()相当于通知所有孩子 收工了于是Child1 Cancel Child2 Cancel Child3 Cancel全部结束。八、为什么 ViewModel 被销毁后协程自动结束因为viewModelScope内部SupervisorJob()作为根节点。结构ViewModel │ └── SupervisorJob │ ├── launch1 │ ├── launch2 │ └── launch3当onCleared()执行scope.cancel()时整棵树全部取消这就是生命周期感知的来源。九、问题来了为什么 Google 不允许这样GlobalScope.launch { }因为没有父Job结构GlobalScope │ └── launch彻底脱离生命周期。于是页面退出 协程还在跑容易内存泄漏所以Google一直不推荐GlobalScope十、一个孩子崩了会发生什么例如CoroutineScope(Job()).launch { launch { throw RuntimeException() } launch { delay(10000) Log.d(TAG, 我还能执行吗) } }答案不能因为一个孩子异常 ↓ 父Job取消 ↓ 所有孩子取消结构Parent │ ├── Child1 崩溃 │ └── Child2 连坐十一、这就是普通 Job普通Job()遵循一荣俱荣 一损俱损例如一个孩子挂了 全家陪葬十二、SupervisorJob 为什么出现Google发现很多场景不合理。例如网络请求A 网络请求B 网络请求CA失败为什么B和C也要死不合理。于是SupervisorJob()出现了。十三、SupervisorJob 的核心思想结构Parent │ ├── Child1 崩溃 │ ├── Child2 正常 │ └── Child3 正常特点孩子之间互不影响例如A失败 B继续 C继续这也是viewModelScope选择SupervisorJob的原因。十四、终于理解结构化并发很多教程喜欢说Structured Concurrency然后大家一脸懵。其实一句话协程必须有父亲例如Activity ↓ viewModelScope ↓ launch ↓ launch ↓ launch形成Job树这样页面退出 整棵树一起结束不会出现孤儿协程十五、为什么 repeatOnLifecycle 这么安全因为repeatOnLifecycle()每次STARTED创建新的子Job离开STOPPED取消这个子Job整个过程都挂在LifecycleScope下面。这也是Flow收集安全的根本原因。十六、最终总结如果让我一句话解释Job我会这样说Job不是协程。 Job是协程生命周期管理器。负责启动 完成 取消 父子关系普通Job()特点一个孩子挂 全家陪葬而SupervisorJob()特点一个孩子挂 其它继续工作而所谓结构化并发本质上就是用Job树管理协程生命周期让协程永远有归属。下篇预告既然Job 负责生命周期那么Dispatchers 到底负责什么 为什么IO和Default要分开 launch(IO) withContext(IO) 真的只是切线程吗下一篇我们继续《Kotlin 协程设计思想三Dispatchers 到底是什么切线程真的只是切线程吗》从线程池开始彻底讲透 Dispatchers.IO、Dispatchers.Default、Dispatchers.Main 的设计思想。