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

387

我正在使用Android O OS上的Service类。

我计划在后台中使用Service

Android文档指出:

如果您的应用程序针对API级别26或更高级别,则系统对使用或创建后台服务施加限制,除非应用程序本身处于前台。如果应用程序需要创建前台服务,则应用程序应调用startForegroundService()

如果您使用startForegroundService(),则Service会抛出以下错误。

Context.startForegroundService() did not then call
Service.startForeground() 

这有什么问题吗?


请提供一个最小化且完整的可再现代码示例([mcve]),包括完整的Java堆栈跟踪和导致程序崩溃的代码。 - CommonsWare
6
API 26和27(27.0.3)仍存在此错误。受影响的Android版本为8.0和8.1。您可以通过在onCreate()和onStartCommand()中添加startForeground()来减少崩溃次数,但是对于某些用户仍然会发生崩溃。目前唯一解决它的方法是在您的build.gradle中将targetSdkVersion设置为25。 - Alex
3
FYI https://issuetracker.google.com/issues/76112072 - v1k
2
我有同样的问题。我解决了这个问题。我在这个主题中分享了我的实现 https://dev59.com/MlMI5IYBdhLWcg3wkMUh#56338570 - Beyazid
显示剩余6条评论
35个回答

149

来自谷歌有关Android 8.0 行为变更的文档:

即使应用程序在后台运行,系统也允许应用程序调用 Context.startForegroundService()。但是,应用程序必须在服务创建后的五秒内调用该服务的 startForeground() 方法。

解决方案: 对于使用 Context.startForegroundService()Service,请在 onCreate() 中调用 startForeground()

另请参阅:Android 8.0(Oreo)背景执行限制


35
我在onStartCommand方法中做了这个,但我仍然收到这个错误。我在MainActivity中调用了startForegroundService(intent)。也许服务启动得太慢了。我认为在他们能够保证立即启动服务之前不应该存在五秒钟的限制。 - Kimi Chiu
45
5秒的时间绝对不够,这种异常在调试会话中经常发生。我怀疑在发布模式下也有可能发生。而且这个致命的异常会使应用程序崩溃!我尝试使用Thread.setDefaultUncaughtExceptionHandler()来捕获它,但即使在那里捕获并忽略了android的停顿,应用程序也会冻结。这是因为这个异常是从handleMessage()触发的,而主循环实际上已经结束了……正在寻找解决方法。 - Kurovsky
17
我认为你应该在onStartCommand()中调用它,而不是在onCreate中调用,因为如果你关闭服务并重新启动它,它可能会进入onStartCommand()而不调用onCreate... - android developer
1
我们可以在此处检查来自Google团队的响应:https://issuetracker.google.com/issues/76112072#comment56 - Prags
10
我有一个在Play Store上拥有3万活跃用户的应用程序,但我遇到了同样的问题:由于5秒启动前台界面的时间限制,在Android 8+上每天会出现大约30次崩溃。这些崩溃来自各种设备,甚至包括今年发布的手机(如S20Ultra、Motorola等)。Android的设计不能保证在5秒内触发呼叫并且服务不被操作系统杀死。Google已经收到了很多关于这个问题的反馈,但他们不承认这是设计缺陷。他们在迫使我们放弃后台服务之前应该重新设计得更好。 - Duna
显示剩余5条评论

126
我调用了ContextCompat.startForegroundService(this, intent)来启动服务,然后在服务的onCreate方法中。
 @Override
 public void onCreate() {
        super.onCreate();

        if (Build.VERSION.SDK_INT >= 26) {
            String CHANNEL_ID = "my_channel_01";
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
                    "Channel human readable title",
                    NotificationManager.IMPORTANCE_DEFAULT);

            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);

            Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                    .setContentTitle("")
                    .setContentText("").build();

            startForeground(1, notification);
        }
}

76
我也是。但我仍然偶尔会出现这个错误。也许Android无法保证在5秒内调用onCreate方法,所以他们应该在强制我们遵循规则之前重新设计它。 - Kimi Chiu
6
我曾在 onStartCommand() 中调用 startForeground(),但偶尔会出现错误。我把它移到了 onCreate() 中,自那以后就没有再看到这个错误了(祈祷)。 - Tyler
4
如何停止显示Oreo中的本地通知,提示“APP_NAME正在运行。 点击关闭或查看信息。”? - Artist404
7
不,安卓团队声称这是一个预期行为。所以我重新设计了我的应用程序。这太荒谬了。总是因为这些“预期行为”需要重新设计应用程序。这已经是第三次了。 - Kimi Chiu
24
这个问题的主要原因是服务在被提升到前台之前就被停止了。但即使服务被销毁后,断言并没有停止。你可以尝试在调用startForegroundService后添加StopService来重现这个问题。 - Kimi Chiu
显示剩余9条评论

