Service

service

本篇博文主要介绍Service相关知识,具体目录如下

0x00 什么是Service

  • Service是一个应用程序组件,可以在后台长时间运行的操作,不提供用户界面;
  • 一个应用程序可以启动一个服务,它将继续在后台运行,即使用户切换到另外一个应用
  • 一个组件可以绑定到一个服务与它交互,甚至执行进程间通信(IPC),如处理网络传输、音乐播放、执行文件I/O,与content provider进行交互等。

0x01 服务的分类

  • 按照运行地点分类

    | 类别 | 区别 | 优点 | 缺点 | 应用 |
    |:————————:|:——————–:|:—————————————————————————————————————————————————————————————————————–:|———————————————————————–|————————————————–|
    | 本地服务(Local Service) | 该服务依附在主进程上 | 服务依附在主进程上而不是独立的进程,这样在一定程度上节约了资源,另外Local服务因为是在同一进程因此不需要IPC,也不需要AIDL。相应bindService会方便很多。 | 主进程被Kill后,服务便会终止。 | 如:音乐播放器播放等不需要常驻的服务。 |
    | 远程服务(Remote Service) | 该服务是独立的进程 | 服务为独立的进程,对应进程名格式为所在包名加上你指定的android:process字符串。由于是独立的进程,因此在Activity所在进程被Kill的时候,该服务依然在运行,不受其他进程影响,有利于为多个进程提供服务具有较高的灵活性。 | 该服务是独立的进程,会占用一定资源,并且使用AIDL进行IPC稍微麻烦一点。 | 一些提供系统服务的Service,这种Service是常驻的。 |

  • 按运行类型分类
类别 区别 应用
前台服务 会在通知栏显示onGoing的 Notification 当服务被终止的时候,通知一栏的 Notification 也会消失,这样对于用户有一定的通知作用。常见的如音乐播放服务。
后台服务 默认的服务即为后台服务,即不会在通知一栏显示 onGoing的 Notification。 当服务被终止的时候,用户是看不到效果的。某些不需要运行或终止提示的服务,如天气更新,日期同步,邮件同步等。
  • 按使用方式分类
类别 区别
startService启动的服务 主要用于启动一个服务执行后台任务,不进行通信。停止服务使用stopService。
bindService启动的服务 方法启动的服务要进行通信。停止服务使用unbindService。
同时使用startService、bindService 启动的服务 停止服务应同时使用stopService与unbindService。

0x02 生命周期

如使用方式分类所提,service使用常分为两大类,start、bind

  • 如果一个应用程序组件(比如一个activity)通过调用startService()来启动服务,则该服务就是被“started”了。一旦被启动,服务就能在后台一直运行下去,即使启动它的组件已经被销毁了。
    通常,started的服务执行单一的操作并且不会向调用者返回结果。比如,它可以通过网络下载或上传文件。当操作完成后,服务应该自行终止。

  • 如果一个应用程序组件通过调用bindService()绑定到服务上,则该服务就是被“bound”了。bound服务提供了一个客户端/服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至可以利用进程间通信(IPC)跨进程执行这些操作。绑定服务的生存期和被绑定的应用程序组件一致。
    多个组件可以同时与一个服务绑定,不过所有的组件解除绑定后,服务也就会被销毁。

二者的生命周期如下:

对应的方法解释如下:

  • 4个手动调用的方法
手动调用方法 作用
startService() 启动服务
stopService() 关闭服务
stopSelf() 关闭服务
bindService() 绑定服务
unbindService() 解绑服务
  • 5个内部调用的方法
内部调用的方法 作用
onCreat() 创建服务
onStartCommand() 开始服务
onDestroy() 销毁服务
onBind() 绑定服务
onUnbind() 解绑服务

其中需要注意以下几点:

  • startService()和stopService()只能开启和关闭Service,无法操作Service;bindService()和unbindService()可以操作Service
  • startService开启的Service,调用者退出后Service仍然存在;BindService开启的Service,调用者退出后,Service随着调用者销毁。

0x03 如何使用

建议参照demo学习https://github.com/xsfelvis/ServiceAIDLStudyDemo.git

本地Service(startService)

通过start启动的service一旦被启动,服务一般会在后台一直运行即使启动它的的组件已经销毁了,而且不会像调用者返回结果,如可以通过它进行网络下载或者上传文件,当操作完成后,该服务自行终止。

Step 1 在AndroidManifest中注册Service

其中一些相关属性需要重点说明下:

属性 说明 备注
android:name Service的类名
android:label Service的名字 若不设置默认为Service类名
android:icon Service的图标
android:permission 申明此Service的权限 有提供了该权限的应用才能控制或连接此服务
android:process 表示该服务是否在另一个进程中运行(远程服务) 不设置默认为本地服务;remote则设置成远程服务
android:enabled 系统默认启动 true:Service 将会默认被系统启动;不设置则默认为false
android:exported 该服务是否能够被其他应用程序所控制或连接 不设置默认此项为 false

