【Android】四大组件之 Service

基本概念

Service(服务)是一种可以在后台执行长时间运行操作而不提供界面的应用组件,服务可以由其他应用组件启动。

在当前应用启动一个服务后,即使用户切换到其他应用,服务仍可以在后台继续运行。

此外,其他组件可以通过绑定(bindService)与 Service 建立绑定关系,并与之进行交互,甚至是执行进程间通信(IPC)。

服务可以在后台处理网络事务,播放音乐,执行文件 io 或与内容提供程序进行交互。

目前为止,随着Android系统的不断升级,对后台管理的限制越来越多,所以到现在为止,作为后台服务这种形式,反而是越来越少了。而且现在也出现了很多替代的方案而已更好的去实现后台任务,例如:WorkManager 去处理可延时的耗时任务。

Service的三种类型

后台服务:后台服务一般用于执行用户不会直接注意到的操作。例如,如果应用在后台使用某个服务来压缩其存储空间,则此服务通常是后台服务。一般来说,可以通过startService(intent)方法来启动一个后台服务。

绑定服务:当应用组件通过 bindService() 绑定到服务时,服务即处于绑定状态(此时可以将service看作是服务器,绑定的组件看作是客户端),绑定服务会提供客户端-服务器接口,以便组件与服务进行交互、发送请求、接受结果,甚至是利用进程间通信跨进程执行这些操作,仅当与另一个应用组件绑定时,绑定服务才会运行,多个组件可同时绑定到同一个服务,全部取消绑定后,该服务会被销毁。这种service的形式是比较常用的。注意:只有 Activity、Service和内容提供程序可以绑定到服务,无法从广播接收器绑定到服务

前台服务:与后台服务不同的是,前台服务一般用于执行一些用户能注意到的操作。例如:音频应用会使用前台服务来播放音频曲目,位置信息提醒,通知栏显示导航信息等场景后可以使用前台服务。前台服务必须显示通知,即使用户停止与应用交互,前台服务仍会继续运行。因为它是用户可以感知到存在的,所以前台服务的优先级比较高,等同与一个处于前台的 Activity 的优先级,几乎不太可能被系统杀掉。

另外需要注意的是,服务在其托管进程的主线程中运行,它不会创建自己的线程,也不会在单独的进程中运行(除非另行指定)。如果服务将执行任何cpu密集型工作或阻止性操作(例如MP3播放或联网),则应通过在服务内创建新线程来完成这项工作。

后台服务

通过 startService(intent) 的形式来启动Service,此时Service就会以后台的形式存在,并无限期运行,只有执行stopService()方法或者stopSelf()方法或者app进程销毁后,后台服务才会停止。

第一次执行startService(intent)时,Service的onCreate方法就会回调。下面举个简单例子演示一下后台服务的特性:

首先创建一个MyNewService继承自Service:在onCreate回调方法中开启了一个协程,每隔1秒打印一次日志。同时在其他生命周期方法中也加了日志,方便查看各个方法的回调时机。注意:别忘了在AndroidManfest中注册这个Service哦!!!

class MyNewService : Service(), CoroutineScope by MainScope() {

    override fun onCreate() {
        super.onCreate()
        Log.d("textService", "onCreate")
        doSomething()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        //内部调用了onStart方法,所以onStart方法已经被弃用了
        Log.d("textService", "onStartCommand")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        //执行onDestroy的时候必须手动释放资源,否则协程会一直执行。
        job?.cancel()
        Log.d("textService", "onDestroy")
    }

    private var job: Job? = null
    
    private fun doSomething() {
        job?.cancel()
        job = launch {
            var i = 0
            while (true) {
                Log.d("textService", "doSomething = ${i++}")
                delay(1000)
            }
        }
    }
    

    /**
     * 以下两个回调方法onBind/onUnbind只有在Service作为绑定服务的时候才会用到,作为后台服务一般是用不到的
     * */
    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d("textService", "onUnbind")
        return super.onUnbind(intent)
    }
}