84
这个问题之所以会出现是因为Android框架不能保证您的服务在5秒内启动,但另一方面框架对前台通知有严格限制,必须在5秒内触发,而不检查框架是否尝试启动服务。
这绝对是一个框架问题,但并非所有遇到此问题的开发人员都已经尽力了:
1.在 onCreate onStartCommand 中都必须有 startForeground 通知,因为如果您的服务已经创建,而某些时候您的活动正在尝试再次启动它,就不会调用 onCreate
2.通知ID不能为0,否则即使不是同一原因也会发生相同的崩溃。
3. stopSelf 必须在 startForeground 之前调用。
通过以上三点,这个问题可以稍微减少,但仍然没有解决,真正的解决方法或者说是变通方法是将目标SDK版本降级到25。
请注意,很可能Android P仍然存在此问题,因为Google拒绝了解发生了什么,并且不认为这是他们的错,阅读#36#56以获取更多信息。

18
为什么要同时使用onCreate和onStartCommand?你能否只在onStartCommand中实现它? - rcell
4
我进行了一些测试,即使服务已经创建,如果onCreate没有被调用,你也不需要再次调用startForeground。因此,你只需要在onCreate方法中调用它而不是在onStartCommand中调用。可能他们认为,在第一次调用之后,单个服务实例会一直处于前台状态直到结束。 - Lxu
2
我将0作为“startForeground”初始调用的通知ID。将其更改为1可以解决我的问题(希望如此)。 - mr5
4
如果您的服务已经创建,而您的活动正在尝试再次启动它,那么onCreate方法将不会被调用。由于服务在onCreate方法中变成前台服务,所以如果服务已经启动过了,那么它已经在前台运行,这句话就没有意义了。 - user924
1
我浪费了很多时间来弄清楚为什么它会崩溃,却不理解原因。问题出在startForeground()的通知ID上。将其更改为非0值即可。 - DIRTY DAVE
显示剩余6条评论

69

我知道,已经有太多答案发布了,然而事实是- startForegroundService不能在应用程序层面进行修复,并且您应该停止使用它。Google建议在调用Context#startForegroundService()之后的5秒内使用Service#startForeground() API并不是应用程序总是可以做到的。

Android同时运行很多进程,并没有任何保证Looper将在5秒钟内调用您的目标服务以调用startForeground()。如果您的目标服务在5秒钟内没有收到调用,则您将会遇到ANR情况。在您的堆栈跟踪中,您将看到以下内容:

Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{1946947 u0 ...MessageService}

main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x763e01d8 self=0x7d77814c00
  | sysTid=11171 nice=-10 cgrp=default sched=0/0 handle=0x7dfe411560
  | state=S schedstat=( 1337466614 103021380 2047 ) utm=106 stm=27 core=0 HZ=100
  | stack=0x7fd522f000-0x7fd5231000 stackSize=8MB
  | held mutexes=
  #00  pc 00000000000712e0  /system/lib64/libc.so (__epoll_pwait+8)
  #01  pc 00000000000141c0  /system/lib64/libutils.so (android::Looper::pollInner(int)+144)
  #02  pc 000000000001408c  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+60)
  #03  pc 000000000012c0d4  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
  at android.os.MessageQueue.nativePollOnce (MessageQueue.java)
  at android.os.MessageQueue.next (MessageQueue.java:326)
  at android.os.Looper.loop (Looper.java:181)
  at android.app.ActivityThread.main (ActivityThread.java:6981)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1445)
据我了解,Looper在分析队列时发现了一个“滥用者”,并将其简单地杀掉了。现在系统很健康,但开发人员和用户不是,但既然谷歌将他们的责任限制在系统上,为什么他们要关心后两者呢?显然他们不关心。他们能让它变得更好吗?当然可以,例如,他们可以提供“应用程序忙碌”对话框,询问用户等待还是关闭应用程序,但为什么要麻烦呢?这不是他们的责任。最重要的是现在系统已经很健康。

