引言

前段时间,小编在修改bug的时候和Service打了不少交道。突然发现,已经很久没碰过Service了,有些东西记忆模糊,那么今天做一个Service总结,方便日后查看。

Service是什么?

Service是Android中实现程序后台运行的解决方案,它非常适合执行哪些不需要和用户交互而且要求长期运行的任务。Service的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service仍然能够保持正常运行。

不过需要注意的是,Service并不是运行在一个独立的进程中,而是依赖于创建Service时所在的应用程序的进程。当某个应用程序进程被杀时,所有依赖于该进程的Service也会停止运行。

另外,也不要被Service的后台概念所迷惑,实际上Service并不会自动打开线程,所有的代码都是默认运行在主线程中。也就是说,我们需要在Service的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。

Service的基本用法

定义一个Service

image-20220916115158211

  • MyService:我们将类的名字定义为MyService
  • Exported:表示是否将这个Service暴露给外部其他程序访问
  • Enabled:表示是否启用这个Service

创建好之后:

class MyService : Service() {

override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
}

可以看到,MyService继承自系统的Service类的。目前MyService中可以算是空空如也,但是有一个onBind()方法特别醒目。这个是Service唯一的抽象方法。后面会讲解onBind的使用,这里暂时不用它。

Service中同样有类似于Activity的生命周期方法,这里咱们先使用,后面会讲解Service的生命周期。

class MyService : Service() {
override fun onCreate() {
super.onCreate()
}

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

override fun onDestroy() {
super.onDestroy()
}

...
}

这些是Service最常用的三个方法,看名字也知道是什么意思,这里就不浪费时间了。

另外,每一个Service都需要在AndroidManifest.xml文件中注册才能生效(这也是四大组件的特点)。不过按照我上述的操作,直接右键New一个Service的话,AndroidStudio就会自动为我们注册,不用再手写,还是很方便的。

<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>

启动和停止Service

下面是两个按钮触发Service

binding.startServiceBtn.setOnClickListener { 
val intent = Intent(this,MyService::class.java)
startService(intent)//启动Service
}
binding.stopServiceBtn.setOnClickListener {
val intent = Intent(this,MyService::class.java)
stopService(intent)//停止Service
}

可以看到在startServiceBtn按钮的点击事件里,我们构建了一个Intent对象,并调用startService()方法来启动MyService。在stopServiceBtn按钮的点击事件里,我们同样构建了一个Intent对象,并调用stopService()方法来停止MyService。startService()stopService()方法都是定义在Context类中的,所以我们在Activity里可以直接调用这两个方法。另外,Service也可以自我停止运行,只需要在Service内调用stopself()方法即可。

从Android8.0之后,应用的后台功能被大幅削减。现在只有当应用保持在前台可见状态的情况下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时可能被系统回收。这样改动,是为了防止许多恶意的应用程序长期在后台占用手机资源,从而导致手机变得越来越卡。

那么,onCreate()和onStartCommand()方法的区别是什么呢?

  • onCreate()方法是在Service第一次创建的时候调用
  • 而onStartCommand()方法则在每次启动Service的时候都会调用

Activity和Service进行通信

通过上述的例子,我们可以知道Activity只是起到启动Service的作用,启动完之后,二者就基本没有关系了。

那如果我们想Activity与Service有关系,或者说Activity能控制Service,能交互,又该怎么办呢?那就要用到前面被搁置的onBind()

我们在MyService中模拟提供下载功能,然后在Activity中可以决定何时开始下载,以及随时查看下载进度。实现这个功能的思路是创建一个专门的Binder对象来对下载功能进行管理。修改MyService中的代码。如下所示:

class MyService : Service() {
private val mBinder = DownloadBinder();
class DownloadBinder : Binder(){
fun startDownloader(){
Log.d("MyService","startDownload executed")
}
fun getProgress():Int{
Log.d("MyService","getProgress executed")
return 0
}
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
...


}

这里,我们新建了一个DownloadBinder类,并让它继承自Binder,然后在它的内部提供了开始下载以及查看下载进度的方法。当然这只是两个模拟方法。

接着,在MyService中创建了DownloadBinder的实例,然后再onBinder()方法里返回了这个实例,这样MyService中准备的工作就完成了。

lateinit var downloadBinder:MyService.DownloadBinder
private val connection = object :ServiceConnection{
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
downloadBinder = service as MyService.DownloadBinder
downloadBinder.startDownloader()
downloadBinder.getProgress()
}

override fun onServiceDisconnected(name: ComponentName?) {

}

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

binding.bindServiceBtn.setOnClickListener {
val intent = Intent(this,MyService::class.java)
bindService(intent,connection,Context.BIND_AUTO_CREATE)//绑定Service
}
binding.unbindServiceBtn.setOnClickListener {
unbindService(connection)//解绑Service
}
  • 首先,我们创建了一个ServiceConnection的匿名内部类实现,并在里面重写了onServiceConnected()方法和onServiceDisconnected()方法

    • onServiceConnected():会在Activity与Service成功绑定的时候调用
    • onServiceDisconnected():只有在Service的创建进程崩溃或被杀掉的时候才会调用,这个方法不太常用

    onServiceConnected()方法中,我们通过向下转型得到了DownloadBinder的实例,有了这个实例,Activity和Service之间的关系就变得非常紧密了。