下面再看下Activity的代码:有两个按钮,分别点击会执行startService()和stopService()方法。

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    /**
     * 点击开启服务按钮
     * */
    fun clickStartBtn(view: View) {
        //这里稍微说明一下:also用于对象生成后同时做某事。相似的还有apply一般用于初始化对象。然后还有别的内联函数,不展开了...
        Intent(this, MyNewService::class.java).also {
            startService(it)
        }
    }
    
    /**
     * 点击停止服务按钮
     * */
    fun clickStopBtn(view: View) {
        Intent(this, MyNewService::class.java).also {
            stopService(it)
        }
    }

}

当我们点击【开启服务】的时候的。日志是这样打印的:

D/textService: onCreate
D/textService: doSomething = 0
D/textService: onStartCommand
D/textService: doSomething = 1
D/textService: doSomething = 2
…………

先执行了onCreate方法,然后协程开始运行,然后onStartCommand方法再被执行。

当我们再次点击【开启服务】的时候,onStrarCommand方法会重复执行,但是onCreate方法不会再执行,日志如下:

D/textService: doSomething = 97
D/textService: onStartCommand
D/textService: doSomething = 98
D/textService: onStartCommand
D/textService: doSomething = 99
…………

当我们点击【停止服务】的时候,日志是这样的:

D/textService: doSomething = 373
D/textService: onDestroy

可以看到,最终执行了onDestroy方法,然后doSomething日志也不再打印了。

注意看上面的代码,我们在onDestroy方法回调的时候,手动将协程作业停止了,如果我们不手动停止协程,仅执行了stopService方法,系统是不会帮我们把协程资源回收的,所以我们必须手动回收资源如果不想这么做,那么可以使用官方提供的LifecycleService 类,它继承自Service并实现了LifecyclerOwner接口,是一个生命周期感知服务组件。

绑定服务

通过bindServce(Intent, ServiceConnection, flag)启动service,此时service就可以看作是一个绑定服务。如果组件通过调用 bindService()来创建服务,并且未调用 onStartCommand(),则服务只会在该组件与其绑定时运行。当该服务与其所有组件取消绑定后,系统便会将其销毁。绑定服务的优先在于,绑定组件与服务可以进行交互、发送请求、接受结果等。

下面我们来演示一下绑定服务的简单使用,先来看看MyNewService类:

class MyNewService : Service(), CoroutineScope by MainScope() {

    override fun onCreate() {
        super.onCreate()
        Log.d("textService", "onCreate")
        doSomething()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("textService", "onStartCommand")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        //执行onDestroy的时候必须手动释放资源,否则协程会一直执行。
        job?.cancel()
        Log.d("textService", "onDestroy")
    }

    private var job: Job? = null

    private fun doSomething() {
        job?.cancel()
        job = launch {
            var i = 0
            while (true) {
                Log.d("textService", "doSomething = ${i++}")
                delay(1000)
            }
        }
    }

    var serviceValue = 999

    private val binder = LocalBinder()

    /**
     * 通过内部类可以拿到当前的service对象
     * */
    inner class LocalBinder : Binder() {
        fun getService(): MyNewService = this@MyNewService
    }

    override fun onBind(intent: Intent): IBinder {
        Log.d("textService", "onBind")
        return binder
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d("textService", "onUnbind")
        return super.onUnbind(intent)
    }
}

然后在MainActivity类中,我们同样定义了两个按钮,分别点击会执行绑定服务和解绑服务逻辑:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    /**
     * 定义一个ServiceConnection类
     * */
    private val connection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, ibinder: IBinder) {
            //在这里可以拿到Service的onBind()方法中返回的IBinder对象,通过这个对象,我们可以与Service进行通信
            val binder = ibinder as MyNewService.LocalBinder
            val mService = binder.getService()
            Log.d("textService", "onServiceConnected serviceValue = ${mService.serviceValue}")
        }

        override fun onServiceDisconnected(arg0: ComponentName) {
            Log.d("textService", "onServiceDisconnected..")
        }
    }

    /**
     * 点击绑定服务按钮
     * */
    fun clickToBindService(view: View) {
        Intent(this, MyNewService::class.java).also {
            bindService(it, connection, Context.BIND_AUTO_CREATE)
        }
    }

    /**
     * 点击解绑服务按钮
     * */
    fun clickToUnbindService(view: View) {
        unbindService(connection)
    }

}