从我的观察来看,这种情况相对较少,在我的情况下大约有1K个用户中每月1次崩溃。无法复现它,即使复现了,你也无法永久修复它。

在这个主题中有一个很好的建议,使用“bind”而不是“start”,然后当服务准备就绪时,在onServiceConnected进程中进行处理,但再次说明,这意味着根本不使用startForegroundService调用。

我认为,谷歌方面正确和诚实的行动应该是告诉每个人,startForegourndServcie存在缺陷,不应该使用。

问题仍然存在:替代品是什么?对我们来说,现在有JobScheduler和JobService,它们是前台服务的更好选择。这是一个更好的选择,因为:

   

当作业正在运行时,系统代表您的应用程序持有wakelock。因此,您不需要采取任何措施来保证设备在作业期间保持唤醒状态。

这意味着您不需要再关心处理wakelocks了,这就是为什么与前台服务没有区别。从实现的角度来看,JobScheduler不是您的服务,而是系统的服务,它应该会正确地处理队列,并且谷歌永远不会终止自己的子进程 :)

三星已经从startForegroundService切换到了JobScheduler和JobService,用于其Samsung Accessory Protocol (SAP)。当像智能手表这样的设备需要与像手机这样的主机交互时,这非常有帮助,其中作业确实需要通过应用程序的主线程与用户交互。由于作业是由调度程序发布到主线程的,所以这变得可能。但是请记住,作业正在主线程上运行,请将所有繁重的工作卸载到其他线程和异步任务中。

该服务在运行每个传入的作业时,都会在您的应用程序的主线程上运行一个Handler。这意味着您必须将执行逻辑卸载到另一个线程/ handler / AsyncTask 中

切换到JobScheduler / JobService的唯一缺陷是您需要重构旧代码,这不是令人愉快的事情。我花了过去两天的时间来做到这一点,以使用新的三星SAP实现。我会监视我的崩溃报告,并告诉您是否再次看到崩溃。理论上不应该发生,但总有我们可能不知道的细节。

更新 Play Store没有报道更多崩溃。这意味着JobScheduler / JobService没有这样的问题,切换到此模

SAAgentV2.requestAgent(App.app?.applicationContext, 
   MessageJobs::class.java!!.getName(), mAgentCallback)

现在您需要反编译三星的代码以查看内部情况。在SAAgentV2中,查看requestAgent实现和以下行:

SAAgentV2.d var3 = new SAAgentV2.d(var0, var1, var2);

where d defined as below

private SAAdapter d;

现在前往SAAdapter类,找到onServiceConnectionRequested函数,在该函数中使用以下调用安排作业:

SAJobService.scheduleSCJob(SAAdapter.this.d, var11, var14, var3, var12); 

SAJobService 是 Android 的 JobService 的一种实现,它用于进行作业调度:

private static void a(Context var0, String var1, String var2, long var3, String var5, SAPeerAgent var6) {
    ComponentName var7 = new ComponentName(var0, SAJobService.class);
    Builder var10;
    (var10 = new Builder(a++, var7)).setOverrideDeadline(3000L);
    PersistableBundle var8;
    (var8 = new PersistableBundle()).putString("action", var1);
    var8.putString("agentImplclass", var2);
    var8.putLong("transactionId", var3);
    var8.putString("agentId", var5);
    if (var6 == null) {
        var8.putStringArray("peerAgent", (String[])null);
    } else {
        List var9;
        String[] var11 = new String[(var9 = var6.d()).size()];
        var11 = (String[])var9.toArray(var11);
        var8.putStringArray("peerAgent", var11);
    }

    var10.setExtras(var8);
    ((JobScheduler)var0.getSystemService("jobscheduler")).schedule(var10.build());
}

正如您所看到的,在此处的最后一行使用了Android的JobScheduler来获取这个系统服务并安排作业。

在requestAgent调用中,我们传递了mAgentCallback,它是一个回调函数,在发生重要事件时将接收控制。这是我应用程序中定义回调的方式:

private val mAgentCallback = object : SAAgentV2.RequestAgentCallback {
    override fun onAgentAvailable(agent: SAAgentV2) {
        mMessageService = agent as? MessageJobs
        App.d(Accounts.TAG, "Agent " + agent)
    }

    override fun onError(errorCode: Int, message: String) {
        App.d(Accounts.TAG, "Agent initialization error: $errorCode. ErrorMsg: $message")
    }
}