step 2 新建子类继承自Service类

需要重写onCreate()、onStartCommand()、onDestroy()和onBind()方法

step 3 构建用于启动Service的intent对象

step 4 调用startService启动,调用stopService/stopSelf停止服务

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

@Override
public void onCreate() {
//这里配置一些信息
//启动运行服务的线程。
//请记住我们要创建一个单独的线程,因为服务通常运行于进程的主线程中,可我们不想阻塞主线程。
//我们还要赋予它后台运行的优先级,以便计算密集的工作不会干扰我们的UI。
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
//获取handlerThread的loop队列并用于Handler
mServiceLooper = handlerThread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
Log.d(TAG, "onCreate");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
msgStr = intent.getStringExtra("startService");
Log.d(TAG, "onStartCommand getExtraString = " + msgStr);
//对于每一个启动请求,都发送一个消息来启动一个处理
//同时传入启动ID,以便任务完成后我们知道该终止哪一个请求。
Message message = mServiceHandler.obtainMessage();
message.arg1 = 1;
mServiceHandler.sendMessage(message);
//如果我们被杀死了,那从这里返回之后被重启
return START_STICKY;
}

由于Service也是运行在主线程中,如果需要执行一些耗时操作需要放到相应的子线程中处理,谷歌内置了一个IntentService(异步处理服务)

它会新开一个线程:handlerThread在线程中发消息,然后接受处理完成后,会清理线程,并且关掉服务。

IntentService有以下特点:

(1) 它创建了一个独立的工作线程来处理所有的通过onStartCommand()传递给服务的intents。

(2) 创建了一个工作队列,来逐个发送intent给onHandleIntent()。

(3) 不需要主动调用stopSelft()来结束服务。因为,在所有的intent被处理完后,系统会自动关闭服务。

(4) 默认实现的onBind()返回null

(5) 默认实现的onStartCommand()的目的是将intent插入到工作队列中

其中intentService在5.0系统中需要显示启动

在之前的例子中我们自己手动维护了一个handleThread去处理耗时操作,intentService已经自带了,然后用户只要是实现onHandleIntent去处理新的业务即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	
/**
* IntentService从缺省的工作线程中调用本方法,并用启动服务的intent作为参数。
* 本方法返回后,IntentService将适时终止这个服务。
*/

@Override
protected void onHandleIntent(@Nullable Intent intent) {
//根据Intent的不同进行不同的事务处理
String taskName = intent.getExtras().getString("taskName");
switch (taskName) {
case "task1":
Log.d(TAG, "do task1");
break;
case "task2":
Log.d(TAG, "do task2");
break;
default:
break;
}

}

以上两种均在demo中有所实现

可通信Service(bind)

如果一个应用程序组件通过调用bindService()绑定到服务上,bound服务提供了一个客户端/服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至可以利用进程间通信(IPC)跨进程执行这些操作。绑定服务的生存期和被绑定的应用程序组件一致。多个组件可以同时与一个服务绑定,不过所有的组件解除绑定后,服务也就会被销毁。

  • 使用场景
    当客户端和服务位于同一个应用程序的进程中,(如一个音乐应用需要把Activity绑定到它自己的后台音乐播放上)

  • 使用步骤

  1. 在你的服务中创建一个Binder的实例,通常需要实现以下3点之一

    • 包含了可供客户端调用的公共方法,如demo中的 getHelloBoundService()方法
    • 返回当前Service实例,其中包含了可供客户端调用的公共方法,如demo中的getRandomNumber
    • 或者,返回内含服务类的其它类的一个实例,服务中包含了可供客户端调用的公共方法,
  2. 从回调方法onBinder()返回Binder的实例

  3. 在客户端中,在回调方法onServiceConnected()中接收Binder并用所提供的方法对绑定的服务进行调用,不过服务和客户端之所以必须位于同一个应用程序中,是为了让客户端能够正确转换(cast)返回的对象并调用对象的API。
    服务和客户端也必须位于同一个进程中,因为这种方式不能执行任何跨进程的序列化(marshalling)操作。
  • Tips
  1. 在没有bind时执行unbind,会报Service not registered crash,可以增加标志位控制bind、unbind可以参看demo中的unbind操作
  2. ServiceConnection中重写2个方法onServiceConnectedonServiceDisconnected,其中bindService会触发onServiceConnected,而unbinderService不会触发onServiceDisconnected;onServiceDisconnected在系统在内存不足的时候可以优先杀死这个服务

前台service

前台service和后台service最大的区别在于

  • 前台service在下来通知栏有显示通知,但是后台service没有
  • 前台service优先级较高,不会由于系统内存不足而被回收,而后台service优先级比较低,当系统出现内存不足情况时有可能被回收

