Android--四大组件之Service

1 Service简介

1.1 Service概览

Service是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信(IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件I/O或与内容提供程序进行交互。

以下是三种不同的服务类型:

  • 前台:前台服务执行一些用户能注意到的操作。例如,音频应用会使用前台服务来播放音频曲目。前台服务必须显示通知。即使用户停止与应用的交互,前台服务仍会继续运行。

  • 后台:后台服务执行用户不会直接注意到的操作。例如,如果应用使用某个服务来压缩其存储空间,则此服务通常是后台服务。

【注意】如果应用面向API级别26或更高版本,当应用本身未在前台运行时,系统会对运行后台服务施加限制。在诸如此类的大多数情况下,应用应改为使用计划作业。

  • 绑定:当应用组件通过调用bindService()绑定到服务时,服务即处于绑定状态。绑定服务会提供客户端-服务器接口,以便组件与服务进行交互、发送请求、接收结果,甚至是利用进程间通信(IPC)跨进程执行这些操作。仅当与另一个应用组件绑定时,绑定服务才会运行。多个组件可同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

服务可同时以启动服务和绑定服务两种方式运行,换言之,它既可以是启动服务(以无限期运行),亦支持绑定。唯一的问题在于是否实现一组回调方法:onStartCommand()(让组件启动服务)和onBind()(实现服务绑定)。

无论服务是处于启动状态还是绑定状态(或同时处于这两种状态),任何应用组件均可像使用Activity那样,通过调用Intent来使用服务(即使此服务来自另一应用)。不过,可以通过清单文件将服务声明为私有服务,并阻止其他应用访问该服务。

【注意】服务在其托管进程的主线程中运行,它既不创建自己的线程,也不在单独的进程中运行(除非另行指定)。如果服务将执行任何CPU密集型工作或阻止性操作(例如MP3播放或联网),则应通过在服务内创建新线程来完成这项工作。通过使用单独的线程,可以降低发生“应用无响应”(ANR)错误的风险,而应用的主线程仍可继续专注于运行用户与Activity之间的交互。

1.2 在服务和线程之间进行选择

简单地说,服务是一种即使用户未与应用交互也可在后台运行的组件,因此,只有在需要服务时才应创建服务。

如果必须在主线程之外执行操作,但只在用户与应用交互时执行此操作,则应创建新线程。例如,如果只是想在Activity运行的同时播放一些音乐,则可在onCreate()中创建线程,在onStart()中启动线程运行,然后在onStop()中停止线程。还可考虑使用AsyncTaskHandlerThread,而非传统的Thread类。

请记住,如果确实要使用服务,则默认情况下,它仍会在应用的主线程中运行,因此,如果服务执行的是密集型或阻止性操作,则仍应在服务内创建新线程。

1.3 基础知识

如要创建服务,必须创建Service的子类(或使用它的一个现有子类)。在实现中,必须重写一些回调方法,从而处理服务生命周期的某些关键方面,并提供一种机制将组件绑定到服务(如适用)。以下是应重写的最重要的回调方法:

  • onStartCommand():当另一个组件(如Activity)请求启动服务时,系统会通过调用startService()来调用此方法。执行此方法时,服务即会启动并可在后台无限期运行。如果实现此方法,则在服务工作完成后,需负责通过调用stopSelf()stopService()来停止服务。(如果只想提供绑定,则无需实现此方法。)

  • onBind():当另一个组件想要与服务绑定(例如执行RPC)时,系统会通过调用bindService()来调用此方法。在此方法的实现中,必须通过返回IBinder提供一个接口,以供客户端用来与服务进行通信。请务必实现此方法;但是,如果并不希望允许绑定,则应返回null。

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

  • onDestroy():当不再使用服务且准备将其销毁时,系统会调用此方法。服务应通过实现此方法来清理任何资源,如线程、注册的侦听器、接收器等。这是服务接收的最后一个调用。

如果组件通过调用startService()启动服务(这会引起对onStartCommand()的调用),则服务会一直运行,直到其使用stopSelf()自行停止运行,或由其他组件通过调用stopService()将其停止为止。

如果组件通过调用bindService()来创建服务,且未调用onStartCommand(),则服务只会在该组件与其绑定时运行。当该服务与其所有组件取消绑定后,系统便会将其销毁。

只有在内存过低且必须回收系统资源以供拥有用户焦点的Activity使用时,Android系统才会停止服务。如果将服务绑定到拥有用户焦点的Activity,则它其不太可能会终止;如果将服务声明为在前台运行,则其几乎永远不会终止。如果服务已启动并长时间运行,则系统逐渐降低其在后台任务列表中的位置,而服务被终止的概率也会大幅提升—如果服务是启动服务,则必须将其设计为能够妥善处理系统执行的重启。如果系统终止服务,则其会在资源可用时立即重启服务,但这还取决于从onStartCommand()返回的值。

1.4 使用清单文件声明服务

如同对Activity及其他组件的操作一样,必须在应用的清单文件中声明所有服务,请添加<service>元素作为<application>元素的子元素,下面是示例:

1
2
3
4
5
6
7
8
9
10
<manifest ... >
...
<application ... >
<service>
android:name=".ExampleService"
android:exported="true"
</service>
...
</application>
</manifest>

还可在<service>元素中加入其他属性,以定义一些特性,如启动服务及其运行时所在进程需要的权限。android:name属性是唯一必需的属性,用于指定服务的类名。发布应用后,请保此类名不变,以避免因依赖显式Intent来启动或绑定服务而破坏代码的风险。

【注意】为确保应用的安全性,在启动Service时,请始终使用显式Intent,且不要为服务声明Intent过滤器。使用隐式Intent启动服务存在安全隐患,因为无法确定哪些服务会响应Intent,而用户也无法看到哪些服务已启动。从Android 5.0(API级别21)开始,如果使用隐式Intent调用bindService(),则系统会抛出异常。

可以通过添加android:exported属性并将其设置为false,确保服务仅适用于应用。这可以有效阻止其他应用启动服务,即便在使用显式Intent时也如此。

【注意】用户可以查看其设备上正在运行的服务。如果他们发现自己无法识别或信任的服务,则可以停止该服务。为避免用户意外停止服务,需要在应用清单的<service>元素中添加android:description,请在描述中用一个短句解释服务的作用及其提供的好处。

2 创建服务

2.1 创建启动服务

启动服务由另一个组件通过调用startService()启动,这会导致调用服务的onStartCommand()方法。

服务启动后,其生命周期即独立于启动它的组件。即使系统已销毁启动服务的组件,该服务仍可在后台无限期地运行。因此,服务应在其工作完成时通过调用stopSelf()来自行停止运行,或者由另一个组件通过调用stopService()来将其停止。

应用组件(如Activity)可通过调用startService()方法并传递Intent对象(指定服务并包含待使用服务的所有数据)来启动服务。服务会在onStartCommand()方法接收此Intent

例如,假设某Activity需要将一些数据保存到在线数据库中。该Activity可以启动一个协同服务,并通过向startService()传递一个Intent,为该服务提供要保存的数据。服务会通过onStartCommand()接收Intent,连接到互联网并执行数据库事务。事务完成后,服务将自行停止并销毁。

【注意】默认情况下,服务与服务声明所在的应用运行于同一进程,并且运行于该应用的主线程中。如果服务在用户与来自同一应用的Activity进行交互时执行密集型或阻止性操作,则会降低Activity性能。为避免影响应用性能,请在服务内启动新线程。

通常,可以扩展两个类来创建启动服务:

  • Service:这是适用于所有服务的基类。扩展此类时,必须创建用于执行所有服务工作的新线程,因为服务默认使用应用的主线程,这会降低应用正在运行的任何 Activity 的性能。

  • IntentService:这是Service的子类,其使用工作线程逐一处理所有启动请求。如果不要求服务同时处理多个请求,此类为最佳选择。实现onHandleIntent(),该方法会接收每个启动请求的Intent,以便执行后台工作。

2.2 启动服务

可以通过将Intent传递给startService()startForegroundService(),从Activity或其他应用组件启动服务。Android系统会调用服务的onStartCommand()方法,并向其传递Intent,从而指定要启动的服务。

【注意】如果应用面向API级别26或更高版本,除非应用本身在前台运行,否则系统不会对使用或创建后台服务施加限制。如果应用需要创建前台服务,则其应调用startForegroundService()。此方法会创建后台服务,但它会向系统发出信号,表明服务会将自行提升至前台。创建服务后,该服务必须在五秒内调用自己的startForeground()方法。

例如,Activity可以结合使用显式Intent与startService(),从而启动上文中的示例服务(HelloService):

1
2
Intent intent = new Intent(this, HelloService.class);
startService(intent);

startService()方法会立即返回,并且Android系统会调用服务的onStartCommand()方法。如果服务尚未运行,则系统首先会调用onCreate(),然后调用onStartCommand()

如果服务亦未提供绑定,则应用组件与服务间的唯一通信模式便是使用startService()传递的Intent。但是,如果希望服务返回结果,则启动服务的客户端可以为广播(通过getBroadcast()获得)创建一个PendingIntent,并将其传递给启动服务的Intent中的服务。然后,服务便可使用广播传递结果。

多个服务启动请求会导致多次对服务的onStartCommand()进行相应的调用。但是,如要停止服务,只需一个服务停止请求(使用stopSelf()stopService())即可。

2.3 停止服务

启动服务必须管理自己的生命周期。换言之,除非必须回收内存资源,否则系统不会停止或销毁服务,并且服务在onStartCommand()返回后仍会继续运行。服务必须通过调用stopSelf()自行停止运行,或由另一个组件通过调用stopService()来停止它。

一旦请求使用stopSelf()stopService()来停止服务,系统便会尽快销毁服务。

如果服务同时处理多个对onStartCommand()的请求,则不应在处理完一个启动请求之后停止服务,因为可能已收到新的启动请求(在第一个请求结束时停止服务会终止第二个请求)。为避免此问题,可以使用stopSelf(int)确保服务停止请求始终基于最近的启动请求。换言之,在调用stopSelf(int)时,需传递与停止请求ID相对应的启动请求ID(传递给onStartCommand()的startId)。此外,如果服务在能够调用stopSelf(int)之前收到新启动请求,则ID不匹配,服务也不会停止。

【注意】为避免浪费系统资源和消耗电池电量,请确保应用在工作完成之后停止其服务。如有必要,其他组件可通过调用stopService()来停止服务。即使为服务启用绑定,如果服务收到对onStartCommand()的调用,始终仍须亲自停止服务。

2.4 创建绑定服务

绑定服务允许应用组件通过调用bindService()与其绑定,从而创建长期连接。此服务通常不允许组件通过调用startService()来启动它。

如需与Activity和其他应用组件中的服务进行交互,或需要通过进程间通信(IPC)向其他应用公开某些应用功能,则应创建绑定服务。

如要创建绑定服务,需通过实现onBind()回调方法返回IBinder,从而定义与服务进行通信的接口。然后,其他应用组件可通过调用bindService()来检索该接口,并开始调用与服务相关的方法。服务只用于与其绑定的应用组件,因此若没有组件与该服务绑定,则系统会销毁该服务。不必像通过onStartCommand()启动的服务那样,以相同方式停止绑定服务。

如要创建绑定服务,必须定义指定客户端如何与服务进行通信的接口。服务与客户端之间的这个接口必须是IBinder的实现,并且服务必须从onBind()回调方法返回该接口。收到IBinder后,客户端便可开始通过该接口与服务进行交互。

多个客户端可以同时绑定到服务。完成与服务的交互后,客户端会通过调用unbindService()来取消绑定。如果没有绑定到服务的客户端,则系统会销毁该服务。

实现绑定服务有多种方法,并且此实现比启动服务更为复杂。

2.5 管理服务的生命周期

服务的生命周期比Activity的生命周期要简单得多。但是,密切关注如何创建和销毁服务反而更加重要,因为服务可以在用户未意识到的情况下运行于后台。服务生命周期(从创建到销毁)可遵循以下任一路径:

  • 启动服务:该服务在其他组件调用startService()时创建,然后无限期运行,且必须通过调用stopSelf()来自行停止运行。此外,其他组件也可通过调用stopService()来停止此服务。服务停止后,系统会将其销毁。

  • 绑定服务:该服务在其他组件(客户端)调用bindService()时创建。然后,客户端通过IBinder接口与服务进行通信。客户端可通过调用unbindService()关闭连接。多个客户端可以绑定到相同服务,而且当所有绑定全部取消后,系统即会销毁该服务。(服务不必自行停止运行。)

这两条路径并非完全独立。可以绑定到已使用startService()启动的服务。例如,可以使用Intent(标识要播放的音乐)来调用startService(),从而启动后台音乐服务。随后,当用户需稍加控制播放器或获取有关当前所播放歌曲的信息时,Activity可通过调用bindService()绑定到服务。此类情况下,在所有客户端取消绑定之前,stopService()stopSelf()实际不会停止服务。

started service(启动服务)是由其他组件调用startService()方法启动的。使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点,关于Service生命周期如图1所示。

图1 已启动并且还允许绑定的服务的生命周期

2.6 实现生命周期回调

与Activity类似,服务也拥有生命周期回调方法,可通过实现这些方法来监控服务状态的变化并适时执行工作,以下框架服务展示了每种生命周期方法:

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
public class ExampleService extends Service {
int startMode; // indicates how to behave if the service is killed
IBinder binder; // interface for clients that bind
boolean allowRebind; // indicates whether onRebind should be used

@Override
public void onCreate() {
// The service is being created
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// The service is starting, due to a call to startService()
return mStartMode;
}
@Override
public IBinder onBind(Intent intent) {
// A client is binding to the service with bindService()
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
// All clients have unbound with unbindService()
return mAllowRebind;
}
@Override
public void onRebind(Intent intent) {
// A client is binding to the service with bindService(),
// after onUnbind() has already been called
}
@Override
public void onDestroy() {
// The service is no longer used and is being destroyed
}
}

【注意】与Activity生命周期回调方法不同,我们不需要调用这些回调方法的超类实现。

图2 服务生命周期

图2展示服务的典型回调方法,左图显示使用startService()创建的服务的生命周期,右图显示使用bindService()创建的服务的生命周期。尽管该图分开介绍通过startService()创建的服务和通过bindService()创建的服务,但请记住,无论启动方式如何,任何服务均有可能允许客户端与其绑定。因此,最初使用onStartCommand()(通过客户端调用startService())启动的服务仍可接收对onBind()的调用(当客户端调用bindService()时)。

通过实现这些方法,可以监控服务生命周期的以下两种嵌套循环:

  • 服务的整个生命周期贯穿调用onCreate()和返回onDestroy()之间的这段时间。与Activity类似,服务也在onCreate()中完成初始设置,并在onDestroy()中释放所有剩余资源。例如,音乐播放服务可以在onCreate()中创建用于播放音乐的线程,然后在onDestroy()中停止该线程。无论所有服务是通过startService()还是bindService()创建,系统均会为其调用onCreate()onDestroy()方法。

  • 服务的活动生命周期从调用onStartCommand()onBind()开始。每种方法均会获得Intent对象,该对象会传递至startService()bindService()。对于启动服务,活动生命周期与整个生命周期会同时结束(即便是在onStartCommand()返回之后,服务仍然处于活动状态)。对于绑定服务,活动生命周期会在onUnbind()返回时结束。

【注意】尽管需通过调用stopSelf()stopService()来停止绑定服务,但该服务并没有相应的回调(没有onStop()回调)。除非服务绑定到客户端,否则在服务停止时,系统会将其销毁(onDestroy()是接收到的唯一回调)。

3 Service生命周期示例

【注意】这里介绍一个小示例,我使用的AVD的Android 6.0(API级别23),某些高版本Android好像不能出现预想的效果。下面分别介绍MainActivity.java、MusicService.java、activity_main.xml和AndroidManifest.xml文件的详细内容。

MainActivity.java主要是设置按钮的响应事件

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// MainActivity.java
package com.musicservice;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

public static final String TAG = "MainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Toast.makeText(this, "MainActivity", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MainActivity onCreate()");
initlizeViews();
}

