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个回答

4

https://developer.android.com/reference/android/content/Context.html#startForegroundService(android.content.Intent)

类似于startService(Intent),但隐含承诺服务一旦开始运行将调用startForeground(int, android.app.Notification)。系统会给予服务与ANR间隔时间相当的时间来执行此操作,否则系统将自动停止服务并宣布应用程序ANR。与普通的startService(Intent)不同,此方法可在任何时候使用,而不管托管服务的应用程序是否处于前台状态。
请确保在onCreate()中调用Service.startForeground(int, android.app.Notification),以确保它将被调用。如果您有任何可能阻止您这样做的条件,那么最好使用正常的Context.startService(Intent),并自己调用Service.startForeground(int, android.app.Notification)。
看起来Context.startForegroundService()添加了一个看门狗,以确保在销毁之前调用了Service.startForeground(int, android.app.Notification)。

2

好的,我注意到了一些东西,它可能会帮助其他人。这仅仅是从测试中看到的结果,为了弄清楚如何修复我所看到的情况。为简单起见,让我们假设我有一个方法从Presenter中调用这个方法。

"Original Answer"的翻译是"最初的回答"。

context.startForegroundService(new Intent(context, TaskQueueExecutorService.class));

try {
    Thread.sleep(10000);
} catch (InterruptedException e) {
    e.printStackTrace();
}       

最初的回答:这将导致相同的错误崩溃。服务在方法完成前不会启动,因此服务中没有onCreate()。因此,即使您在主线程之外更新UI,如果有任何可能阻塞该方法的内容,则它将无法按时启动并给您带来可怕的前台错误。在我的情况下,我们正在将一些内容加载到队列中,并每次调用startForegroundService,但是每个背景中都涉及某些逻辑。因此,如果逻辑太长而不能结束该方法,则会发生崩溃,因为它们是依次回调的。旧版的startService只是忽略了它,并继续执行,因为我们每次都调用它,下一轮就会完成。 这让我想知道,如果我从后台线程调用服务,它是否无法完全绑定并立即运行,因此我开始尝试实验。即使这样做不能立即启动它,但它不会崩溃。
new Handler(Looper.getMainLooper()).post(new Runnable() {
        public void run() {
               context.startForegroundService(new Intent(context, 
           TaskQueueExecutorService.class));
               try {
                   Thread.sleep(10000);
               } catch (InterruptedException e) {
                  e.printStackTrace();
              }       
        }
});

我不会假装知道为什么它不会崩溃,但我猜测这会强制它等待主线程及时处理。虽然将其绑定到主线程并不理想,但由于我的使用是在后台调用它,所以如果它等待完成而不是崩溃,我并不担心。"最初的回答"

你是从后台启动服务还是在活动中启动? - Mateen Chaudhry
背景。问题在于Android无法保证服务将在分配的时间内启动。人们可能认为,如果您愿意,他们只会抛出一个错误来忽略它,但不幸的是,Android不是这样的。它必须是致命的。这在我们的应用程序中有所帮助,但并没有完全解决它。 - a54studio

2

onStartCommand(...)中更新数据

onBind(...)

onBind(...)是一个更好的生命周期事件,用于启动startForeground,而不是onCreate(...),因为onBind(...)传递了一个Intent,其中可能包含初始化Service所需的重要数据。然而,这并非必要,因为onStartCommand(...)会在Service第一次创建或以后每次调用时都被调用。

onStartCommand(...)

onStartCommand(...)中的startForeground很重要,以便在Service被创建后进行更新。

当在创建了Service之后调用ContextCompat.startForegroundService(...)时,onBind(...)onCreate(...)不会被调用。因此,可以通过IntentBundle将更新的数据传递到onStartCommand(...)中,以更新Service中的数据。

示例

我正在使用这个模式来在Coinverse加密货币新闻应用中实现PlayerNotificationManager

Activity / Fragment.kt

context?.bindService(
        Intent(context, AudioService::class.java),
        serviceConnection, Context.BIND_AUTO_CREATE)
ContextCompat.startForegroundService(
        context!!,
        Intent(context, AudioService::class.java).apply {
            action = CONTENT_SELECTED_ACTION
            putExtra(CONTENT_SELECTED_KEY, contentToPlay.content.apply {
                audioUrl = uri.toString()
            })
        })

AudioService.kt

private var uri: Uri = Uri.parse("")

override fun onBind(intent: Intent?) =
        AudioServiceBinder().apply {
            player = ExoPlayerFactory.newSimpleInstance(
                    applicationContext,
                    AudioOnlyRenderersFactory(applicationContext),
                    DefaultTrackSelector())
        }

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    intent?.let {
        when (intent.action) {
            CONTENT_SELECTED_ACTION -> it.getParcelableExtra<Content>(CONTENT_SELECTED_KEY).also { content ->
                val intentUri = Uri.parse(content.audioUrl)
                // Checks whether to update Uri passed in Intent Bundle.
                if (!intentUri.equals(uri)) {
                    uri = intentUri
                    player?.prepare(ProgressiveMediaSource.Factory(
                            DefaultDataSourceFactory(
                                    this,
                                    Util.getUserAgent(this, getString(app_name))))
                            .createMediaSource(uri))
                    player?.playWhenReady = true
                    // Calling 'startForeground' in 'buildNotification(...)'.          
                    buildNotification(intent.getParcelableExtra(CONTENT_SELECTED_KEY))
                }
            }
        }
    }
    return super.onStartCommand(intent, flags, startId)
}

