我在Android O操作系统上使用服务类。

我计划在后台使用服务。

Android文档指出

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

如果使用startForegroundService(),服务抛出以下错误。

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

这有什么问题?


当前回答

我在Pixel 3和Android 11中遇到了一个问题,当我的服务运行得很短时,前台通知没有被取消。

在stopForeground() stopSelf()之前添加100ms的延迟似乎有所帮助。

人们在这里写道,stopprospect()应该在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();
        }
    }
}
}

其他回答

我有一个解决这个问题的办法。我已经在自己的应用(DAU超过30万)中验证了这一修复方法,它至少可以减少95%的这种崩溃,但仍然不能100%避免这个问题。

即使您确保在服务启动后调用start前台(),也会发生此问题。这可能是因为在很多情况下,服务创建和初始化过程已经花费了超过5秒的时间,那么无论何时何地调用startForeground()方法,这个崩溃都是不可避免的。

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

Do not use startForegroundService at the first place, use bindService() with auto_create flag. It will wait for the service initialization. Here is the code, my sample service is 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); Then here is MusicBinder implementation: /** * 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(); } } The most important part, MusicService implementation, forceForeground() method will ensure that startForeground() method is called just after service start: 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); } } } If you want to run the step 1 code snippet in a pending intent, such as if you want to start a foreground service in a widget (a click on widget button) without opening your app, you can wrap the code snippet in a broadcast receiver, and fire a broadcast event instead of start service command.

仅此而已。希望能有所帮助。祝你好运。

我有一个小部件,当设备处于清醒状态时,它会相对频繁地进行更新,我在短短几天内就看到了数千次崩溃。

问题触发

我甚至在我的Pixel 3 XL上也注意到了这个问题,而我本以为这款设备负载并不大。所有代码路径都被startForeground()覆盖。但后来我意识到,在很多情况下,我的服务可以很快地完成工作。我认为触发我的应用程序的原因是,在系统真正抽出时间显示通知之前,服务已经结束。

解决方案/解决方案

我摆脱了所有的崩溃。我所做的就是删除对stopSelf()的调用。(我正在考虑延迟停止,直到我非常确定通知被显示,但我不希望用户看到通知,如果它不是必要的。)当服务空闲一分钟或系统正常销毁它而不抛出任何异常时。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    stopForeground(true);
} else {
    stopSelf();
}

更新onStartCommand中的数据(…)

上绑定(...

与onCreate(…)相比,onBind(…)是一个更好的生命周期事件来初始化start前台,因为onBind(…)传递了一个Intent,其中可能包含初始化服务所需的Bundle中的重要数据。然而,这是不必要的,因为onStartCommand(…)是在服务第一次创建或后续多次调用时调用的。

onStartCommand(...)

onStartCommand(…)中的start前台是很重要的,以便在服务创建后更新它。

当ContextCompat.startForegroundService(…)在服务创建后被调用时,onBind(…)和onCreate(…)没有被调用。因此,更新后的数据可以通过Intent Bundle传递到onStartCommand(…)来更新服务中的数据。

样本

我使用这个模式在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)
}

我只是检查PendingIntent null或或不是之前调用 context.startForegroundService (service_intent)功能。

这对我很有用

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);
        }
}

因为每个来这里的人都遭受着同样的痛苦,我想分享我的解决方法,之前没有人尝试过(在这个问题中)。我可以向您保证,它是工作的,甚至在一个停止的断点上确认这个方法。

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

Bind the service to a context with a binder from the service before calling Context.startForegroundService() If the bind is successful, call Context.startForegroundService() from the service connection and immediately call Service.startForeground() inside the service connection. IMPORTANT NOTE: Call the Context.bindService() method inside a try-catch because in some occasions the call can throw an exception, in which case you need to rely on calling Context.startForegroundService() directly and hope it will not fail. An example can be a broadcast receiver context, however getting application context does not throw an exception in that case, but using the context directly does.

这甚至可以在我绑定服务后和触发“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 . onserviceconnconnected()方法。然后,处理上面所示代码中的逻辑。示例代码如下所示:

// 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));
}

它似乎在我开发的应用程序上工作,所以当你尝试时它应该也能工作。