private void initlizeViews() {
Button btnStart = (Button) findViewById(R.id.startMusic);
Button btnStop = (Button) findViewById(R.id.stopMusic);
Button btnBind = (Button) findViewById(R.id.bindMusic);
Button btnUnbind = (Button) findViewById(R.id.unbindMusic);

OnClickListener ocl = new OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, MusicService.class);
switch (view.getId()) {
case R.id.startMusic:
startService(intent);
break;
case R.id.stopMusic:
stopService(intent);
break;
case R.id.bindMusic:
bindService(intent, conn, Context.BIND_AUTO_CREATE);
break;
case R.id.unbindMusic:
unbindService(conn);
break;
}
}
};

btnStart.setOnClickListener(ocl);
btnStop.setOnClickListener(ocl);
btnBind.setOnClickListener(ocl);
btnUnbind.setOnClickListener(ocl);
}

final ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Toast.makeText(MainActivity.this, "MainActivity onServiceConnected", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MainActivity onServiceConnected");
}

@Override
public void onServiceDisconnected(ComponentName componentName) {
Toast.makeText(MainActivity.this, "MainActivity onServiceDisconnected", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MainActivity onServiceDisconnected");
}
};
}

MusicService.java主要是实现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
52
53
54
55
56
// MusicService.java
package com.musicservice;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