MessageJobs是我实现的一个类,用于处理来自三星智能手表的所有请求。这不是完整的代码,只是一个框架:

class MessageJobs (context:Context) : SAAgentV2(SERVICETAG, context, MessageSocket::class.java) {


    public fun release () {

    }


    override fun onServiceConnectionResponse(p0: SAPeerAgent?, p1: SASocket?, p2: Int) {
        super.onServiceConnectionResponse(p0, p1, p2)
        App.d(TAG, "conn resp " + p1?.javaClass?.name + p2)


    }

    override fun onAuthenticationResponse(p0: SAPeerAgent?, p1: SAAuthenticationToken?, p2: Int) {
        super.onAuthenticationResponse(p0, p1, p2)
        App.d(TAG, "Auth " + p1.toString())

    }


    override protected fun onServiceConnectionRequested(agent: SAPeerAgent) {


        }
    }

    override fun onFindPeerAgentsResponse(peerAgents: Array<SAPeerAgent>?, result: Int) {
    }

    override fun onError(peerAgent: SAPeerAgent?, errorMessage: String?, errorCode: Int) {
        super.onError(peerAgent, errorMessage, errorCode)
    }

    override fun onPeerAgentsUpdated(peerAgents: Array<SAPeerAgent>?, result: Int) {

    }

}

如您所见,MessageJobs 需要 MessageSocket 类的实现,它处理来自设备的所有消息。

总之,这并不简单,需要深入了解内部和编码,但它能够正常工作,最重要的是——它不会崩溃。


4
回答不错,但存在一个重要问题,“JobIntentService”在Oreo以下会立即作为“IntentService”运行,但在Oreo及以上版本会安排一个作业,因此“JobIntentService”不会立即启动。更多信息 - CopsOnRoad
1
你说得对,前台服务可能无法在5秒的时间限制内启动(这是谷歌应该为此负责),但JobService适用于15分钟间隔的任务。我点赞了你的帖子,因为方法很好,但并不实用。 - CopsOnRoad
1
@CopsOnRoad 这非常有用并且在我的情况下立即启动,正如我所写的一样,它用于手机和智能手表之间的实时交互。用户不可能等待15分钟来发送这两者之间的数据。它运行得非常好,并且从未崩溃。 - Oleg Gryb
2
可能在时间允许的情况下,我会这样做:我需要将其与定制特定代码解耦,并反编译一些专有的三星库。 - Oleg Gryb
2
因此将JobService称为良好的替代方案是错误的,因为它不能一直运行,只有粘性前台服务才能做到这一点。 - user924
显示剩余12条评论

49
如果在调用Context.startForegroundService(...)并在调用Service.startForeground(...)之前调用Context.stopService(...),则您的应用程序将崩溃。
我在这里有一个明确的重现:ForegroundServiceAPI26 我在此处提出了一个错误:Google问题跟踪器 已经开放和关闭了几个关于这个问题的错误,但不修复。
希望我的清晰的重现步骤可以解决这个问题。
谷歌团队提供的信息: Google问题跟踪器评论36 这不是框架的错误;这是故意的。如果应用程序使用startForegroundService()启动服务实例,则必须将该服务实例转换为前台状态并显示通知。如果在对它调用startForeground()之前停止服务实例,则未履行该承诺:这是应用程序中的错误。 回复#31,发布其他应用程序可以直接启动的服务基本上是不安全的。您可以通过将对该服务的所有启动操作视为需要startForeground()来缓解这个问题,尽管显然这可能不是您想要的。 Google问题跟踪器评论56 有几种不同的情况会导致出现相同的结果。

纯粹的语义问题就是,如果使用startForegroundService()启动服务但未通过startForeground()将其转换为前台,则这只是一个错误。这被视为应用程序错误。在将服务转换为前台之前停止它也是一个应用程序错误。这是OP的核心,也是为什么此问题已被标记为“按预期工作”的原因。

然而,还存在有关此问题的误报检测的问题。这被视为真正的问题,尽管它与此特定的错误跟踪器问题分开跟踪。我们不会忽视这个投诉。


