Bootstrap

Android的前台服务

概述

前台服务是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。前台服务必须为状态栏提供通知,将其放在运行中的标题下方。这意味着除非将服务停止或从前台移除,否则不能清除该通知。

在 Android 8.0(API 级别 26)及更高版本中,系统对后台服务进行了限制,如果想要一直保持服务的运行就需要将服务设置为前台服务。前台服务与普通服务的区别在于它会有个通知在状态栏显示。当然有时可能也不仅仅是为了防止服务被回收才使用前台服务,有些项目的需要要求必须使用前台服务。如音乐播放、小说听书、天气等软件,这些都需要设置为前台服务,跟随进程的销毁而销毁。有些不仅仅是依赖应用进程,也可设置为系统白名单,保持一直运行的状态。

需要注意的是:应尽量限制应用使用前台服务。只有当应用执行的任务需供用户查看(即使该任务未直接与应用交互)时,您才应使用前台服务。因此,前台服务必须显示优先级为 PRIORITY_LOW 或更高的状态栏通知,这有助于确保用户知道应用正在执行的任务。如果某操作不是特别重要,您希望使用最低优先级通知,则可能不适合使用服务;此时,您可以考虑使用WorkManager、JobScheduler

每个运行服务的应用都会给系统带来额外负担,从而消耗系统资源。如果应用尝试使用低优先级通知隐藏其服务,则可能会降低用户正在主动交互的应用的性能。因此,如果某个应用尝试运行拥有最低优先级通知的服务,则系统会在抽屉式通知栏的底部调用出该应用的行为。

例如,应将服务播放音乐的音乐播放器设置为在前台运行,因为用户会明确意识到其操作。状态栏中的通知可能表示正在播放的歌曲,并且其允许用户通过启动 Activity 与音乐播放器进行交互。同样,如果应用允许用户追踪其运行,则需通过前台服务来追踪用户的位置。

注意:如果应用面向 Android 9(API 级别 28)或更高版本并使用前台服务,则其必须请求 FOREGROUND_SERVICE 权限。这是一种普通权限,因此,系统会自动为请求权限的应用授予此权限。如果面向 API 级别 28 或更高版本的应用试图创建前台服务但未请求 FOREGROUND_SERVICE,则系统会抛出 SecurityException。

有关前台服务的通知:如果您的应用正在运行“前台服务”(一种长时间在后台运行且用户可以察觉到的 Service,如媒体播放器),则需要发出通知。不能像关闭其他通知那样关闭这种通知。要移除此类通知,必须停止运行服务或者将其从“前台”状态中移除。如要从前台移除服务,请调用 stopForeground()。此方法接受布尔值参数,指示是否需同时移除状态栏通知。

应用场景

前台服务执行用户可以注意到的操作。前台服务显示一个状态栏通知,让用户知道你的应用程序正在前台执行任务,正在消耗系统资源。

应用程序使用前台服务的例子包括:

  • 一个在前台服务中播放音乐的音乐播放器应用程序。通知可能会显示当前正在播放的歌曲。
  • 一种健身应用程序,在获得用户的许可后,在前台服务中记录用户的跑步情况。该通知可能会显示用户在当前健身会话中走过的距离。

只有当你的应用需要执行用户可以注意到的任务时,才使用前台服务,即使他们没有直接与应用程序交互。如果操作的重要性足够低,你想使用最低优先级通知,那么创建一个后台任务。

本文介绍了使用前台服务所需的权限,以及如何启动前台服务并将其从后台移除。它还描述了如何将某些用例与前台服务类型相关联,以及当您从正在后台运行的应用程序启动前台服务时生效的访问限制。

前台服务的特点

默认情况下用户可取消前台服务

从Android 13 (API级别33)开始,默认情况下用户可以取消与前台服务相关的通知。此时只需要用户在通知上执行滑动手势即可。通常情况下,除非前台服务停止或从前台删除,否则通知不会被取消。

如果您希望通知不被用户划掉,则在使用notification.builder创建通知时将true传递给setOngoing() 方法。