当我们点击【绑定服务】的时候的。日志是这样打印的:

D/textService: onCreate
D/textService: onBind
D/textService: doSomething = 0
D/textService: onServiceConnected serviceValue = 999
D/textService: doSomething = 1
D/textService: doSomething = 2
…………

可以看到,当执行了bindService(it, connection, Context.BIND_AUTO_CREATE)方法后,Service的onCreate方法和onBind方法开始回调,然后onCreate方法中的协程任务也开始执行。其中,onBind()方法会把一个IBinder对象返回到ServiceConnection中的onServiceConnected方法。然后ServiceConnection的onServiceConnected方法开始回调,通过IBinder对象,我们可以与Service进行通信。日志中可以看到,我们把Service的serviceValue值取出并打印了出来。

当我们点击【解绑服务】的时候的。日志是这样打印的:

D/textService: doSomething = 55
D/textService: doSomething = 56
D/textService: onUnbind
D/textService: onDestroy

可以看到,onUnbind方法和onDestroy方法回调了。注意,当调用**unbindService(connection)**时ServiceConnection中的onServiceDisconnected并没有被回调,这是应为这个方法只有在Service被不正常kill之后才会回调,通过这个回调是为了让用户知道服务被kill了,然后可以重新启动服务。

前台服务

前台服务用于执行用户可以注意到的操作。前台服务会显示状态栏通知。Android 9.0及之后,要求必须申请如下权限才能使用前台服务:

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

如果在前台服务中要实现用户的位置,或者要使用相机等,还需要申请一些更具体的权限,官网有详细说明,这里就不展开说了。

注意:在Android 12以上运行前台服务,系统会等待 10 秒,然后才会显示与前台服务关联的通知。

下面我们简单的演示一下前台服务的使用,先看一下MyNewService类实现:

class MyNewService : Service(), CoroutineScope by MainScope() {

    private val channelId = "1"

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate() {
        super.onCreate()
        Log.d("textService", "onCreate")

        //创建channel
        val name = "my channel."
        val importance = NotificationManager.IMPORTANCE_DEFAULT
        val mChannel = NotificationChannel(channelId, name, importance)
        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(mChannel)

        //创建一个PendingIntent,当点击通知时,可以跳到指定Activity
        val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let {
            PendingIntent.getActivity(this, 0, it, 0)
        }

        //创建通知对象
        val notification: Notification = Notification.Builder(this, channelId)
            .setContentTitle("标题")
            .setContentText("内容")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentIntent(pendingIntent)
            .build()

        //注意这个id不能是0,否则会报异常
        startForeground(1, notification)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("textService", "onStartCommand")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("textService", "onDestroy")
    }

    override fun onBind(intent: Intent?): IBinder? {
        TODO("Not yet implemented")
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d("textService", "onUnbind")
        return super.onUnbind(intent)
    }
}

MyNewService类的onCreate方法中,创建了一个Channel对象,并为其设置了name,importance(优先级),一般来说,importance我们用IMPORTANCE_DEFAULT默认优先级即可,表示这个前台服务的优先级跟前台可见的activity优先级一致,一般不会被系统强杀,与之对应的,还有其他优先级选项,这里就不展开说了。然后,我们还创建了一个PendingIntent,用来当点击通知时,可以跳到指定Activity。接下来,又创建了一个通知对象,并为其设置了图标,标题,正文等信息。最后执行startForeground(id: Int, notification: Notification)方法将服务设置为前台服务。

注意:在启动一个服务之后,在几秒之内执行startForeground(id: Int, notification: Notification)方法,就会将这个服务以前台的形式启动。当超过一定的时间后再执行startForeground()方法,就不能设置为前台服务了。