7
我看到的最好的更新是https://issuetracker.google.com/issues/76112072#comment56。 我重写了我的代码,使用Context.bindService完全避免了对Context.startForegroundService的问题调用。 您可以在https://github.com/paulpv/ForegroundServiceAPI26/tree/bound/app/src/main/java/com/github/paulpv/foregroundserviceapi26 上查看我的示例代码。 - swooby
1
是的,正如我所写的那样,绑定应该可以工作,但我已经切换到JobService了,除非这个也出问题了,否则我不会改变任何东西 ':) - Oleg Gryb
1
@swooby,你的解决方案对我帮助很大。 - lasec0203
1
在我的情况下,这是不可能的,因为我没有使用context.stopService。我从活动发送本地广播来停止服务,服务注册到此广播,调用stopSelf,因此它会停止自身,当它调用stopSelf时,startForeground在100%之前被调用。 - user924
2
大家看看我的答案,它对我有效。https://dev59.com/AVcP5IYBdhLWcg3wa5bQ#72754189 - Kunal Kalwar

31

因为来到这里的每个人都在遭受同样的问题,所以我想分享我的解决方案,这是其他人(至少在这个问题中)都没有尝试过的。我可以保证它有效,甚至在停止的断点上也能确认这种方法。

问题是从服务本身调用Service.startForeground(id, notification),对吧?不幸的是,在Android框架中,无法保证在5秒内在Service.onCreate()内调用Service.startForeground(id, notification),但是无论如何都会抛出异常,所以我想出了这种方法。

  1. 使用来自服务的绑定程序将服务绑定到上下文,然后调用Context.startForegroundService()
  2. 如果绑定成功,请从服务连接处调用Context.startForegroundService(),并立即从服务连接中调用Service.startForeground()
  3. 重要提示:try-catch内调用Context.bindService()方法,因为在某些情况下,该调用可能会引发异常,在这种情况下,您需要依赖于直接调用Context.startForegroundService()并希望它不会失败。例如,广播接收器上下文可能会引发异常,但在这种情况下获取应用程序上下文不会引发异常,但直接使用上下文会。

即使我在绑定服务之后并在触发“startForeground”调用之前等待断点时,它也可以正常工作。等待3-4秒钟不会触发异常,而超过5秒钟就会抛出异常。(如果设备无法在5秒内执行两行代码,那么就是时候将其丢进垃圾桶了。)

所以,从创建服务连接开始。

// Create the service connection.
ServiceConnection connection = new ServiceConnection()
{
    @Override
    public void onServiceConnected(ComponentName name, IBinder service)
    {
        // The binder of the service that returns the instance that is created.
        MyService.LocalBinder binder = (MyService.LocalBinder) service;

        // The getter method to acquire the service.
        MyService myService = binder.getService();

        // getServiceIntent(context) returns the relative service intent 
        context.startForegroundService(getServiceIntent(context));

        // This is the key: Without waiting Android Framework to call this method
        // inside Service.onCreate(), immediately call here to post the notification.
        myService.startForeground(myNotificationId, MyService.getNotification());

        // Release the connection to prevent leaks.
        context.unbindService(this);
    }

    @Override
    public void onBindingDied(ComponentName name)
    {
        Log.w(TAG, "Binding has dead.");
    }

    @Override
    public void onNullBinding(ComponentName name)
    {
        Log.w(TAG, "Bind was null.");
    }

    @Override
    public void onServiceDisconnected(ComponentName name)
    {
        Log.w(TAG, "Service is disconnected..");
    }
};
在您的服务中,创建一个返回您服务实例的绑定器。
public class MyService extends Service
{
    public class LocalBinder extends Binder
    {
        public MyService getService()
        {
            return MyService.this;
        }
    }

    // Create the instance on the service.
    private final LocalBinder binder = new LocalBinder();

    // Return this instance from onBind method.
    // You may also return new LocalBinder() which is
    // basically the same thing.
    @Nullable
    @Override
    public IBinder onBind(Intent intent)
    {
        return binder;
    }
}

接下来,尝试从该上下文绑定服务。如果成功,它将从您正在使用的服务连接调用ServiceConnection.onServiceConnected()方法。然后,在上面显示的代码中处理逻辑。例如代码如下:

