Android基础复习:Service组件详解
概况
Service组件是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。
挑重点来说,Service组件的特点包括:
- 在后台运行:Service组件可以在后台长时间运行,即使用户关闭了与应用程序的所有交互界面。
- 没有UI:Service组件没有用户界面,它们通常用于执行不需要用户交互的任务。
- 可以与Activity组件交互:Service组件可以与Activity组件交互,例如通过Broadcast、AIDL等机制实现Activity与Service之间的通信。
- 生命周期:Service组件有自己的生命周期,包括onCreate()、onStartCommand()、onBind()和onDestroy()方法,这些方法的调用顺序与Activity组件的生命周期不同。
疑问
相信已经接触到多线程技术的读者一开始肯定会有和我相同的疑问:已经有了多线程技术可以让我们在后台执行任务,那为什么还需要Service组件呢?
这和Android系统对多线程技术的限制有关。在Android中,如果我们需要在后台长时间运行的任务,比如下载文件、播放音乐等等,使用多线程技术可能会受到一些限制,比如系统可能会因为内存不足而杀死线程,或者在某些情况下,应用程序可能会被系统挂起,导致线程无法正常运行。
而Service组件的出现就是为了解决这个问题。Service组件是在后台运行的组件,它可以独立于应用程序的界面进行运行。与多线程技术相比,Service组件的生命周期更加稳定,它拥有自己独立的生命周期,可以在后台长时间运行,即使应用程序被系统挂起,Service组件也可以继续运行。
而且Service组件还可以实现多线程技术无法实现的IPC通信,可以扮演一个本地服务端的角色。
选择服务还是线程
正如我们之前提到的,线程和服务有一定的相似性,那我们在不同场景下该如何选择呢?官方提到了:
“服务是一种即使用户未与应用交互也可在后台运行的组件,因此,只有在需要服务时才应创建服务。”
所以说,如果必须在主线程之外执行操作,但只在用户与您的应用交互时执行此操作,则应创建新线程。毕竟Service组件可以在没有和应用程序交互时继续保持运行。
Service组件的基础使用
由于Service组件也属于四大组件之一,因此其具体创建实际上和Activity相似,创建一个类拓展Service的基类然后在manifest清单文件中声明。Android Studio可以很好地帮助我们解决这个问题。
Service组件的生命周期
前面我们提到,Service有其独立的生命周期。接下来就来看Service的生命周期:
上面这幅图是从官网上截下来的Service的生命周期图,可以看到我们可以通过两种方法来启动Service的生命周期:startService方法和bindService方法。具体的两种启动方式的细节,我们将在后面详细介绍,这里我们只要知道如果是通过startService启动的,那么Service就会先调用onCreate方法,然后执行onStartCommand回调方法;如果是通过bindService启动的,那么就会先执行onCreate方法接着执行onBind方法,当帮顶接触后则会执行onUnbind方法。
创建我们的Service类
上文提到,我们需要拓展Service的基类来创建Service,实际上Service存在两种基类,分别是IntentService类和Service类,这里我们从简单的先入手,先拓展IntentService类。
拓展IntentService
这是 Service 的子类,其使用工作线程逐一处理所有启动请求。如果您不要求服务同时处理多个请求,此类为最佳选择。实现 onHandleIntent(),该方法会接收每个启动请求的 Intent,以便您执行后台工作。
上文是官方的简介,从中我们可以知道IntentService的使用场景和特点之一就是串行处理启动请求,无需考虑并发情况,这也是比较简单的情况。接下来我们开始拓展IntentService类:
public class MyIntentService extends IntentService {
private static final String TAG = "MyIntentService";
private static final String EVENT_A = "com.example.myService.event_a";
private static final String EVENT_B = "com.example.myService.event_b";
private static final String PARAM = "myService.Param";
//一般我们不需要重写,如果重写了返回值一般情况下也一定是super.onStartCommand
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
Log.d(TAG, "运行在线程: "+Thread.currentThread().getName());
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind: "+"Success");
return super.onBind(intent);
}
//留给外部调用的接口
public static void startEventA(Context context,String param){
Intent intent = new Intent(context,MyIntentService.class);
intent.setAction(EVENT_A);
intent.putExtra(PARAM,param);
context.startService(intent);
}
public static void startEventB(Context context,String param){
Intent intent = new Intent(context,MyIntentService.class);
intent.setAction(EVENT_B);
intent.putExtra(PARAM,param);
context.startService(intent);
}
public MyIntentService() {
super("MyIntentService");
}
@Override //在onHandleIntent处理事件时会自动创建子线程执行耗时任务
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
String param = intent.getStringExtra(PARAM);
switch (action){
case EVENT_A:
onHandleEventA(param);
Log.d(TAG, "onHandleIntentA运行在: "+Thread.currentThread().getName());
break;
case EVENT_B:
onHandleEventB(param);
Log.d(TAG, "onHandleIntentB运行在: "+Thread.currentThread().getName());
break;
default:
break;
}
return ;
}
private void onHandleEventA(String para){
Log.d(TAG, "onHandleEventA: "+para + "运行在:"+Thread.currentThread().getName());
}
private void onHandleEventB(String para){
Toast.makeText(this, para+" 运行在:"+Thread.currentThread().getName(), Toast.LENGTH_SHORT).show();
}
}
这里我们只是通过startService方法来启动的,因为这种方式比较简单,bindService的方法我们会在后面介绍。
在这个IntentService中,我们必须要实现的方法只有它的构造方法和onHandleIntent方法,构造方法中必须调用它超类的构造方法并传入一个字符串用于指定IntentService的工作线程的名称。onIntentService方法则是用来处理各种请求的回调方法。
在上面的示例中我主要是通过Intent的Action值来区分需要处理的事件,并留出了两个静态的公共方法用于给外部进行调用,这里还打印出了执行方法所在的回调:
通过打印出来的信息可以发现其他方法仍将运行在主线程中,但是onHandleIntent方法将会单独运行在一个子线程中,IntentService会自动创建一个工作线程来处理耗时的任务,具体来说,IntentService类会执行以下操作:
- 创建默认的工作线程,用于在应用的主线程外执行传递给 onStartCommand() 的所有 Intent。
- 创建工作队列,用于将 Intent 逐一传递给 onHandleIntent() 实现,这样您就永远不必担心多线程问题。
- 在处理完所有启动请求后停止服务,因此您永远不必调用 stopSelf()。
- 提供 onBind() 的默认实现(返回 null)。
- 提供 onStartCommand() 的默认实现,可将 Intent 依次发送到工作队列和 onHandleIntent() 实现。
除了构造方法还有onHandleIntent方法之外,我们也可以实现别的回调方法,比如说onCreate,onStartCommand,onDestroy等,但是一定要确保调用超类实现,这可以防止出现一些不必要的错误,就拿onStartCommand方法来说,我们最后就一定要返回它的超类实现,即如何将Intent传递给onHandleIntent:
//一般我们不需要重写,如果重写了返回值一般情况下也一定是super.onStartCommand
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
Log.d(TAG, "运行在线程: "+Thread.currentThread().getName());
return super.onStartCommand(intent, flags, startId);
}
另外这里还有一个小小的补充:onHandleEventB方法中使用了Toast,而该方法是在IntentService的子线程中执行的,但是Toast本身只能在主线程中显示,因此这段代码在运行时可能会崩溃。但实际上在Android 9.0(API 28)之后,Google增加了对Toast的线程检查,如果在非主线程中使用Toast,系统会自动将其转移到主线程中进行显示,从而避免了崩溃问题。
拓展Service类
拓展IntentService类的话,我们要做的工作就会相对少一点,但是IntentService只是支持串行执行任务,如果我们有一些多线程实现的要求的话,就要拓展Service类。接下来我们来拓展Service类:
public class MyService extends Service {
private static final String TAG = "MyService";
private Looper serviceLooper;
private ServiceHandler serviceHandler;
private static final int SIT_A = 1;
private static final int SIT_B = 2;
private static final String SIT_A_STRING = "com.situationA";
private static final String SIT_B_STRING = "com.situationB";
private final class ServiceHandler extends Handler{
public ServiceHandler(Looper looper){
super(looper);
}
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case SIT_A:
Toast.makeText(MyService.this, "Situation A", Toast.LENGTH_SHORT).show();
break;
case SIT_B:
Toast.makeText(MyService.this, "Situation B", Toast.LENGTH_SHORT).show();
break;
default:
break;
}
stopSelf(msg.arg1);//根据启动的Id值来决定是否结束Service
}
}
@Override
public void onCreate() {
//设置工作线程的优先级
HandlerThread handlerThread = new HandlerThread("NormalServiceThread", Process.THREAD_PRIORITY_BACKGROUND);
handlerThread.start();//启动工作线程
//绑定Looper和Handler
serviceLooper = handlerThread.getLooper();
serviceHandler = new ServiceHandler(serviceLooper);
}
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "服务已启动", Toast.LENGTH_SHORT).show();
Message message = serviceHandler.obtainMessage();
message.arg1 = startId;//设置arg1为启动Id
switch (intent.getAction()){
case SIT_A_STRING:
message.what = SIT_A;
break;
case SIT_B_STRING:
message.what = SIT_B;
break;
default:
break;
}
serviceHandler.sendMessage(message);//发送Message
return START_STICKY;
}
public static void StartSITA(Context context){
Intent intent = new Intent(context,MyService.class);
intent.setAction(SIT_A_STRING);
context.startService(intent);
}
public static void StartSITB(Context context){
Intent intent = new Intent(context,MyService.class);
intent.setAction(SIT_B_STRING);
context.startService(intent);
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy: ");
}
}
这个示例是根据官方的示例改的,由于Service和IntentService类不同,Service类并不是自带工作线程的,它的任务默认是执行在主线程中, 所以我们需要自己创建工作线程,这里我们用HandleThread来用作工作线程,这个类与普通Thread的区别就是其内置了MessageQueue和Looper,使用起来会比较方便一点。具体的事件处理是交由Handler在工作线程中执行的。这里虽然我们需要自建工作线程,看起来比较麻烦,但是它也赋予了我们一定的自由性,我们可以在Service中创建多个线程来达到并行处理的效果。
其次我们重写了onStartCommand方法,这里需要特别说明的就是onStartCommand方法的返回值,该方法必须要返回一个整型数,整型数是一个值,用于描述系统应如何在系统终止服务的情况下继续运行服务。IntentService 的默认实现会为您处理此情况,但我们可以对其进行修改。从 onStartCommand() 返回的值必须是以下常量之一:
- START_NOT_STICKY:如果系统杀死了服务,那么除非有新的 Intent 请求启动此服务,否则系统不会重启此服务。
- START_STICKY:如果系统杀死了服务,那么系统会尝试重新启动此服务,但不会重新传递最后一个 Intent, 直到调用了 stopSelf() 或 stopService() 方法停止服务。
- START_REDELIVER_INTENT:如果系统杀死了服务,那么系统会重新启动服务,并将之前的 Intent 对象重新传递给服务的 onStartCommand() 方法,让服务继续处理之前的请求。
开启服务
上面已经提到过开启服务的生命周期主要是两种方式:启动服务和绑定服务,接下来我们就来介绍这两种方法。
启动服务
启动服务主要使用显示Intent启动Service(也不允许隐式启动Service就是了),具体的启动方法和启动Activity是一样的。我们可以将Intent传递给startService或者startForegroundService(启动前台服务,我们后面再讲),从Activity或其他应用组件启动服务。
Android系统会调用服务的onStartCommand方法,并向其传递Intent,从而指定要启动的服务。
其实我们在上面的示例中也演示到了:
Intent intent = new Intent(context,MyIntentService.class);
intent.setAction(EVENT_B);
intent.putExtra(PARAM,param);
context.startService(intent);
startService() 方法会立即返回,并且 Android 系统会调用服务的 onStartCommand() 方法。如果服务尚未运行,则系统首先会调用 onCreate(),然后调用 onStartCommand()。多个服务启动请求会导致多次对服务的 onStartCommand() 进行相应的调用。但是,如要停止服务,只需一个服务停止请求(使用 stopSelf() 或 stopService())即可。
绑定服务
绑定服务是客户端-服务器接口中的服务器。借助绑定服务,组件(例如 Activity)可以绑定到服务、发送请求、接收响应,以及执行进程间通信 (IPC)。绑定服务通常只在为其他应用组件提供服务时处于活动状态,不会无限期在后台运行。
绑定服务中用到的方法就是bindService方法,bindService方法需要接受三个参数,分别是指定了服务的Intent。还有实现了ServiceConnection接口的一个对象,该对象是用来管理Service的连接情况的,ServiceConnection有两个必须要实现的方法:分别是onServiceConnected方法和onServiceDisconnection方法,前者是当连接成功建立起来时会执行的回调方法,后者是当连接丢失时要执行的回调方法。
我们先通过IntentService来实现我们的绑定服务类:
public class MyBindService extends IntentService {
private static final String TAG = "MyBindService";
public MyBindService() {
super("MyBindService");
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "绑定成功---Service");
return new MyBinder();//返回业务接口给客户端使用
}
@Override
protected void onHandleIntent(Intent intent) {
}
public static class MyBinder extends Binder{
//一些服务端的业务接口
public void Show(Context context){
Toast.makeText(context, "测试接口Show", Toast.LENGTH_SHORT).show();
}
}
public static class MyConnection implements ServiceConnection{
public MyBinder mBinder = null;
public Context mContext = null;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected: "+"已经成功连接服务");
mBinder = (MyBinder) service;
mBinder.Show(mContext);
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected: "+"服务连接已丢失");
}
}
}
当其他组件调用了bindService方法就会执行onBind回调方法,在这个类中我还实现了两个内部类,一个就是之前说的ServiceConnection的实现类,另一个就是Binder的子类。这个Binder类就是用于进行Service和其绑定组件的通信的,我们可以实现自己的Binder类,并在这个类中将一些业务接口给暴露出来给与其绑定的组件来使用,那具体是在哪里将这个Binder类传递给与其绑定的组件呢?一般是在ServiceConnection实现类的onServiceConnected回调方法中处理的。我们可以看上文的onServiceConnected方法中有一个IBinder参数,这个参数来自于哪里呢?实际上是来自于Service的onBind方法中返回的Binder类。
接下来给出在被绑定组件里的调用代码:
binding.btBind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, MyBindService.class);
MyBindService.MyConnection connection = new MyBindService.MyConnection();
connection.mContext = MainActivity.this;
bindService(intent,connection,Context.BIND_AUTO_CREATE);
}
});
bindService的第三个方法是一个标志位,用于指定绑定服务的行为,具体有以下几种取值:
- Context.BIND_AUTO_CREATE: 如果服务不存在,则系统会自动创建一个服务并绑定,当所有绑定的客户端都解除绑定后,系统会销毁服务。
- Context.BIND_DEBUG_UNBIND: 表示如果客户端和服务端连接出现异常情况,例如客户端崩溃或服务端崩溃等,会自动调用onUnbind()方法来解绑服务,并会调用ServiceConnection的onBindingDied()方法通知客户端连接出现了异常。
- Context.BIND_NOT_FOREGROUND: 表示服务不应该是一个前台服务。这是一个暗示,通知系统这个服务不应该在前台运行,因此系统会在内存不足时更容易终止它。
- Context.BIND_ABOVE_CLIENT: 表示绑定时服务应该处于客户端之上,即服务的优先级应该高于客户端。
- Context.BIND_ALLOW_OOM_MANAGEMENT: 表示服务可以被系统进行OOM内存管理,当系统内存不足时,系统会根据LRU算法来回收服务进程的内存。
一般情况下,我们使用Context.BIND_AUTO_CREATE来指定绑定服务的行为,表示在客户端与服务端进行绑定时,如果服务不存在则会自动创建服务。
另外,关于绑定服务,还有AIDL和Messenger的使用,但是由于这两个内容涉及到IPC的内容,我会在其他文章里单独总结。
使用前台服务
实际上服务可以分为两种类型,第一种就是我们之前提到的普通服务,还有一种就是前台服务。前台服务是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。前台服务必须为状态栏提供通知,将其放在运行中的标题下方。这意味着除非将服务停止或从前台移除,否则不能清除该通知。
具体来说什么是前台服务呢?拿QQ音乐来说,当我们返回桌面时QQ音乐仍可以实现在后台播放,并且在通知栏里会有一项专门显示QQ音乐,我们还可以通过这个通知栏来切换歌曲。
限制前台服务
由于前台服务收到系统的限制较小,所以我们在使用时需要自己对其加以限制,避免消耗过大的内存造成卡顿:
只有当应用执行的任务需供用户查看(即使该任务未直接与应用交互)时,您才应使用前台服务。因此,前台服务必须显示优先级为 PRIORITY_LOW 或更高的状态栏通知,这有助于确保用户知道应用正在执行的任务。如果某操作不是特别重要,因而您希望使用最低优先级通知,则可能不适合使用服务;相反,您可以考虑使用计划作业。
每个运行服务的应用都会给系统带来额外负担,从而消耗系统资源。如果应用尝试使用低优先级通知隐藏其服务,则可能会降低用户正在主动交互的应用的性能。因此,如果某个应用尝试运行拥有最低优先级通知的服务,则系统会在抽屉式通知栏的底部调用出该应用的行为。
创建前台服务
在创建前台服务之前我们可能需要申请一下相关的权限:如果应用面向 Android 9(API 级别 28)或更高版本并使用前台服务,则其必须请求 FOREGROUND_SERVICE 权限。这是一种普通权限,因此,系统会自动为请求权限的应用授予此权限。
如果面向 API 级别 28 或更高版本的应用试图创建前台服务但未请求 FOREGROUND_SERVICE,则系统会抛出 Security-Exception。
所以我们需要在Manifest清单文件中添加一下权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
第一行代码是申请前台服务的权限,第二行代码是申请发送通知的权限。接着给出完整的代码:
public class NotificationService extends Service {
private static final String TAG = "NotificationService";
private static final int NOTIFICATION_ID = 1;
private static final String CHANNEL_ID = "MyNotificationService";
NotificationManager manager;
NotificationChannel channel;
@Override
public void onCreate() {
manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
channel = new NotificationChannel(CHANNEL_ID,CHANNEL_ID,NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(channel);//创建通知渠道
Toast.makeText(this, "创建完毕", Toast.LENGTH_SHORT).show();
}
}
public NotificationService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
Log.d(TAG, "run: ");
}
}
}).start();
Intent intent1 = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent1,PendingIntent.FLAG_IMMUTABLE);
Notification notification = new NotificationCompat.Builder(this,CHANNEL_ID)
.setContentTitle("前台服务正在执行")
.setContentText("测试样例一")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.build();
startForeground(NOTIFICATION_ID,notification);//提升为前台服务
return START_STICKY;
}
}
这里需要一些Notification的知识,具体可以看我的关于notification的博客→Notification的使用
既然想要显示通知的话还是得先按照创建通知的流程来,我们在Service的创建时来初始化通知渠道,以便为后面的显示通知做准备。接下来我们创建一个Intent和PendingIntent,这主要是为了我们后面点击前台服务的通知时能够跳转到主Activity的界面。
接着创建Notification,设置需要显示的标题和正文等内容。最后最重要的就是startForeground方法,这个方法将会接受两个参数,第一个为需要显示通知的通知渠道ID,第二个就是我们之前创建的Notification。此通知必须拥有 PRIORITY_LOW 或更高的优先级。此方法会创建后台服务,但它会向系统发出信号,表明服务会将自行提升至前台。创建服务后,该服务必须在五秒内调用自己的 startForeground() 方法。
至此,Service的基础内容就比较详细地介绍完了,所有的示例demo的代码就放在我的github里了:→示例代码✿