Android 9(Pie),Context.startForegroundService()未调用Service.startForeground():ServiceRecord。

33
首先,我查看了以下内容:

图片描述

我的流媒体应用程序已经有近一百万用户在使用。我正在为播放器使用前台服务。但我还没有实现MediaSession。我有99.95%的会话没有崩溃。这个应用程序能在所有版本上运行,但我开始收到在Android 9上崩溃的报告(ANR)。这个崩溃只发生在三星手机上,特别是S9、S9+、S10、S10+和Note9型号。

我尝试了以下方法:

  • onCreate()中调用startForeground()方法
  • Context.stopService()之前调用Service.startForeground()
  • 参考其他类似问题的stackoverflow答案

我读了一些来自Google开发者的评论,他们说这只是“预期行为”。我想知道这是由三星系统还是Android OS引起的。有人对此有意见吗?我该如何解决这个问题?


1
有没有可能出现不调用startForeground的路径?或者主线程被阻塞/休眠/执行过多任务而无法处理服务的启动? - Gabe Sechan
1
这是一款收音机/音乐应用程序,它只播放流媒体URL。我在我的手机上没有遇到崩溃问题。一些用户表示,在后台听音乐时会出现崩溃问题。 - Beyazid
2
如果一个完美编写的应用程序存在这种情况,我不会感到惊讶,因为无法保证您的代码将及时调用。虽然 此答案 中的特定代码有些混乱,但解决方法似乎很有前途:通过绑定启动服务,将其移至前台,然后使用 startForegroundService(),稍后取消绑定。 - CommonsWare
1
对的。在通过startForegroundService启动前台服务后,您有一个短窗口来调用服务中的startForeground。由于某种原因,在那些设备上没有快速发生这种情况。 - Gabe Sechan
1
如果这只局限于三星设备,那就说明它们的定制版 Android 9 AOSP 存在 bug,而不是你的代码有问题。在能够真正重现之前,任何解决方法都只是纯猜测。 - Mark
显示剩余5条评论
8个回答

20

经过长时间的挣扎,我终于完全修复了这个崩溃并找到了解决方法。

请确保在您的服务中完成以下操作:

1- 在onCreateonStartCommand中都调用

startForeground()

。(可以多次调用startForeground())

  @Override
public void onCreate() {
    super.onCreate();
    startCommand();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (intent == null) {
        return START_NOT_STICKY;
    }
    final int command = intent.getIntExtra(MAIN_SERVICE_COMMAND_KEY, -1);

    if (command == MAIN_SERVICE_START_COMMAND) {
        startCommand();
        return START_STICKY;
    }
    return START_NOT_STICKY;
}

 private void startCommand() {
    createNotificationAndStartForeground();
    runningStatus.set(STARTED);
}

2- 使用

context.stopService()

停止您的服务,无需调用stopForeground()stopSelf()

  try {
        context.stopService(
                new Intent(
                        context,
                        NavigationService.class
                )
        );
    } catch (Exception ex) {
        Crashlytics.logException(ex);
        LogManager.e("Service manager can't stop service ", ex);
    }

3- 使用

ContextCompat.startForegroundService()

启动您的服务,它将处理不同的API版本。

   ContextCompat.startForegroundService(
            context,
            NavigationService.getStartIntent(context)
    );

4- 如果您的服务有操作(需要挂起的意图),请使用广播接收器处理您的挂起意图,而不是使用当前的服务(它会在Create()上调用您的服务,并可能存在危险,或使用PendingIntent.FLAG_NO_CREATE)。为处理服务通知操作,拥有一个特定的广播接收器是一个好习惯,我的意思是使用PendingIntent.getBroadcast()创建所有挂起的意图。

    private PendingIntent getStopActionPendingIntent() {
    final Intent stopNotificationIntent = getBroadCastIntent();

    stopNotificationIntent.setAction(BROADCAST_STOP_SERVICE);

    return getPendingIntent(stopNotificationIntent);
}

private PendingIntent getPendingIntent(final Intent intent) {
    return PendingIntent.getBroadcast(
            this,
            0,
            intent,
            0
    );
}

new NotificationCompat.Builder(this, CHANNEL_ID)
            .addAction(
                    new NotificationCompat.Action(
                            R.drawable.notification,
                            getString(R.string.switch_off),
                            getStopActionPendingIntent()
                    )
            )

5- 在停止你的服务之前,请确保你的服务已经被创建并启动了(我创建了一个全局类来保存我的服务状态)

  if (navigationServiceStatus == STARTED) {
            serviceManager.stopNavigationService();
        }

6- 将您的notificationId设置为长数字,例如121412。

