Android Service

Service

Service 是 Android 中实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务。

Service 的运行不依赖任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service 依然能够保持运行。Service 并不是运行在一个独立的进程当中,而是依赖于创建 Service 时所在的应用程序。当某个应用程序进程被杀掉时,所有依赖于该进程的 Service 也会停止运行。

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

Android 8.0 开始,应用的后台功能被大幅消减。现在只有当应用保持在前台可见状态的情况下,Service 才能保证稳定运行,一旦应用进入后台之后,Service 随时可能被系统回收。之所以做这样的改动,是为了防止许多恶意的应用程序长期在后台占用手机资源,从而导致手机变得越来越卡。(可使用前台 Service 或者 WorkManager 来长期在后台执行一些任务,如果想在应用程序中使用 WorkManager,实现后台作业,可以去读一下 http://dontkillmyapp.com 上的所有内容,并且要关注一下与 WorkManager 相关的 Google 问题列表。)

分类 服务
按运行地点分 本地服务,远程服务
按运行类型分 前台服务,后台服务
按功能分 可通信服务,不可通信服务

本地服务(LocalService)

特点:运行在主进程中。主进程终止后,服务也会终止。它有两种启动方式:

通过start方式开启服务

使用service的步骤:

  1. 定义一个类继承service
  2. manifest.xml文件中配置service
  3. 使用context的startService(Intent)方法启动service
  4. 不在使用时,调用stopService(Intent)方法停止服务

特点:一旦服务开启就跟调用者(开启者)没有任何关系了。开启者退出或者挂了,服务还在后台长期的运行,开启者不能调用服务里面的方法。

通过bing方式开启服务

使用service的步骤:

  1. 定义一个类继承Service
  2. 在manifest.xml文件中注册service
  3. 使用context的bindService(Intent,ServiceConnection,int)方法启动service
  4. 不再使用时,调用unbindService(ServiceConnection)方法停止该服务

特点:bind的方式开启服务,绑定服务,调用者挂了,服务也会跟着挂掉。绑定者可以调用服务里面的方法。

生命周期

  • 通过 Context 的 startService() 启动相应的 Service,并回调 onStartCommand()。如果第一次创建,会先调用 onCreate()。Service 启动后会一直保持运行状态,直到 stopService() 或 stopSelf() 被调用,或者被系统回收。
  • 调用 Context 的 bindService() 来获取一个 Service 的持久连接,并回调 Service 中的 onBind()。如果第一次创建,会先调用 onCreate()。之后,调用方可以获取到 onBind() 里返回的 IBinder 对象的实例,这样就可以自由地和 Service 通信了。只要调用方和 Service 之间的连接没有断开,Service 就会一直保持运行状态,直到被系统回收。
  • 当调用了 startService() 后,再调用 stopService(),这时 Service 中的 onDestory() 会执行,表示 Service 已经销毁。类似地,当调用 bindService() 后,再调用 unbindService() ,onDestory() 也会执行,表示取消绑定。但如果对一个 Service 既调用了 startService() ,又调用了 bindService(),根据 Android 系统的机制,一个 Service 只要被启动或者被绑定之后,就会处于运行状态,必须要让以上两种条件同时不满足,Service 才能被销毁。所以,在这种情况下,要同时调用 stopService() 和 unbindService() ,onDestory() 才会执行。

远程服务()

特点:调用者和service不在同一个进程中,service在单独的进程中的main线程,是一种垮进程通信方式
绑定远程服务的步骤:

  1. 在服务的内部创建一个内部类,提供一个方法,可以间接调用服务的方法
  2. 把暴露的接口文件的扩展名改为.aidl文件 去掉访问修饰符
  3. 实现服务的onbind方法,继承Bander和实现aidl定义的接口,提供给外界可调用的方法
  4. 在activity 中绑定服务。bindService()
  5. 在服务成功绑定的时候会回调 onServiceConnected方法 传递一个 IBinder对象
  6. aidl定义的接口.Stub.asInterface(binder) 调用接口里面的方法

IntentService

IntentService是Service子类,特征:

  • 会创建独立的worker线程来处理所有的Intent请求;
  • 会创建独立的worker线程来处理onHandleIntent()方法实现的代码,无需处理多线程问题;
  • 所有请求处理完成后,IntentService会自动停止,无需调用stopSelf()方法停止Service;
  • 为Service的onBind()提供默认实现,返回null;
  • 为Service的onStartCommand提供默认实现,将请求Intent添加到队列中;

基本用法

示例(Kotlin):创建 Service

New -> Service -> Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 手动创建的要记得在文件内注册
* 任何一个 Service 在整个应用程序范围内都是通用的。
*/
class MyService : Service() {

/**
* Service 中唯一的抽象方法,所以必须在子类里实现。
*/
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}

/**
* Service 第一次创建时调用
*/
override fun onCreate() {
super.onCreate()
Log.d("TAG","onCreate")
}

/**
* 每次 Service 启动时调用,立刻执行某个动作。
*
* 日志打印:
* D/TAG: onCreate
* D/TAG: onStartCommand
* D/TAG: onStartCommand
* D/TAG: onStartCommand
* 虽然 onStartCommand 被调用多次,但实际上每个 Service 只会存在一个实例。
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("TAG","onStartCommand")
// Service 中的代码都是默认运行在主线程中,不适合执行耗时逻辑,需要使用多线程编程技术。
thread {
// 处理具体逻辑

// 如果需要在执行完毕后自动停止的功能
stopSelf()
}
return super.onStartCommand(intent, flags, startId)
}

/**
* Service 销毁时调用,回收不再使用的资源。
*/
override fun onDestroy() {
super.onDestroy()
Log.d("TAG","onDestroy")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
private fun initView() {
btnStartService.setOnClickListener{
val intent = Intent(this,MyService::class.java)
// startService 和 stopService 都是定义在 Context 类中的,所以可在 Activity 里直接调用。
startService(intent) // 启动 Service
}
btnStopService.setOnClickListener{
val intent = Intent(this,MyService::class.java)
// Service 也可以自我停止运行,只需要在 Service 内部调用 stopSelf()。
stopService(intent) // 启动 Service
}
}
AndroidManifest.xml
1
2
3
4
5
<!--  enabled:是否启用此 Service, exported:是否将此 Service 暴露给外部其它程序访问。     -->
<service
android:name=".service.MyService"
android:enabled="true"
android:exported="true"></service>

示例(Kotlin):Activity 与 Service 通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 借助 onBind() 实现通信
*/
class MyService : Service() {

...

private val mBinder = DownloadBinder()

/**
* 提供下载功能,创建一个专门的 Binder 对象来对下载功能进行管理。
*/
class DownloadBinder: Binder(){

fun startDownload(){
// 下载
Log.d("TAG","startDownload executed")
}

fun getProgress():Int{
// 查看进度
Log.d("TAG","getProgress executed")
return 0
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ServiceActivity:BaseActivity() {

lateinit var downloadBinder: MyService.DownloadBinder

/**
* 创建 ServiceConnection 的匿名类实现
*/
private val connection = object :ServiceConnection{
/**
* 在 Activity 和 Service 成功绑定时调用
*/
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// 通过向下转型得到了 DownloadBinder 实例
downloadBinder = service as MyService.DownloadBinder
downloadBinder.startDownload()
downloadBinder.getProgress()
}

/**
* 只有在 Service 的创建进程崩溃或者被杀掉时才会调用,这个方法不太常用。
*/
override fun onServiceDisconnected(name: ComponentName?) {

}
}

...

private fun initView() {
...

btnBindService.setOnClickListener{
val intent = Intent(this,MyService::class.java)
// 绑定 Service,
// 第三个参数是一个标志位,这里表示在 Activity 和 Service 进行绑定后自动创建 Service。
// 这会使得 MyService 中的 onCreate() 得到执行,但 onStartCommand() 不会执行。
bindService(intent,connection,Context.BIND_AUTO_CREATE)

// 打印结果:
// D/TAG: onCreate
// D/TAG: startDownload executed
// D/TAG: getProgress executed
}
btnUnBindService.setOnClickListener{
val intent = Intent(this,MyService::class.java)
// 解绑 Service
unbindService(connection)

// 打印结果:
// D/TAG: onDestroy
}
}
}

使用前台 Service

前台 Service 和 普通 Service 最大的区别在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。

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

示例(Kotlin):

从 Android 9.0 开始,前台 Service 必须进行权限声明:

1
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyService : Service() {

...

override fun onCreate() {
super.onCreate()
Log.d("TAG","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,ServiceActivity::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.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
.setContentIntent(pi)
.build()
// 构建 Notification 对象后并没使用 NotificationManager 将通知显示出来,
// 而是调用了 startForeground(),让 MyService 变成了一个前台 Service,并在系统状态栏显示出来。
// 第一个参数是通知的 id,类似于 notify() 的第一个参数,
// 第二个参数是构建的 Notification 对象,
startForeground(1,notification)
}

...
}
1
2
3
4
btnStartService.setOnClickListener{
val intent = Intent(this,MyService::class.java)
startService(intent) // 启动 Service
}

使用 IntentService

一个异步的、会自动停止的 Service。

示例(Kotlin):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 首先必须调用父类的构造函数并传入字符串,字符串可随意指定,只在调试时有用。
*
*/
class MyIntentService : IntentService("MyIntentService") {

/**
* 实现这个抽象方法,位于子线程中运行,可以处理一些耗时逻辑。
*/
override fun onHandleIntent(intent: Intent?) {
// 打印当前线程的 id
Log.d("TAG","Thread id is ${Thread.currentThread().name}")
}

/**
* Service 会自动停止,并打印日志。
*/
override fun onDestroy() {
super.onDestroy()
Log.d("TAG","onDestroy executed")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
private fun initView() {
btnStartIntent.setOnClickListener{
// 打印主线程的 id
Log.d("TAG","Thread id is ${Thread.currentThread().name}")
val intent = Intent(this,MyIntentService::class.java)
startService(intent)

// 打印结果:
// D/TAG: Thread id is main
// D/TAG: Thread id is IntentService[MyIntentService]
// D/TAG: onDestroy executed
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- enabled:是否启用此 Service, exported:是否将此 Service 暴露给外部其它程序访问。 -->
<service
android:name=".service.MyIntentService"
android:enabled="true"
android:exported="true"></service>
</application>

备注

参考资料:

第一行代码(第3版)

Android 开发艺术探索