startForeground(id: Int, notification: Notification)的传参

id不能等于0,否则前台服务是起不来的,

notification是一个通知的对象,可以通过**NotificationCompat.Builder(this, ChannelId).build()**创建,同时可以指定通知的图标,标题,正文等信息。ChannelId在Android 8.0之前可以是任意字符串的,但是在Android 8.0之后,必须创建出一个真正的Channel对象,就不能这样乱搞了,原因就是为了让用户能够自由的选择接受哪些通知和不接受哪些通知,把选择权交给用户。

下面我们再来看看MainActivity类的实现,我们同样定义了两个按钮,分别点击会执行开启服务和停止服务逻辑::

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    /**
     * 点击绑定服务按钮
     * */
    fun clickToStartService(view: View) {
        Intent(this, MyNewService::class.java).also {
            startService(it)
        }
    }

    /**
     * 点击解绑服务按钮
     * */
    fun clickToStopService(view: View) {
        Intent(this, MyNewService::class.java).also {
            stopService(it)
        }
    }

}

当我们点击【开启前台服务】的时候的。日志是这样打印的:

D/textService: onCreate
D/textService: onStartCommand

可以看到,跟启动一个后台服务的生命周期是一样的,不同的是,通知栏会出现一个前台服务对应的通知,

当我们点击【停止前台服务】的时候的。日志是这样打印的:

D/textService: onDestroy

回调了onDestroy方法,同时通知栏的通知也会随之关闭。

Service的生命周期

onCreate()

首次创建服务时,系统会在调用 onStartCommand()或onBind() 之前调用onCreate()方法来执行一次性设置程序。如果服务已在运行,则不会调用此方法。

onStartCommand()

内部其实调用了服务的onStart,所以onStart()这个生命周期已被弃用。

每次执行startService(intent)时,都会回调Service的onStartCommand生命周期。在执行bindService(intent)时,是不会回调Service的onStartCommand的

如果系统终止了服务,那么会在资源可用时立即重启服务。这取决于你的onStartCommand()的返回值

onDestroy()

作为后台服务/前台服务时,当我们手动调用stopService(intent)stopSelf()方法的时候,服务就停止了。然后回调onDestroy()。

作为绑定服务时,当我们调用unbindService(connection)时,服务就停止了。然后回调onDestroy()。

服务应通过实现onDestroy()方法来清理任何资源,如线程、注册的监听器、接收器等。

onBind()

Service的成员方法。返回一个IBinder对象。

当我们把服务作为一个绑定服务的时候,才会用到这个回调方法。如果Service是后台服务的形式,一般是不需要用到这个方法的。在Service是绑定的形式时,需要实现这个方法。

作为绑定服务时,当我们调用bindService()时,回调完onCreate()方法,就会马上回调onBind()方法。

onUnbind()

同理,当服务作为绑定服务时,当我们调用unbindService(connection)时,就会回调onUnbind()方法。

onStartCommand返回值

当调用startService(intent)方法启动Service时,会回调Service的onStartCommand生命周期。

如果面临资源匮乏时,系统可能会销毁掉当前运行的Service,然后待内存充足的时候可以重新创建Service,Service被强制销毁并再次重建的行为,依赖于Service中onStartCommand方法的返回值,下面我来列举几个它的常用的返回值:

  • START_NOT_STICKY:如果onStartCommand返回该值,表示当Service被强杀后,不会重新创建该Service
  • START_STICKY:当Service被强杀后,系统仍会将该Service设置为运行状态,但是不再保存onStartCommand方法传入的intent对象,当系统资源充足时,会尝试再次重新创建该Service,并执行onStartCommand回调方法,但是onStartCommand回调方法的Intent参数为null。这也是onStartCommand的默认的返回值。
  • START_REDELIVER_INTENT:该返回值与START_STICKY类似,不同的是,系统会保留Service被强杀前的最后一次传入onStartCommand方法中的Intent对象
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一场雪ycx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值