You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

求助:解决Context.startForegroundService()未调用Service.startForeground()崩溃问题

排查Context.startForegroundService()未调用startForeground()的偶发崩溃问题

咱们来拆解下你遇到的这个偶发崩溃问题——虽然你已经做了不少优化,但从日志和代码来看,还有几个潜在的问题点可能导致这个崩溃反复出现。

先从日志和崩溃栈分析现状

从你提供的日志来看:

2019-07-29 21:41:27,146 [[BaseOverlayService:62 onCreate]]: onCreate
2019-07-29 21:41:27,146 [[BaseOverlayService:142 b]]: BEFORE moveToForeground (called by onCreate)
2019-07-29 21:41:27,152 [[BaseOverlayService:159 b]]: AFTER moveToForeground (called by onCreate) - moved to foreground: true
2019-07-29 21:41:27,176 [[BaseOverlayService:79 onStartCommand]]: onStartCommand: isForeground: true | action: null | isStopping: false
2019-07-29 21:41:27,945 [[BaseOverlayService:142 b]]: BEFORE moveToForeground (called by updateNotification [OverlayService [onInitFinished]])
2019-07-29 21:41:27,947 [[BaseOverlayService:159 b]]: AFTER moveToForeground (called by updateNotification [OverlayService [onInitFinished]]) - moved to foreground: false

服务明明已经在onCreate阶段成功调用startForeground()并标记为前台状态,但还是触发了RemoteServiceException。这说明系统可能收到了多次startForegroundService()请求,但其中某次请求对应的startForeground()没有被系统检测到

代码中的潜在问题点

1. 重复调用startForegroundService()而非startService()

sendAction方法中,你对已运行的服务仍然使用ContextCompat.startForegroundService()

val intent = Intent(context, T::class.java)
intent.action = action
intentUpdater(intent)
ContextCompat.startForegroundService(context, intent)

根据Android的规则:

  • startForegroundService()仅用于启动新服务,并且要求服务在5秒内调用startForeground()
  • 服务已经运行时,应该使用startService()来传递指令,不需要再次触发前台服务的强制检测

如果对已处于前台的服务调用startForegroundService(),系统会再次启动一个“前台服务检测流程”,但此时你的代码因为isForegroundtrue不会调用startForeground(),这就会触发系统的崩溃检测。

2. isForeground变量非线程安全

isForeground是普通的布尔变量,没有任何线程同步措施:

protected var isForeground = false
private set

当多个线程(比如主线程和系统服务线程)同时访问这个变量时,可能出现竞态条件:比如服务刚调用startForeground()但还没更新isForeground,此时新的onStartCommand触发,代码会认为服务不在前台,重复调用startForeground();或者反过来,isForeground被提前设为true,但startForeground()还没执行完成,导致系统检测超时。

3. 停止服务的流程存在时序问题

stopService()方法中,你先调用moveToBackground(true)isForeground设为false,再调用stopSelf()

onStopEvent()
isStopping = true
moveToBackground(true)
stopSelf()

如果此时有一个延迟的startForegroundService()请求进入,onStartCommand会看到isForegroundfalse,尝试调用startForeground(),但此时服务正在停止,这会导致异常或者系统检测失败。

针对性修复方案

1. 区分服务启动和指令传递的调用方式

修改sendAction方法,当服务已运行时使用startService()

inline fun <reified T : BaseOverlayService<T>> sendAction(context: Context, checkIfServiceIsRunning: Boolean, action: String, intentUpdater: ((Intent) -> Unit) = {}) {
    if (checkIfServiceIsRunning && !isRunning<T>(context)) {
        L.logIf { DEBUG }?.d { "IGNORED action intent - action: $action" }
        return
    }
    L.logIf { DEBUG }?.d { "send action intent - action: $action" }
    val intent = Intent(context, T::class.java)
    intent.action = action
    intentUpdater(intent)
    // 已运行的服务用startService,避免触发前台检测
    if (isRunning<T>(context)) {
        context.startService(intent)
    } else {
        ContextCompat.startForegroundService(context, intent)
    }
}

2. 用原子变量保证isForeground的线程安全

isForeground替换为AtomicBoolean,避免竞态条件:

protected val isForeground = AtomicBoolean(false)
private set

然后修改moveToForeground方法中的状态判断和更新逻辑:

private fun moveToForeground(caller: String): Boolean {
    L.logIf { DEBUG }?.d { "BEFORE moveToForeground (called by $caller)" }
    val notification = notificationCreator(this as T)
    
    return if (!isForeground.get()) {
        // 先调用startForeground,再更新原子变量
        startForeground(foregroundNotificationId, notification)
        isForeground.set(true)
        L.logIf { DEBUG }?.d { "AFTER moveToForeground (called by $caller) - moved to foreground: true" }
        true
    } else {
        notificationManager.notify(foregroundNotificationId, notification)
        L.logIf { DEBUG }?.d { "AFTER moveToForeground (called by $caller) - moved to foreground: false" }
        false
    }
}

同时修改onStartCommand中的判断:

if (!isForeground.get()) {
    moveToForeground("onStartCommand")
}

3. 优化停止服务的流程,阻止停止期间的前台操作

moveToForeground中先检查isStopping状态,避免在停止过程中调用startForeground()

private fun moveToForeground(caller: String): Boolean {
    if (isStopping) {
        L.logIf { DEBUG }?.d { "IGNORED moveToForeground - service is stopping" }
        return false
    }
    // 原有的逻辑...
}

同时调整stopService的时序,确保先标记停止状态,再处理前台切换:

protected fun stopService() {
    L.logIf { DEBUG }?.d { "stopService | isStopping: $isStopping" }
    if (isStopping) {
        L.logIf { DEBUG }?.d { "IGNORED stopService" }
        return
    }
    isStopping = true // 先标记停止状态
    onStopEvent()
    moveToBackground(true)
    stopSelf()
    L.logIf { DEBUG }?.d { "stopService finished" }
}

4. 处理服务重启的边界情况

因为你使用了START_STICKY,当服务被系统回收重启时,onStartCommandintent可能为null。此时要确保无论之前的状态如何,都强制调用moveToForeground(),避免系统检测超时:

final override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    val returnValue = START_STICKY
    L.logIf { DEBUG }?.d { "onStartCommand: isForeground: ${isForeground.get()} | action: ${intent?.action} | isStopping: $isStopping" }
    
    if (isStopping) {
        return returnValue
    }
    
    // 强制调用moveToForeground,即使isForeground为true也没关系(内部会判断)
    moveToForeground("onStartCommand")
    
    onStartCommandEvent(intent, flags, startId)
    return returnValue
}

总结

这个偶发崩溃的核心原因大概率是重复调用startForegroundService()导致的系统检测触发,加上线程安全问题放大了偶现概率。按照上面的方案修复后,应该能彻底解决这个问题。

内容的提问来源于stack exchange,提问作者prom85

火山引擎 最新活动