public class MusicService extends Service {

private MediaPlayer mPlayer;
public static final String TAG = "MusicServiceActivity";

@Override
public void onCreate() {
Toast.makeText(this, "MusicService onCreate()", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MusicService onCreate()");
mPlayer = MediaPlayer.create(getApplicationContext(), R.raw.music);
mPlayer.start();
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId){
mPlayer.start();
Toast.makeText(this, "MusicSevice onStartCommand()", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MusicSevice onStartCommand()");
return START_STICKY;
}

@Override
public void onDestroy() {
Toast.makeText(this, "MusicService onDestroy()", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MusicService onDestroy()");
mPlayer.stop();
super.onDestroy();
}

@Override
public IBinder onBind(Intent intent) {
Toast.makeText(this, "MusicService onBind()", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MusicService onBind()");
mPlayer.start();
return null;
}

@Override
public boolean onUnbind(Intent intent) {
Toast.makeText(this, "MusicService onUnbind()", Toast.LENGTH_SHORT).show();
Log.d(TAG, "MusicService onUnbind()");
mPlayer.stop();
return super.onUnbind(intent);
}
}

activity_main.xml主要是对MainActivity界面进行布局,主要是放了4个按钮,如图3所示。

图3 activity_main.xml布局文件

AndroidManifest.xml主要是配置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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.musicservice">

<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />

<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/Theme.Helloworld">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MusicService" android:exported="true" />
</application>

</manifest>

首先,点击“开启音乐播放服务”按钮,即可播放对应的音乐,将应用退出后台后音乐播放暂停,但立刻音乐又重新开始播放。如果点击“停止音乐播放服务”按钮,即可停止播放音乐。这是因为在执行startService时,Service会经历onCreate->onStartCommand。当执行stopService时,直接调用onDestroy方法。这里调用者如果没有stopService,Service会一直在后台运行,下次调用者再起来仍然可以stopService。

然后,点击“绑定音乐播放服务”按钮,即可播放对应的音乐,点击“解除绑定音乐播放服务”按钮,即可停止播放音乐。这是因为执行bindService时,Service会经历onCreate->onBind。这个时候调用者和Service绑定在一起。调用者调用unbindService方法或者调用者Context不存在了(如Activity被finish了),Service就会调用onUnbind->onDestroy。这里所谓的绑定在一起就是说两者共存亡了。

4 参考文献


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。

×

喜欢就点赞,疼爱就打赏