// Try to bind the service
try
{
     context.bindService(getServiceIntent(context), connection,
                    Context.BIND_AUTO_CREATE);
}
catch (RuntimeException ignored)
{
     // This is probably a broadcast receiver context even though we are calling getApplicationContext().
     // Just call startForegroundService instead since we cannot bind a service to a
     // broadcast receiver context. The service also have to call startForeground in
     // this case.
     context.startForegroundService(getServiceIntent(context));
}

看起来在我开发的应用程序上运行良好,因此当您尝试时也应该能够正常工作。


5
到目前为止,这对我很有效——一个月过去了,再也没有与startForegroundService()相关的崩溃或ANR——谢谢! - gregko
一个棘手的解决方案,我会尝试它,在我的应用中有80k活跃用户。 - user924
但是您必须在活动周期的onStop中解除服务绑定。 - user924
1
// 这是关键:不用等待Android Framework来调用此方法 // 在Service.onCreate()内立即调用此方法以发布通知。 - 我不确定这一点,您如何在调用Service.onCreate之前启动通知。当onServiceConnected被调用时, //则意味着已经调用了onCreate,因此您的句子有些荒谬。 - user924
这仍然没有帮助,我有80K活跃用户(200K下载)。 - user924
显示剩余4条评论

24

现在在Android O中,您可以将背景限制设置如下:

调用服务类的服务

Intent serviceIntent = new Intent(SettingActivity.this,DetectedService.class);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    SettingActivity.this.startForegroundService(serviceIntent);
} else {
    startService(serviceIntent);
}

服务类别应该是这样的

public class DetectedService extends Service { 
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        int NOTIFICATION_ID = (int) (System.currentTimeMillis()%10000);
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForeground(NOTIFICATION_ID, new Notification.Builder(this).build());
        }


        // Do whatever you want to do here
    }
}

13
你是否尝试在至少有数千用户的应用程序中测试过它?我已经尝试过了,但仍有一些用户遇到了崩溃问题。 - Alex
1
你有多少用户?我每天看到大约10-30个应用程序崩溃(带有错误信息),而该应用程序每天有10,000个用户。当然,这仅发生在使用Android 8.0和Android 8.1的用户身上,如果targetSdkVersion为27。在我将targetSdkVersion设置为25之后,所有问题都消失了,没有任何崩溃。 - Alex
1
@Jeeva,不是真的。目前,我使用targetSdkVersion 25和compileSdkVersion 27。经过许多实验后,这似乎是最好的方法...希望他们能在2018年8月之前完成https://developer.android.com/topic/libraries/architecture/workmanager#java,因为https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html。 - Alex
为什么要将它仅限于Oreo操作系统的前台呢,这没有任何意义。 - user924
如果您正在AVD上进行测试,请删除代码:startService(serviceIntent)。好吧,AVD将始终尝试运行此代码 - 但它会发现一个大于26的版本。 - Vicente Domingos
显示剩余9条评论

18
我有一个widget,当设备处于唤醒状态时会进行相对频繁的更新,我在短短几天内看到了成千上万次崩溃。
问题触发器
即使在我认为设备没有太多负载的情况下,我甚至注意到了在我的Pixel 3 XL上也出现了这个问题。所有代码路径都被startForeground()覆盖。但后来我意识到,在许多情况下,我的服务非常快地完成任务。我认为导致应用程序出现问题的触发器是服务在系统实际显示通知之前就已经结束了。
解决方法/解决方案
我成功消除了所有崩溃。我所做的是删除对stopSelf()的调用。(我曾考虑延迟停止直到我相当确定通知已经显示,但如果不必要,我不希望用户看到通知。)当服务闲置一分钟或系统正常销毁它而不抛出任何异常时。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    stopForeground(true);
} else {
    stopSelf();
}

但是,根据文档,stopForeground() ...不会停止服务的运行(要停止服务的运行,请使用stopSelf()或相关方法) - Dmytro Rostopira
@DimaRostopira 是的,实际上我看到系统会在服务一段时间不活动后(可能是一分钟)停止其运行。虽然这并不理想,但它可以防止系统抱怨。 - Roy Solberg
我也遇到了前台通知不被移除的问题,因为我的服务运行时间非常短。在调用stopForeground(true); stopSelf();之前添加100毫秒的延迟有所帮助。你的解决方案没有起作用。(Pixel 3,Android 11) - Malachiasz

16

我有一个解决此问题的方法。 我已经在自己的应用程序(300K+ DAU)中验证了此修复程序,可以减少至少95%的这种崩溃,但仍无法完全避免此问题。

