如何通过协程实现循环等待任务完成后再启动下一轮周期执行?
问题:实现8小时内等待任务完成后再循环执行的协程逻辑
需要封装一个持续8小时的while循环,要求当前任务执行完毕后再启动下一轮,不能依赖固定延迟时间。
原实现代码
fun startPeriodicTask(timeout: Long, block: suspend CoroutineScope.() -> Unit) { periodicTask = viewModelScope.launcher { while (dataService.timeConfigData.runTime.value!!.toLong() < dataService.timeConfigData.timeEnd.value!!.toLong()) { withTimeout(timeout) { // 执行任务 XLog.v("startPeriodicTask run block") block() } delay(timeout) } } }
原调用代码
vm.startPeriodicTask(2000) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mVibrator?.vibrate(VibrationEffect.createOneShot(1000, 255)) } else { mVibrator?.vibrate(longArrayOf(1000, 2000), 1) } }
原方案存在问题:协程的挂起机制没被正确利用,既无法实现等待任务完成的效果,还可能阻塞主线程。
修改后的代码及新问题
按方法修改后,未实现等待逻辑,while循环短时间内疯狂重复执行,任务被频繁触发:
fun startPeriodicTask(timeout: Long, block:() -> Unit) { // periodicTask = viewModelScope.launcher { while (dataService.timeConfigData.runTime.value!!.toLong() < dataService.timeConfigData.timeEnd.value!!.toLong()) { XLog.v("startPeriodicTask run block") block() // delay(timeout) // } } } vm.dataService.testConfigData.isSpeakerSwitch.observe(viewLifecycleOwner) { if (it) { vm.startPeriodicTask(25000) { play { XLog.d("play end") } } } } fun play(onCompleted: () -> Unit) { stop() mMediaPlayer = MediaPlayer.create(requireContext(), R.raw.testsignal) mMediaPlayer?.setOnCompletionListener(OnCompletionListener { stop() onCompleted() }) mMediaPlayer?.start() }

解决方案
核心是将基于回调的异步任务转为挂起函数,配合协程实现"任务完成后再循环"的逻辑:
1. 把play转为挂起函数
利用suspendCancellableCoroutine将回调逻辑包装成挂起函数,让协程等待播放完成:
suspend fun play() { stop() return suspendCancellableCoroutine { cont -> mMediaPlayer = MediaPlayer.create(requireContext(), R.raw.testsignal) // 播放完成时恢复协程 mMediaPlayer?.setOnCompletionListener { stop() cont.resume(Unit) } // 播放出错时抛出异常 mMediaPlayer?.setOnErrorListener { _, _, _ -> stop() cont.resumeWithException(RuntimeException("MediaPlayer播放失败")) true } mMediaPlayer?.start() // 协程取消时释放资源 cont.invokeOnCancellation { stop() } } }
2. 修复循环逻辑
将循环放在协程内执行,确保任务完成后再进入下一轮:
fun startPeriodicTask() { periodicTask?.cancel() // 先取消旧任务,避免重复运行 periodicTask = viewModelScope.launch { // 用空安全替代强制非空,避免空指针 val runTime = dataService.timeConfigData.runTime.value?.toLong() ?: 0 val timeEnd = dataService.timeConfigData.timeEnd.value?.toLong() ?: 0 while (runTime < timeEnd) { XLog.v("startPeriodicTask run block") try { play() // 等待播放完成后才会继续循环 } catch (e: Exception) { XLog.e("任务执行失败", e) } // 实时更新时间判断条件,避免循环用初始值一直执行 val currentRunTime = dataService.timeConfigData.runTime.value?.toLong() ?: 0 if (currentRunTime >= timeEnd) break } } }
3. 调整调用逻辑
vm.dataService.testConfigData.isSpeakerSwitch.observe(viewLifecycleOwner) { isOn -> if (isOn) { vm.startPeriodicTask() } else { vm.periodicTask?.cancel() // 关闭时取消循环任务 } }
关键说明
- 回调转挂起函数是协程处理异步等待的标准方式,确保任务完成前协程处于挂起状态,不会占用线程资源。
- 循环放在协程内执行,完全非阻塞,不会影响主线程。
- 增加空安全和错误处理,避免程序崩溃。
内容的提问来源于stack exchange,提问作者乐于助人的王老师
相关产品推荐
相关产品推荐