立即显示通知的服务

如果前台服务有以下特征之一,系统在服务启动后立即显示相关的通知,即使在运行Android 12或更高版本的设备上:

  • 该服务与包含操作按钮的通知相关联。
  • 该服务的前台服务类型为mediaPlayback、mediaProjection或phoneCall。
  • 该服务提供了与电话呼叫、导航或媒体播放相关的用例,这些用例在通知的category属性中定义。
  • 在设置通知时,服务通过将FOREGROUND_SERVICE_IMMEDIATE传递给setForegroundServiceBehavior()来选择退出行为。
<manifest ...>
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
    <application ...>
      <service
          android:name=".MyMediaPlaybackService"
          android:foregroundServiceType="mediaPlayback"
          android:exported="false">
      </service>
    </application>
</manifest>

在Android 13 (API级别33)或更高版本上,如果用户拒绝通知权限,他们仍然会在任务管理器中看到与前台服务相关的通知,但在通知抽屉中看不到它们。

申请前台服务权限

针对Android 9 (API级别28)或更高版本并使用前台服务的应用程序需要请求FOREGROUND_SERVICE权限,如下面的代码片段所示。这是一个正常的权限,所以系统会自动将其授予请求应用程序。

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

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

注意:如果目标API级别为28或更高的应用程序试图创建前台服务而没有请求FOREGROUND_SERVICE权限,系统将抛出一个SecurityException。

启动一个前台服务

  • 紧接上文的案例,这里首先添加前台服务的权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
  • 在请求系统将某个服务作为前台服务运行之前,请先启动该服务本身: 在MainActivity中修改启动服务的方式如下:
//启动一个普通后台服务
//startService(startIntent);
 //启动一个前台服务 在api大于26才可使用startForegroundService此方法
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
     startForegroundService(startIntent);
 }else{
     startService(startIntent);
 }

  • 在服务内部,通常是在onStartCommand() 中,你可以请求你的服务在前台运行。为此,调用startForeground()。此方法接受两个参数:一个在状态栏中唯一标识通知的正整数和notification对象本身。接着修改服务内部的代码:

首先创建一个通知,在MyService的oncreate方法和onStartCommand()方法中做如下修改:

public class MyService extends Service {

    public MyService() {
    }

    private Looper serviceLooper;
    private ServiceHandler serviceHandler;
    private NotificationManager manager;

    // 从当前线程接收消息的处理程序
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            try {
                Thread.sleep(5000);
                Log.i("Myservice","当前进程编号"+ Thread.currentThread().getName()+" ·····正在处理任务");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
          //服务处理完成后,使用startId停止服务,这样我们就不会在停止处理另一个作业的服务
//            stopSelf(msg.arg1);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        manager= (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Log.i("Myservice","onCreate");
        //启动运行该服务的线程。因为默认情况下服务通常运行在进程的主线程中,我们不希望阻塞主线程。所以创建了一个单独的线程,
        // 我们还将其设置为后台优先级,这样cpu密集型工作就不会破坏我们的UI。
        HandlerThread thread = new HandlerThread("ServiceStartArguments",
                Process.THREAD_PRIORITY_BACKGROUND);
        thread.start();

        // Get the HandlerThread's Looper and use it for our Handler
        serviceLooper = thread.getLooper();
        serviceHandler = new ServiceHandler(serviceLooper);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("Myservice","onStartCommand");

        // If the notification supports a direct reply action, use
// PendingIntent.FLAG_MUTABLE instead.
        Intent notificationIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent =
                PendingIntent.getActivity(this, 0, notificationIntent,
                        PendingIntent.FLAG_IMMUTABLE);

        Notification notification =
                null;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {

            //只在Android O之上需要渠道
            NotificationChannel notificationChannel = new NotificationChannel("channelid1","channelname",NotificationManager.IMPORTANCE_HIGH);
            //如果这里用IMPORTANCE_NOENE就需要在系统的设置里面开启渠道,通知才能正常弹出
            manager.createNotificationChannel(notificationChannel);

            notification = new Notification.Builder(this, "channelid1")
                    .setContentTitle(getText(R.string.notification_title))
                    .setContentText(getText(R.string.notification_message))
                    .setSmallIcon(R.drawable.ic_launcher_background)
                    .setContentIntent(pendingIntent)
                    .setTicker(getText(R.string.ticker_text))
                    .build();
        }

// Notification ID cannot be 0.
        startForeground(1, notification);

        Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

//对于每个开始请求,发送一个消息来开始一个作业,并传递启动ID,这样我们就知道当我们完成作业时我们正在停止哪个请求
        Message msg = serviceHandler.obtainMessage();
        msg.arg1 = startId;
        //在当前线程下执行服务的任务。
        serviceHandler.sendMessage(msg);

        // If we get killed, after returning from here, restart
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i("Myservice","onDestroy");
    }
}