即使您确保在服务启动后立即调用startForeground(),此问题也会发生,如Google所述。 这可能是因为在许多情况下,服务的创建和初始化过程已经花费超过5秒钟,因此无论何时何地调用startForeground()方法,都无法避免此崩溃。

我的解决方案是确保在startForegroundService()方法后的5秒内执行startForeground(),无论您的服务需要多长时间才能创建和初始化。 以下是详细的解决方案。

  1. 首先请勿使用startForegroundService,而是使用auto_create标志的bindService()。 它将等待服务初始化。 这里是代码,我的示例服务是MusicService:

    final Context applicationContext = context.getApplicationContext();
    Intent intent = new Intent(context, MusicService.class);
    applicationContext.bindService(intent, new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            if (binder instanceof MusicBinder) {
                MusicBinder musicBinder = (MusicBinder) binder;
                MusicService service = musicBinder.getService();
                if (service != null) {
                    // start a command such as music play or pause.
                    service.startCommand(command);
                    // force the service to run in foreground here.
                    // the service is already initialized when bind and auto_create.
                    service.forceForeground();
                }
            }
            applicationContext.unbindService(this);
        }
    
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    }, Context.BIND_AUTO_CREATE);
    
  2. 这里是MusicBinder的实现:

  3. /**
     * Use weak reference to avoid binder service leak.
     */
     public class MusicBinder extends Binder {
    
         private WeakReference<MusicService> weakService;
    
         /**
          * Inject service instance to weak reference.
          */
         public void onBind(MusicService service) {
             this.weakService = new WeakReference<>(service);
         }
    
         public MusicService getService() {
             return weakService == null ? null : weakService.get();
         }
     }
    
  4. 最重要的部分是MusicService的实现,forceForeground()方法将确保在服务启动后立即调用startForeground()方法:

  5. public class MusicService extends MediaBrowserServiceCompat {
    ...
        private final MusicBinder musicBind = new MusicBinder();
    ...
        @Override
        public IBinder onBind(Intent intent) {
            musicBind.onBind(this);
            return musicBind;
        }
    ...
        public void forceForeground() {
            // API lower than 26 do not need this work around.
            if (Build.VERSION.SDK_INT >= 26) {
                Notification notification = mNotificationHandler.createNotification(this);
                // call startForeground just after service start.
                startForeground(Constants.NOTIFICATION_ID, notification);
            }
        }
    }
    
  6. 如果你想在挂起的意图中运行步骤1的代码片段,比如说你想在小部件中启动前台服务(点击小部件按钮)而不打开应用程序,那么你可以将代码片段包装在广播接收器中,并触发广播事件,而不是启动服务命令。

就这些了,希望能有所帮助,祝好运。


这仍然没有帮助,我有80K活跃用户(200K下载)。 - user924
@user924 正如我之前所提到的,它可以至少减少95%的这种崩溃,但仍然无法完全避免这个问题。我没有找到一种方法来100%解决这个异常。抱歉。 - Hexise
如果必须使用Context.startForeground()来从app的后台状态启动服务,那么这怎么能算是一个解决方案呢? - Malachiasz
尽管@CommonsWare在他的评论中推荐了这个解决方案https://dev59.com/MlMI5IYBdhLWcg3wkMUh#7P7rnYgBc1ULPQZFAR5V,但它并不完美。在调用startForeground() unbindService()之后,有时系统会销毁服务,然后立即重新启动。当服务在启动期间初始化工作线程时,我们会收到一个错误java.lang.InternalError: Thread starting during runtime shutdown :-( - zelig74

15

提醒大家,我在这个问题上浪费了太多时间。即使我在onCreate(..)中的第一件事就是调用startForeground(..),我仍然一直收到该异常提示。最终我发现,问题是由于使用NOTIFICATION_ID = 0引起的。使用任何其他值似乎可以解决此问题。


在我的情况下,我使用的是ID 1,谢谢,问题已解决。 - Amin Pinjari
9
我正在使用ID 101,但有时仍会收到此错误。 - CopsOnRoad
@RavindraKushwaha 不是很确定。 - CopsOnRoad
我有一个随机的通知ID,但我仍然遇到这个问题。在Android 13上比其他版本更频繁地遇到它。 - Alex

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