Android 使用前台服务

启动前台服务

前台服务可以给用户提供界面上的操作。
每个前台服务都必须要在通知栏显示一个通知(notification)。用户可以感知到app的前台服务正在运行。
这个通知(notification)默认是不能移除的。服务停止后,通知会被系统移除。
当用户不需要直接操作app,app需要给用户一个状态显示的时候,可以用前台服务。

市面上的app,例如各类音乐app
music-1.png

本文针对Android 8(Oreo,SDK_INT 26)及以后的版本。

使用说明

本例会使用1个Activity和1个Service。演示如何启动前台服务,停止服务。

manifest

在manifest里注册ForegroundDemoActForegroundService1。并且申请权限FOREGROUND_SERVICE

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

    <!--  前台服务权限  -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application ...  >

        <service android:name=".service.foreground.ForegroundService1" />

        <activity
            android:name=".service.foreground.ForegroundDemoAct"
            android:launchMode="singleTop" />

    </application>
</manifest>

Activity的启动模式我们选择了singleTop。是为了方便演示点击通知时候的跳转效果。

启动前台服务

在activity中启动服务,调用startForegroundService(Intent)方法。

startForegroundService(Intent(applicationContext, ForegroundService1::class.java))

然后在service中,需要对应地使用startForeground方法。

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "onStartCommand flags:$flags, startId:$startId [$this] ${Thread.currentThread()}")

        val pendingIntent: PendingIntent =
                Intent(this, ForegroundDemoAct::class.java).let { notificationIntent ->
                    PendingIntent.getActivity(this, 0, notificationIntent, 0)
                }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val chanId = "f-channel"
            val chan = NotificationChannel(chanId, "前台服务channel",
                    NotificationManager.IMPORTANCE_NONE)
            chan.lightColor = Color.BLUE
            chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
            val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            service.createNotificationChannel(chan)
            Log.d(TAG, "服务调用startForeground")

            val notification: Notification =
                    Notification.Builder(applicationContext, chanId)
                            .setContentTitle("RustFisher前台服务")
                            .setContentText("https://an.rustfisher.com")
                            .setSmallIcon(R.drawable.f_zan_1)
                            .setContentIntent(pendingIntent)
                            .build()
            startForeground(1, notification)
        } else {
            Log.d(TAG, "${Build.VERSION.SDK_INT} < O(API 26) ")
        }
        return super.onStartCommand(intent, flags, startId)
    }

我们来看service里的这段代码。创建了一个简单的Notification

  • PendingIntent会被分配给Notification,作为点击通知后的跳转动作
  • 使用NotificationManager先创建了一个NotificationChannel
  • Notification.Builder配置并创建一个Notification,例如配置标题,内容文字,图标等
  • 启动前台服务,调用startForeground(1, notification)方法

在设备上会显示出一个通知(以OnePlus5为例)
f1.png

点击这个通知,会跳转到ForegroundDemoAct。这是之前用PendingIntent设置的。

停止服务

可以用stopService来停止服务

stopService(Intent(applicationContext, ForegroundService1::class.java))

这样Service退出,走onDestroy方法。

停止前台服务

在Service中调用stopForeground(boolean)方法,能停止前台,但是不退出整个服务。
这个boolean表示是否取消掉前台服务的通知。false表示保留通知。

例如在Service中调用

stopForeground(false)

服务变成了后台服务,并没有退出。此时对应的通知可以滑动取消掉。

报错信息

ANR

在Activity中调用startForegroundService(Intent)启动服务,但是不调用Service.startForeground()
一加5手机Android10运行log如下

2021-08-26 23:03:25.352 25551-25551/com.rustfisher.tutorial2020 D/rustAppUseStartService: 调用 startForegroundService 主线程信息Thread[main,5,main]
2021-08-26 23:03:25.368 25551-25551/com.rustfisher.tutorial2020 D/rustAppForeground1: onCreate Thread[main,5,main] rustfisher.com
2021-08-26 23:03:25.370 25551-25551/com.rustfisher.tutorial2020 D/rustAppForeground1: onStartCommand flags:0, startId:1 [com.rustfisher.tutorial2020.service.foreground.ForegroundService1@c77d408] Thread[main,5,main]
2021-08-26 23:03:35.375 1596-1720/? W/ActivityManager: Bringing down service while still waiting for start foreground: ServiceRecord{53d70f2 u0 com.rustfisher.tutorial2020/.service.foreground.ForegroundService1}
2021-08-26 23:03:35.382 25551-25551/com.rustfisher.tutorial2020 D/rustAppForeground1: onDestroy [com.rustfisher.tutorial2020.service.foreground.ForegroundService1@c77d408] Thread[main,5,main]

2021-08-26 23:03:52.956 1596-1720/? E/ActivityManager: ANR in com.rustfisher.tutorial2020
    PID: 25551
    Reason: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{53d70f2 u0 com.rustfisher.tutorial2020/.service.foreground.ForegroundService1}

Bad notification

我们在ForegroundService1的方法onStartCommand里加入startForeground
如果startForeground(0, noti)的id传入0,则会报错RemoteServiceException

 29871-29871/com.rustfisher.tutorial2020 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.rustfisher.tutorial2020, PID: 29871
    android.app.RemoteServiceException: Bad notification for startForeground

小结

  • 通过startForegroundService()启动前台服务,必须在Service中有startForeground(),否则会ANR,或者崩溃
  • startForeground()中的id不能为0,notification不能为null
  • 服务可以退出前台stopForeground

参考

(完)