从前台服务启动的协程已暂停

问题描述 投票:0回答:1

我每秒更新一条通知。

@AndroidEntryPoint
class StopwatchService : Service() {
    @Inject
    lateinit var stopwatch: Stopwatch

    // scope
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job) // which dispatcher to use?

    // notifications
    private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }

    private val notificationBuilder by lazy {
        createNotificationBuilder(title = stopwatch.laps.size.let { if (it > 0) "Lap ${it + 1}" else "" })
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        intent?.action.let {
            when (it) {              
                Actions.START_SERVICE.name -> {
                    startForeground(
                        ONGOING_NOTIFICATION_ID, notificationBuilder.build()
                    )

                    scope.launch {
                        Log.d("stopwatch-service", "state LAUNCH")

                        stopwatch.eventFlow.collectLatest {
                            Log.d("stopwatch-service", "state ${it.toString()}")

                            when (it) {
                                Stopwatch.Event.START -> {
                                    notificationBuilder.clearActions()
                                    ...
                                }

                                Stopwatch.Event.STOP -> {
                                    notificationBuilder.clearActions()
                                    ...
                                }

                                Stopwatch.Event.RESET -> {
                                    stopForeground(STOP_FOREGROUND_REMOVE)
                                    stopSelf()
                                }
                            }
                        }
                    }

                    scope.launch {
                        Log.d("stopwatch-service", "update LAUNCH")

                        stopwatch.elapsedTimeFlow(
                            refreshRate = 100,
                            precision = 1000,
                            format = ::formatNotification,
                            uiScope = scope,
                            updateUI = false
                        ).collectLatest {
                            Log.d("stopwatch-service", "update $it")
                            notificationManager.notify(
                                ONGOING_NOTIFICATION_ID,
                                notificationBuilder.apply { setContentText(it) }.build()
                            )
                        }
                    }
                }

            }
        }
        Log.d("stopwatch-service", "onStartCommand ${intent.toString()}")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("stopwatch-service", "onDestroy")
        job.cancel()
    }
state LAUNCH
onStartCommand Intent { act=START_SERVICE cmp=com.uzential.stopwatch/.services.StopwatchService }
update LAUNCH
state START
update 00:01
update 00:02
update 00:03
...
update 00:28
update 00:29
update 00:30
update 00:31
update 01:48
update 01:49
update 01:50
update 01:51
...
update 02:06
update 02:07
update 02:08
update 02:09
update 04:08
update 04:09
update 04:10
update 04:11

即使通知可见,协程也会暂停一分钟(直到我按下操作按钮)。

基于this评论,我在应用程序信息->电池使用中启用了“允许后台活动”,这似乎解决了问题。我很惊讶这必须为前台服务完成。更新通知费用高吗? (

notificationManager.notify
只是更改内容文本字段,但在通知可见之前不会执行任何 UI 计算)

我使用

Dispatchers.Default
来避免使用主线程。

fun Stopwatch.elapsedTimeFlow(
    uiScope: CoroutineScope,
    format: (millis: Long) -> String,
    refreshRate: Long = 100,
    precision: Long = 1000,
    updateUI : Boolean = true,
    dispatcher: CoroutineDispatcher = Dispatchers.Default
): Flow<String> {
    // flow
    val elapsedTimeFlow = MutableStateFlow(elapsedTime)
    suspend fun refreshLoop() {
        while (true) {
            elapsedTimeFlow.value = elapsedTime
            delay(refreshRate)
        }
    }
    // listen to changes to isRunning
    uiScope.launch(dispatcher) {
        val job = AtomicReference<Job?>(null)
        eventFlow.collectLatest {
            when (it) {
                // launch refresh loop
                Stopwatch.Event.START -> {
                    launch {
                        refreshLoop()
                    }.also(job::set)
                }
                // cancel refresh loop
                Stopwatch.Event.STOP, Stopwatch.Event.RESET -> {
                    job.get()?.cancel()
                    // update UI one last time
                    if(updateUI) elapsedTimeFlow.emit(elapsedTime)
                }
            }
        }
    }
    // UI flow
    return elapsedTimeFlow
        .map { it / precision }
        .distinctUntilChanged()
        .map { format(elapsedTime) }
        .flowOn(dispatcher)
}

电池优化无法以编程方式禁用。是否还有其他可以做的事情,例如仅在通知不可见时才允许暂停协程?

编辑:简单的日志也会发生同样的事情。

Actions.START_SERVICE.name -> {
                    startForeground(
                        ONGOING_NOTIFICATION_ID, notificationBuilder.build()
                    )

                    scope.launch {
                        while(true){
                            Log.d("stopwatch-service", "LOG ${stopwatch.elapsedTime}")
                            delay(1000)
                        }
LOG 28559
LOG 29565
LOG 161978 (after clicking on the notification)

编辑 2:计时器也停止。

                    val timerTask = object : TimerTask() {
                        override fun run() {
                            Log.d("stopwatch-service", "LOG ${stopwatch.elapsedTime}")
                        }
                    }
                    timer.schedule(timerTask, 0, 1000)
LOG 28816
LOG 29817
LOG 46124 (clicked on notification)
android android-service kotlin-coroutines
1个回答
0
投票

前台服务本身似乎无论如何都会继续运行,所以我做了这样的解决方案:

    override fun onCreate() {
    ...
    Thread {
        run {
            // Hack to attempt to restart cancelled routines due to Android's unconfigurable battery management
            while (true) {
                Thread.sleep(10000)
                routineMap.forEach { (_, job) ->
                    if(job.isCancelled) {
                        job.start()
                    }
                }
            }
        }
    }.start();
    ...
    }

前台服务启动一个线程,该线程只是重新启动被取消的作业。看起来效果很好。唯一需要注意的是,您需要跟踪您希望继续运行的作业。

© www.soinside.com 2019 - 2024. All rights reserved.