// Calling 'startForeground' in 'onNotificationStarted(...)'.
private fun buildNotification(content: Content): Unit? {
    playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
            this,
            content.title,
            app_name,
            if (!content.audioUrl.isNullOrEmpty()) 1 else -1,
            object : PlayerNotificationManager.MediaDescriptionAdapter {
                override fun createCurrentContentIntent(player: Player?) = ...
                override fun getCurrentContentText(player: Player?) = ...
                override fun getCurrentContentTitle(player: Player?) = ...
                override fun getCurrentLargeIcon(player: Player?,
                                                 callback: PlayerNotificationManager.BitmapCallback?) = ...
            },
            object : PlayerNotificationManager.NotificationListener {
                override fun onNotificationStarted(notificationId: Int, notification: Notification) {
                    startForeground(notificationId, notification)
                }
                override fun onNotificationCancelled(notificationId: Int) {
                    stopForeground(true)
                    stopSelf()
                }
            })
    return playerNotificationManager.setPlayer(player)
}

2

我在Pixel 3,Android 11上遇到了一个问题,当我的服务运行时间非常短时,前台通知无法消失。

在stopForeground() stopSelf()之前添加100毫秒延迟似乎有所帮助

有人在这里写道应该先调用stopForeground()再调用stopSelf()。我无法确认,但我猜这样做不会有影响。

public class AService extends Service {

@Override
public void onCreate() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        startForeground(
            getForegroundNotificationId(),
            channelManager.buildBackgroundInfoNotification(getNotificationTitle(), getNotificationText()),
            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
    } else {
        startForeground(getForegroundNotificationId(),
            channelManager.buildBackgroundInfoNotification(getNotificationTitle(), getNotificationText())
        );
    }

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    startForeground();

    if (hasQueueMoreItems()) {
        startWorkerThreads();
    } else {
        stopForeground(true);
        stopSelf();
    }
    return START_STICKY;
}

private class WorkerRunnable implements Runnable {

    @Override
    public void run() {

        while (getItem() != null && !isLoopInterrupted) {
            doSomething(getItem())   
        }

        waitALittle();
        stopForeground(true);
        stopSelf();
    }

    private void waitALittle() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
}

2

可能的一个问题是AndroidManifest文件中没有启用Service类。请检查一下。

<service
        android:name=".AudioRecorderService"
        android:enabled="true"
        android:exported="false"
        android:foregroundServiceType="microphone" />

1
这是我需要在清单文件中添加的内容,以便能够继续播放音频:<service android:name=".common.audio.AudioPlayerService" android:foregroundServiceType="mediaPlayback" - Dan Alboteanu

2
我在@humazed的回答中添加了一些代码,所以没有初始通知。这可能是一个解决方法,但对我有效。"最初的回答"
@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("")
                    .setColor(ContextCompat.getColor(this, R.color.transparentColor))
                    .setSmallIcon(ContextCompat.getColor(this, R.color.transparentColor)).build();

            startForeground(1, notification);
        }
}

我在小图标中添加了transparentColor和通知中的颜色。 它会起作用。最初的回答:

我在小图标和通知中添加了透明颜色和颜色。


1

在 Service 或 IntentService 创建后立即调用 startForeground 方法。就像这样:

import android.app.Notification;
public class AuthenticationService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();
        startForeground(1,new Notification());
    }
}

1
致命异常:主要的 android.app.RemoteServiceException: startForeground的通知不好:java.lang.RuntimeException:服务通知的无效通道:Notification(channel=null pri=0 contentView=null vibrate=null sound=null defaults=0x0 flags=0x40 color=0x00000000 vis=PRIVATE) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6626) - Gerry

1

在调用context.startForegroundService(service_intent)函数之前,我会检查PendingIntent是否为空。

这对我有用。

PendingIntent pendingIntent=PendingIntent.getBroadcast(context,0,intent,PendingIntent.FLAG_NO_CREATE);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pendingIntent==null){
            context.startForegroundService(service_intent);
        }
        else
        {
            context.startService(service_intent);
        }
}

这实际上只是启动服务的方式,而不是如果为null则启动前台服务。否则,if语句必须嵌套,否则应用程序在某个时候尝试使用较新版本的服务时会崩溃。 - a54studio

0
我的十多年的应用程序突然开始出现这个错误。我搜索了几个小时,然后意识到我在Android Studio中启动我的应用程序时,布局检查器仍然处于开启和运行状态。
一旦我关闭它,一切都恢复正常了。
希望这对某人有所帮助。

0

我已经通过使用startService(intent)代替Context.startForeground() 并在 super.OnCreate()之后立即调用startForegound()解决了启动服务的问题。此外,如果您需要在开机时启动服务,可以启动一个Activity并在引导广播中启动服务。虽然这不是永久性的解决方案,但它有效。


在一些小米设备中,活动无法从后台启动。 - Mateen Chaudhry

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