  • 当然,需要真正的绑定还需要bindService(),需要三个参数:

    • Intent对象
    • ServiceConnection实例,已经创建
    • 一个标志位,这里传入BIND_AUTO_CREATE表示在Activity和Service进行绑定后自动创建Service。这会使得MyServicede onCreate()方法得到执行,但onStartCommand()方法不会执行。

Service的生命周期

image-20220916163946370

一旦在项目中的任何位置调用了Context的startService()方法,相应的Service就会启动,并回调onStartCommand()方法。如果这个Service之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行。Service启动之后会一直保持运行状态,直到stopService()或stopself()方法被调用,或者被系统回收。注意,虽然每调用一次startService()方法,onStartCommaned()就会执行一次,但实际上每个Service只会保存一个实例。虽然不管你调用了多少次startService()方法,只需调用一次stopService()或stopself()方法,Service就会停止。

另外,还可以调用Context的bindService()来获取一个Service的持久连接,这时就会回调Service中的onBind()方法。类似地,如果这个Service之前还没有创建过,onCreate()方法会先于onBind()方法执行。之后,调用方可以获取到onBind()方法里返回地IBinder对象的实例,这样就能自由地和Service进行通信了。只要调用方和Service之间的连接没有断开,Service就会一直保持运行状态,直到被系统回收。

当调用了startService()方法后,再去调用stopService()方法。这时,Service中的onDestroy()方法就会执行,表示Service已经销毁。类似的,当调用了bindService()方法后,再去调用unbindService()方法,onDestroy()方法也会执行,这两种情况都很好理解。但是需要注意,我们是完全有可能对一个Service既调用了startService()又调用了bindService()方法,在这种情况下该如何让Service销毁呢?根据Android系统的机制,一个Service只要被启动或绑定了之后,就会处于运行状态,必须要让以上两种条件同时不满足,Service才能被销毁。所以,这种情况下要同时调用stopService()和unbindService()方法,onDestroy()方法才会执行。

前台Service

前面说过,Android8.0以后,只有当应用保持在前台可见状态下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。而你如果希望Service能够一直保持运行状态,就可以考虑使用前台Service。前台Service与后台Service最大的区别就在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。比如你的音乐、导航都用到了前台Service。

由于状态栏一直有一个正在运行的图标,相当于我们的应用以另外一种形式保持在前台可见状态,所以系统不会倾向于回收前台Service。另外,用户也可以通过下拉状态栏清楚地直到当前什么应用正在运行,因此也不存在某些应用长期在后台偷偷占用手机资源的情况。

下面来看一下如何创建前台Service。

首先,需要在AndroidManifest.xml中加入权限

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
class MyService : Service() {
private val mBinder = DownloadBinder();
class DownloadBinder : Binder(){
fun startDownloader(){
Log.d("MyService","startDownload executed")
}
fun getProgress():Int{
Log.d("MyService","getProgress executed")
return 0
}
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
override fun onCreate() {
super.onCreate()
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
val channel = NotificationChannel("my_service","前台Service通知",NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
val intent = Intent(this,MainActivity::class.java)
val pi = PendingIntent.getActivity(this,0,intent,0)
val notification = NotificationCompat.Builder(this,"my_service")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setLargeIcon(BitmapFactory.decodeResource(resources, androidx.loader.R.drawable.notification_bg))
.setContentIntent(pi)
.build()
startForeground(1,notification)
}

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

...


}

可以看到,这里只是修改了onCreate()方法中的代码,相信大家对这部分代码都比较熟悉——没错,就是创建通知(后续,小编会加一章关于通知的文章)。

  • 创建通知渠道channel
  • PendingIntent用于让通知实现可点击进入应用
  • 然后是通知的创建notification

调用startForeground()方法,两个参数:

  • 通知的id
  • 构建的Notification对象

看一下效果:

录制_2022_09_16_17_53_49_838

使用IntentService

Service中的代码都是默认运行在主线程,如果直接在Service里处理一些耗时任务,同样很容易出现ANR。

解决方法是开启一个子线程,并且还要记得线程执行完后主动stopself()或调用stopService()

是不是很麻烦?!因为程序要很可能会忘记开线程,或者忘记调用stopself()

这个时候,我们的主角IntentService就诞生了!!!一个集Service、HandlerThread、Handler一身的“天才”(感兴趣的读者可以查一下它的原理,或者如果有需要可以联系小编出一期关于Handler和HandlerThread的文章,小编可以尝试聊聊它们的原理)

使用那就很简单了,不然我们怎么会用它呢,直接上代码:


class MyIntentService : IntentService("MyIntentService") {

override fun onHandleIntent(intent: Intent?) {
//用于处理耗时任务
}

override fun onDestroy() {
super.onDestroy()
}
}

创建方式也是与Service一样,可以右键直接new一个,亦或创建个类继承IntentService,同样的使用后者这种方式别忘了注册。

这里就不介绍使用了,因为IntentService和Service的使用一摸一样,而且大家分别打印Activity和IntentService中的当前线程Thread.currentThread().name就会发现,IntentService自动为我们完成了线程切换,当然,这一切还是归功于Handler。

参考资料:《第一行代码》