7- 使用NotificationCompat.Builder将处理不同的API版本,您只需要为Build版本> = Build.VERSION_CODES.O创建通知频道。(这不是一个解决方案,只是使您的代码更易读)

8- 添加

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> 

在你的清单文件中添加权限(这是安卓文档中提到的)。安卓前台服务。希望对你有所帮助 :))

2
为什么我们需要在onCreate和onStartCommand两个方法中都执行它? - IgorGanapolsky
@Sepehr MAIN_SERVICE_COMMAND_KEY 是什么? - WISHY
很高兴在这里见到你,Sepehr,已经有一段时间了...谢谢,通过在onStartCommand中添加startForegroundService,它对我非常有效。 - imansdn

6

我一直在等我的崩溃报告来分享解决方案。近20天来,我没有遇到任何崩溃或ANR问题。我想分享一下我的解决方案。这可以帮助那些遇到此问题的人。

onCreate()方法中:

  • 首先,我的应用程序是一个媒体应用程序。我还没有实现mediasession。我正在onCreate()顶部创建一个通知渠道官方文档
  • 我在prepareAndStartForeground()方法中,在调用Context.startForegroundService()方法之后,调用了Service.startForeground()方法。
  • 注意:我不知道为什么,但ContextCompat.startForegroundService()无法正常工作。

因此,我已经手动向我的服务类中添加了同样的功能,而不是调用ContextCompat.startForegroundService()方法。

private fun startForegroundService(intent: Intent) {
    if (Build.VERSION.SDK_INT >= 26) {
        context.startForegroundService(intent)
    } else {
        // Pre-O behavior.
        context.startService(intent)
    }
}

prepareAndStartForeground() 方法

private fun prepareAndStartForeground() {
    try {
        val intent = Intent(ctx, MusicService::class.java)
        startForegroundService(intent)

        val n = mNotificationBuilder.build()
        // do sth
        startForeground(Define.NOTIFICATION_ID, n)
    } catch (e: Exception) {
        Log.e(TAG, "startForegroundNotification: " + e.message)
    }
}

这是我的onCreate()
override fun onCreate() {
    super.onCreate()
    createNotificationChannel()
    prepareAndStartForeground()
}

我的onStartCommand()

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (intent == null) {
        return START_STICKY_COMPATIBILITY
    } else {
        //....
        //...
    }
    return START_STICKY
}

onRebindonBindonUnbind这样的方法

internal var binder: IBinder? = null

override fun onRebind(intent: Intent) {
    stopForeground(true) // <- remove notification
}

override fun onBind(intent: Intent): IBinder? {
    stopForeground(true) // <- remove notification
    return binder
}

override fun onUnbind(intent: Intent): Boolean {
    prepareAndStartForeground() // <- show notification again
    return true
}

在调用onDestroy()方法时,我们需要清除一些东西。

   override fun onDestroy() {
    super.onDestroy()
    releaseService()
   }

private fun releaseService() {
    stopMedia()
    stopTimer()
    // sth like these
    player = null
    mContext = null
    afChangeListener = null
    mAudioBecomingNoisy = null
    handler = null
    mNotificationBuilder = null
    mNotificationManager = null
    mInstance = null
}

我希望这个解决方案能够适合您的需要。

3
你好,你能否说明一下如何创建这个服务?因为在调用ContextCompat.startForegroundService(context, intent)后会调用服务的onCreate方法,而你在服务的onCreate方法中又调用了startForegroundService。请解释一下。 - CookieMonster
6
谢谢您的提问。根据您的描述,您似乎对@Beyazid的回答有疑惑。您想知道在@Beyazid的示例中,onCreate方法是MusicService的onCreate方法吗?如果是,看起来您调用了startForegroundService两次,一次是从应用程序的不同位置调用(为了“触发”服务的onCreate),第二次是在服务的onCreate中。 - CookieMonster
嗨,@Beyazid,这个问题解决了吗?在你的崩溃报告中不再出现了吗?谢谢。 - thecr0w
2
@Beyazid 我也很困惑。请在下面评论“onCreate()”方法放置的位置。在Service的onCreate方法中调用服务对我来说没有意义。 - aguagu
答案不是很清楚。你的onCreate方法来自哪个类?你是在同一个服务的onCreate中调用startForegroundService吗? - Gaurav Singla
显示剩余11条评论

4

2023年1月6日更新

我在我的音乐播放器应用中遇到了这个问题

Context.startForegroundService()没有调用 Service.startForeground()

经过大量的研究,我找到了一些解决方案。也许这会帮助其他人

解决方案1:

如果您的应用程序的目标API级别为26或更高,则当应用程序自身不在前台时,系统会对运行后台服务施加限制。如果应用程序需要创建前台服务,则应用程序应调用 startForegroundService()。即使在应用程序处于后台时,系统也允许应用程序调用 Context.startForegroundService()。但是,应用程序必须在服务创建后的五秒内调用该服务的 startForeground() 方法。

注意:为使用 Context.startForegroundService() 的服务调用 onCreate() 中的 startForeground()

为什么会出现这个问题呢?因为 Android 框架无法保证您的服务在5秒内启动,但另一方面,框架对前台通知有严格的5秒内必须发送通知的限制。因此,在您的服务中,startForeground() 必须在 onCreate()onStartCommand() 中都有通知,因为如果您的服务已经创建,并且您的活动正在尝试再次启动它,则不会调用 onCreate()。通知 ID 不能为0,否则即使原因不同也会发生相同的崩溃。必须在 startForeground 方法之前调用 stopSelf

后台服务限制:

应用程序服务有五秒钟的时间调用 startForeground(),如果应用程序未在时间限制内调用 startForegroudn(),系统将停止服务并声明应用程序发生 ANR。

可能情况:

  1. 要么您的前台服务在调用 startForeground() 方法之前被销毁/结束。
  2. 要么如果前台服务已经实例化并再次调用它,则不会调用 onCreate() 方法,而是将调用 onStartCommand()。然后将逻辑移到 onStartCommand() 中以调用 startForeground() 方法。
  3. startForeground() 中的通知 ID 不得为0,否则也会导致崩溃。

解决方案2:

  1. onCreate()onStartCommand()中都调用startForeground()(可以多次调用startForeground()

  2. 使用context.stopService()停止您的服务,不需要调用 stopForeground()stopSelf()

  3. 使用ContextCompat.startForegroundService()启动您的服务。它将处理不同的API。

  4. 如果您的服务有操作(需要挂起意图),请使用广播接收器处理您的挂起意图,而不是您当前的服务(它会在onCreate()中调用您的服务,可能会很危险,或者使用PendingIntent.FLAG_NO_CREATE),最好为处理您的服务通知操作创建一个特定的广播接收器,我的意思是使用PendingIntent.getBroadcast()创建所有待处理的意图。

  5. 一些崩溃报告与Oreo中的BOOT_COMPLETED处理相关的问题有关

  6. 基本上,一旦您执行了startForegroundService(),就必须执行startForeground()。如果您不能在服务中执行startForeground(),那么最好在广播接收器中进行此类检查等 - 这样,只有在确保服务将执行startForeground()时才会启动服务

  7. 添加像这样的Handler()

    Handler().postDelayed(()>ContextCompat.startForegroundService(activity, new Intent(activity, ChatService.class)), 500);

解决方案3:

代替前台服务应该使用什么?

现在有JobSchedulerJobService,它们是前台服务的更好选择。这是一个更好的选择,因为: 当作业正在运行时,系统代表您的应用程序持有唤醒锁定。因此,您不需要采取任何措施来保证设备在作业期间保持唤醒状态。

这意味着您不再需要关心处理唤醒锁了,这就是为什么它与前台服务没有区别。从实现的角度来看,JobScheduler不是您的服务,而是系统的服务,它可能会正确地处理队列,并且Google永远不会终止自己的子服务。

三星已经在其 Samsung Accessory Protocol (SAP) 中从 startForegroundService 切换到了 JobSchedulerJobService。这对于智能手表等设备需要与手机这样的主机通讯,其中作业确实需要通过应用程序的主线程与用户进行交互非常有帮助。由于作业是由调度程序发布到主线程上的,因此变得可能。但请记住,作业正在主线程上运行,请将所有繁重的工作都卸载到其他线程和异步任务中。
解决方案4:
问题是要在服务本身中调用Service.startForeground(id, notification),对吗?不幸的是,Android Framework无法保证在5秒内从Service.onCreate()调用Service.startForeground(id, notification),但无论如何都会抛出异常,所以我想出了这个方法。在调用Context.startForegroundService()之前,使用服务将绑定器绑定到上下文。
如果绑定成功,则从服务连接调用Context.startForegroundService()并立即在服务连接内部调用Service.startForeground()
重要提示:在try-catch中调用Context.bindService()方法,因为在某些情况下调用可能会引发异常,在这种情况下,您需要依靠直接调用Context.startForegroundService()并希望它不会失败。一个示例可以是广播接收器上下文,但在该情况下获取应用程序上下文不会抛出异常,但直接使用上下文会抛出异常。
解决方案5:
如果在调用Context.startForegroundService(...)之后,在调用Service.startForeground(...)之前调用Context.stopService(...),则应用程序将崩溃。
谷歌团队提供的信息
这不是框架错误;这是故意的。如果应用程序使用startForegroundService()启动服务实例,则必须将该服务实例转换为前台状态并显示通知。如果在调用startForeground()之前停止服务实例,则该承诺未能实现:这是应用程序中的错误。
发布其他应用程序可以直接启动的服务基本上是不安全的。您可以通过将该服务的所有启动操作视为需要startForeground()来减轻一些风险,但显然可能不是您想要的。
解决方案6:
我成功地摆脱了所有崩溃。我所做的就是删除对stopSelf()的调用。(我正在考虑推迟停止,直到我相当确定已显示通知,但我不希望用户在不必要的情况下看到通知。)当服务闲置了一分钟或系统正常销毁它而不抛出任何异常时。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    stopForeground(true);
} else {
    stopSelf();
}

