Android 别三日,当 deprecated 相待。
我当时写程序的时候有这种需求,希望某一段协程在某一个生命周期开始(下文假设生命周期为 onStart)进行,而且就执行一次。如果执行一次的话,单走一个lifecycleScope.launch
其实也行,但并不安全。我去谷歌查阅了资料,又受到 IDE 提示的影响,了解到了“神奇”的lifecycleScope.launchWhenStarted
,这个函数恰好满足我的需求。可是贴上去发现,弃用了!
好家伙,让我去用repeatOnLifecycle
?我心想这不对啊,我要执行一次,而这个函数的重点是repeat
,还是不符合我要求。
我又转向了LifecycleOwner::whenStarted
,该函数跟上面那个函数作用是一样的,只不过作用域不太一样。我想看看他是让你怎么做的。
好家伙,让我去用withStarted
?但是withStarted
里面只能放 non-suspending work(非挂起任务),这还是不符合我的要求啊。
其实如果你不怎么在乎警告,而且这种代码并没有给你的软件带来多大损失,你可以继续这么用着。但是在这个函数受众面这么广泛的情况下,为什么官方这么执意把它删除呢?
repeatOnLifecycle 的问题
优势
我们可能都看过一张比较优势图:
可以看到,repeatOnLifecycle
会在每次重复时都从头开始执行协程,并在生命周期状态降低到指定状态以下时取消。对于收集大多数流来说,它非常适合,因为在不需要时能完全取消流,从而节省了资源。
而 launchWhenX
系列函数不会取消协程并重新启动。它只会推迟启动时间,并在生命周期状态低于指定状态时挂起。
举一个简单的例子,
delay(5000)Log.d("test", "test")delay(5000) Log.d("test", "test")delay(5000) Log.d("test", "test")
如果被repeatOnLifecycle(Lifecycle.State.STARTED)
包裹起来,且 1s 后返回主界面,该挂起函数会立即取消,因为取消了,所以你再等 4s,也不会有任何反应。你返回程序再等 5s 后,log 信息会出现在控制台中,因为重新执行了。
如果被launchWhenStarted
包裹起来,且 1s 后返回主界面,该挂起函数会立即挂起,因为挂起了,所以你再等 4s,也不会有任何反应,但是它还是一直在后台进行着 delay 的。当你过了 4s 多打开程序后,log 信息会立即出现在控制台中,因为挂起恢复了。
详细例子就不从这里细说了,可以参考一下这篇文章:使用更为安全的方式收集 Android UI 数据流
局限
我们从这篇文章也可以看到,这里面处理的是一个位置信息。位置信息,我们自然需要最新的,而且后台把该流取消掉,然后返回后重新创建也是可以接受的。
弃用launchWhenX
方法,基本就是为了有效遏制挂起函数在后台还在运行,导致资源浪费甚至程序崩溃。
但是实际的需求是无穷无尽的,假如需要显示一个一次性 SnackBar,或者打开一个一次性 window,这些需求如果用repeatOnLifecycle
,让我想起了LiveData
当年的痛,有点类似处理粘性事件了,但又不太一样。
把 repeatOnLifecycle 用成 launchWhenX 的样子
官方在 Alternative APIs for running one-time suspend code when lifecycle state is at least X? 中提供了三种解决方案,适用于不同需求
-
让挂起的代码继续运行至结束
这样做的好处是整个代码块以原子方式运行,也就是说不会在中途被取消。
lifecycleScope.launch {// 直到 STARTED 前一直挂起withStarted { }// 之前你写在 launchWhenStarted 中的代码写到这下面doYourOneTimeWork()// 注意:即使生命周期低于 STARTED 对应的 onStop,代码也将继续运行,// 这个和 launchWhenStarted 还不太一样!}lifecycleScope.launch { // 直到 STARTED 前一直挂起 withStarted { } // 之前你写在 launchWhenStarted 中的代码写到这下面 doYourOneTimeWork() // 注意:即使生命周期低于 STARTED 对应的 onStop,代码也将继续运行, // 这个和 launchWhenStarted 还不太一样! }
lifecycleScope.launch { // 直到 STARTED 前一直挂起 withStarted { } // 之前你写在 launchWhenStarted 中的代码写到这下面 doYourOneTimeWork() // 注意:即使生命周期低于 STARTED 对应的 onStop,代码也将继续运行, // 这个和 launchWhenStarted 还不太一样! }
这种类型的代码假设你的一次性任务不依赖于保持在某个生命周期状态以上,但这是一个库无法知道的情况。举个例子,如果在挂起任务之后运行了一个 FragmentTransaction,那么在该调用发生之前可能会保存状态。
-
取消挂起的代码,并在返回到该状态以上时重新启动它
每次回到给定的生命周期状态以上时再重新运行代码块确实是
repeatOnLifecycle
的规定。然而,对于一个库来说,是无法知道是否可以安全地重新运行整个代码块,或者是否需要更频繁地进行检查点操作的。简单来说,如果可以多次运行整个代码块直到成功完成,那么只需要用一个
isComplete
来标记:lifecycleScope.launch {var isComplete = falserepeatOnLifecycle(Lifecycle.State.STARTED) {if (!isComplete) {// 之前你写在 launchWhenStarted 中的代码写到这下面// 如果在运行时生命周期走向 STARTED 对应的 onStop 时会被直接取消doYourOneTimeWork()// 标记是否成功isComplete = true}}}lifecycleScope.launch { var isComplete = false repeatOnLifecycle(Lifecycle.State.STARTED) { if (!isComplete) { // 之前你写在 launchWhenStarted 中的代码写到这下面 // 如果在运行时生命周期走向 STARTED 对应的 onStop 时会被直接取消 doYourOneTimeWork() // 标记是否成功 isComplete = true } } }
lifecycleScope.launch { var isComplete = false repeatOnLifecycle(Lifecycle.State.STARTED) { if (!isComplete) { // 之前你写在 launchWhenStarted 中的代码写到这下面 // 如果在运行时生命周期走向 STARTED 对应的 onStop 时会被直接取消 doYourOneTimeWork() // 标记是否成功 isComplete = true } } }
当然不能保证如果中途被取消,重新运行该代码块就一定是安全的。
-
取消挂起的代码,不重新启动
这个跟上一个类似,但是这个无论完没完成,都会在 finally 块中设置
isComplete
,让它彻底取消。lifecycleScope.launch {var isComplete = falserepeatOnLifecycle(Lifecycle.State.STARTED) {if (!isComplete) {try {// 之前你写在 launchWhenStarted 中的代码写到这下面// 如果在运行时生命周期走向 STARTED 对应的 onStop 时会被直接取消doYourOneTimeWork()} finally {// 即使一次性任务运行了一半没运行完,也当它运行完了,// 并且在 finally 块中标记该任务已完成,所以该代码块不会重启。isComplete = true}}}}lifecycleScope.launch { var isComplete = false repeatOnLifecycle(Lifecycle.State.STARTED) { if (!isComplete) { try { // 之前你写在 launchWhenStarted 中的代码写到这下面 // 如果在运行时生命周期走向 STARTED 对应的 onStop 时会被直接取消 doYourOneTimeWork() } finally { // 即使一次性任务运行了一半没运行完,也当它运行完了, // 并且在 finally 块中标记该任务已完成,所以该代码块不会重启。 isComplete = true } } } }
lifecycleScope.launch { var isComplete = false repeatOnLifecycle(Lifecycle.State.STARTED) { if (!isComplete) { try { // 之前你写在 launchWhenStarted 中的代码写到这下面 // 如果在运行时生命周期走向 STARTED 对应的 onStop 时会被直接取消 doYourOneTimeWork() } finally { // 即使一次性任务运行了一半没运行完,也当它运行完了, // 并且在 finally 块中标记该任务已完成,所以该代码块不会重启。 isComplete = true } } } }
五种方式怎么选?
我不太懂 PS,后面几个用画图软件 P 的,能看懂意思就行
首先遵循以下的 lifecycle 活动:
launchWhenStarted
不安全,且已被弃用,但可以后台加载,最重要的是一次性
repeatOnLifecycle(Lifecycle.State.STARTED)
安全,但可能产生粘性事件
解决方案1
不安全,谨慎使用
注意和
lifecycleScope.launch
的区别
解决方案2
安全,可以说是替代
launchWhenStarted
的较优方案。缺点是不能后台加载
解决方案3
安全,但可能没处理完就结束了,不太适用于界面处理