与普通service使用类似,核心是增加构建通知部分的处理,具体可以查看demo中代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int onStartCommand(Intent intent, int flags, int startId) {
//API11之后构建Notification的方式
Notification.Builder builder = new Notification.Builder(this);
Intent frontServiceIntent = new Intent(this, MainActivity.class);
PendingIntent frontServicePeningIntent = PendingIntent.getActivity(this, 0, frontServiceIntent, 0);
builder.setContentIntent(frontServicePeningIntent)
.setContentTitle("下拉列表中的Title")
.setContentText("要显示的内容")
.setSmallIcon(R.mipmap.ic_front_small)
.setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_front_big))
.setWhen(System.currentTimeMillis());
Notification notification = builder.getNotification();
notification.defaults = Notification.DEFAULT_SOUND;
// 参数一:唯一的通知标识;参数二:通知消息。
startForeground(110, notification);// 开始前台服务
return START_STICKY;
}

远程service

主要是为了让Service与多个应用程序的组件进行跨进程通信(IPC),这里涉及到两个概念

  • IPC:Inter-Process Communication,即跨进程通信
  • AIDL:Android Interface Definition Language,即Android接口定义语言;用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。

AIDL(Android 接口定义语言)与您可能使用过的其他 IDL 类似。 您可以利用它定义客户端与服务使用进程间通信 (IPC) 进行相互通信时都认可的编程接口。 在 Android 上,一个进程通常无法访问另一个进程的内存。 尽管如此,进程需要将其对象分解成操作系统能够识别的原语,并将对象编组成跨越边界的对象。 编写执行这一编组操作的代码是一项繁琐的工作,因此 Android 会使用 AIDL 来处理。

并且谷歌特意注明了AIDL使用的场景

注:只有允许不同应用的客户端用 IPC 方式访问服务,并且想要在服务中处理多线程时,才有必要使用 AIDL。 如果您不需要执行跨越不同应用的并发 IPC,就应该通过实现一个 Binder 创建接口;或者,如果您想执行 IPC,但根本不需要处理多线程,则使用 Messenger 类来实现接口。无论如何,在实现 AIDL 之前,请您务必理解绑定服务。

首先新建一个aidl文件

1
2
3
4
interface IAidlService {

void aidlService();
}

然后make一下,在build/generated/source/aidl文件下生成一个接口文件

在使用到通信的地方使用这个接口文件中的api,AIDLService1.Stub.asInterface()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mAidlServiceConnection = new ServiceConnection() {

//重写onServiceConnected()方法和onServiceDisconnected()方法
//在Activity与Service建立关联和解除关联的时候调用
@Override
public void onServiceDisconnected(ComponentName name) {
}

//在Activity与Service建立关联时调用
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//使用AIDLService1.Stub.asInterface()方法将传入的IBinder对象传换成了mServerAidlService对象
mServerAidlService = IAidlService.Stub.asInterface(service);

try {
//通过该对象调用在MyAIDLService.aidl文件中定义的接口方法,从而实现跨进程通信
mServerAidlService.aidlService();

} catch (RemoteException e) {
e.printStackTrace();
}
}
};

可以看出二者不在一个线程中


具体代码在demo中~

0x04 一些容易混淆的点

Service和Thread的区别

官方有两点描述

  • 1.A Service is not a separate process. The Service object itself does
    not imply it is running in its own process; unless otherwise specified,
    it runs in the same process as the application it is part of.
  • 2.A Service is not a thread. It is not a means itself to do work off
    of the main thread (to avoid Application Not Responding errors).

第二点清楚提到不是一个thread,只是有些时候二者均工作在后台而已。

service和调用者之间的通讯都是同步的(不论是远程service还是本地service),它跟线程一点关系都没有!

service和intentService之间区别

  • 1.Service:依赖于应用程序的主线程不要误以为是独立的进程 or 线程,因此不能处理耗时操作,否则就会报ANR(Activity—–>5秒
    Broadcast—–>10秒,Service—–>20秒),而intentService内部启动一个HandleThread工作线程来去处理耗时任务
    1. Service需要主动调用stopSelf()来结束服务,而IntentService不需要(在所有intent被处理完后,系统会自动关闭服务,内部调用了stopself()方法)

IntentService与线程的区别

  • intentService内部采用了HandlerThread实现,作用类似于后台线程;与后台线程相比,IntentService是一种后台服务,优势是:优先级高(不容易被系统杀死),从而保证任务的执行
  • 在应用中,如果是长时间的在后台运行,而且不需要交互的情况下,使用服务。
  • 同样是在后台运行,不需要交互的情况下,如果只是完成某个任务,之后就不需要运行,而且可能是多个任务,需要长时间运行的情况下使用线程。
  • 如果任务占用CPU时间多,资源大的情况下,要使用线程。

0x05 小结

service是Android 四大组件之一,掌握好它对平时的开发有着莫大益处,本文只是介绍了service的常见用法和一些容易混淆的点,还有一些深入的点比如aidl数据传递、自定义notification定制化和在不同android版本的坑,这些都需要在实际开发中遇到再去针对性处理了~

参考文档

0%