解决方案 7:

与前台服务使用相关的常见崩溃是:

Context.startForegroundService()没有调用Service.startForeground()

要消除此问题,请使用以下策略:

不要将服务作为前台服务启动,而应将其作为后台服务启动,绑定到它,然后当您在活动/ UI组件中获得服务实例时,可以直接调用服务内的方法,该方法调用, 并添加通知。

这可能听起来像一个小技巧,但请想象一下像 Spotify 这样的音乐应用如何启动他们的服务。这是一种类似的方法。结果是杰出的。使用此方法,问题计数从一个显着的不可透露的数字减少到0。


1
非常感谢您花时间分享这个,它确实有所帮助。 - lnstadrum

3

Android的Service组件有点麻烦,特别是在后期Android版本中,因为操作系统添加了额外的限制。如其他答案所述,启动Service时,请使用ContextCompat.startForegroundService()。接下来,在Service.onStartCommand()中立即调用startForeground()。将要展示的Notification存储为成员字段,并使用该字段,除非它为空。例如:

private var notification:Notification? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (notification == null) {
        notification = createDefaultNotification()
    }
    startForeground(NOTIFICATION_ID, notification)

    // Do any additional setup and work herre

    return START_STICKY
}

请始终在您的 Service 中返回 START_STICKY 。其他任何选项都可能是错误的选择,尤其是如果您正在使用任何类型的音频播放器。事实上,如果您正在使用音频播放器,您不应该实现自己的服务,而应该使用 MediaBrowserServiceCompat (来自AndroidX)。

我还建议您阅读我写的这些博客帖子:https://hellsoft.se/how-to-service-on-android-part-3-1e24113152cd


1
始终在您的 Service 中返回 START_STICKY。其他任何选项可能都是错误的东西。这可能有点傲慢,您需要解释吗? - Tim
@Tim 请阅读我链接的帖子。在那个系列中,我解释了为什么START_STICKY是你今天应该使用的内容。当然,也有例外情况,但对于99%需要Service的情况,这是正确的方法。 - Erik Hellman
为什么您建议使用ContextCompat而不是Context?这会有什么区别吗? - Emil
1
@Emil ContextCompat 是 AndroidX 库的一部分,包含增强功能和错误修复,并定期更新。平台上的 Context 仅在 Android 操作系统更新时才会更新。始终优先考虑使用可用的 AndroidX 封装。 - Erik Hellman

1

我曾经浪费了很多时间去找出它为什么会崩溃,但并不理解原因。问题出在startForeground()的通知ID上。将其更改为非0值即可。这是在Android 11上运行的。

@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        
        //Create notification here
        
        startForeground(5, builder.build()); //Change the ID to something other than 0

        return START_NOT_STICKY;
    }

0

在遇到同样的手机问题后,我进行了几项更改,崩溃问题得到了解决。我不确定是什么技巧起了作用,但我猜测在onCreate和onStartCommand两个函数中都调用startForeground。如果服务已经启动并且所有调用都在onCreate中正确执行,那么我不确定为什么需要这样做。

其他更改: - 将serviceId更改为较小的数字(1-10) - 通过单例同步类较少地调用startFororegroundService(在出现崩溃之前就实现了此功能,以防止在onStartCommand回调中停止服务启动,但现在它还会过滤已经启动服务的调用)。 - 使用START_REDELIVER_INTENT(不应对任何事情产生影响)

这个问题只发生在某些用户的提到过的手机上,所以我怀疑这与三星的一些新更新有关,最终将会得到解决。


当已经创建了一个服务时,对于调用Context.startForegroundService()的情况,您需要使用startForeground onStartCommand。 - Malachiasz

0

我几乎已经解决了与startForeground()在MediaSessionCompat.Callback方法(如onPlay(),onPause())中的问题。


0
根据此错误消息,当您调用Context.startForegroundService()时,必须使用Service.startForeground()方法发出通知。这是我的理解。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接