经过上述三步,我们来测试一下效果:

这会在前台开启一个通知,日志可见并没有进行销毁。服务列表中也可查看当前前台服务运行的时间:

这样一个前台任务就创建好了。

注意:状态栏通知必须使用优先级为PRIORITY_LOW或更高。如果你的应用程序试图使用一个优先级较低的通知,系统会在通知抽屉中添加一条消息,提醒用户应用程序使用了前台服务。

后台启动限制

针对Android 12 (API级别31)或更高版本的应用程序不能在后台运行时启动前台服务,除非有一些特殊情况。如果应用程序在后台运行时试图启动前台服务,而前台服务不满足前文中的情况,系统将抛出ForegroundServiceStartNotAllowedException异常。

注意:如果一个应用程序调用Context.startForegroundService()来启动另一个应用程序拥有的前台服务,这里限制只适用于两个应用程序都针对Android 12或更高版本。

检查应用程序是否执行后台启动

为了更好地验证当你的应用程序试图在后台运行时启动前台服务,可以在连接到测试设备或模拟器的开发机器上执行以下ADB命令,每当这种行为发生时就会出现通知:

adb shell device_config put activity_manager \ default_fgs_starts_restriction_notification_enabled true
免除后台启动限制

在以下情况下,即使你的应用程序在后台运行,应用程序也可以启动前台服务:

  • 你的应用从一个用户可见的状态转换,比如一个活动。
  • 当应用程序在一个现有任务的后台堆栈中有一个活动时,服务可以从后台启动一个活动。
  • 您的应用程序使用云消息接收高优先级消息。

注意:如果应用程序没有使用高优先级消息向用户显示时间敏感的内容,系统可以将高优先级消息降级为正常优先级。如果消息的优先级被降级,你的应用程序不能启动前台服务,并且试图启动前台服务会导致ForegroundServiceStartNotAllowedException异常。

因此,在尝试启动前台服务之前,建议检查RemoteMessage.getPriority() 的结果,并确认它是PRIORITY_HIGH。

  • 用户在与应用程序相关的UI元素上执行操作。例如,他们可能与气泡、通知、小部件或活动交互。
  • 应用程序调用精确的Alarm来完成用户请求的操作。
  • 应用程序是设备当前的输入法。
  • 应用程序接收到一个与地理围栏或活动识别转换相关的事件。
  • 在设备重启并在广播接收器中接收到ACTION_BOOT_COMPLETED、ACTION_LOCKED_BOOT_COMPLETED或action_my_package_replace意图动作后。
  • 应用程序在广播接收器中接收ACTION_TIMEZONE_CHANGED、ACTION_TIME_CHANGED或ACTION_LOCALE_CHANGED意图动作。
  • 具有特定系统角色或权限的应用程序,例如设备所有者和配置文件所有者。
  • ........

从前台删除一个服务

要从前台删除服务,请调用stopForeground()。此方法接受一个布尔值,该值指示是否也删除状态栏通知。如果在前台运行时停止服务,则会删除其通知。

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            stopForeground(Service.STOP_FOREGROUND_REMOVE);
        }
        Log.i("Myservice","onDestroy");
    }

此时通知也会被移除。

;