Android后台服务

1. Service简单概述

Service(服务)是一个一种可以在后台执行长时间运行操作而没有用户界面的应用组件。服务可由其他应用组件启动(如Activity),服务一旦被启动将在后台一直运行,即使启动服务的组件(Activity)已销毁也不受影响。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行,Service基本上分为两种形式:

1.1 启动状态

  当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响,除非手动调用才能停止服务, 已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。

1.2 绑定状态

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

2. Service启动服务

首先要创建服务,必须创建 Service 的子类(或使用它的一个现有子类如IntentService)。

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
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.Nullable;

public class SimpleService extends Service {

/**
* 绑定服务时才会调用
* 必须要实现的方法
* @param intent
* @return
*/
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* 首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。
* 如果服务已在运行,则不会调用此方法。该方法只被调用一次
*/
@Override
public void onCreate() {
System.out.println("onCreate invoke");
super.onCreate();
}

/**
* 每次通过startService()方法启动Service时都会被回调。
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
System.out.println("onStartCommand invoke");
return super.onStartCommand(intent, flags, startId);
}

/**
* 服务销毁时的回调
*/
@Override
public void onDestroy() {
System.out.println("onDestroy invoke");
super.onDestroy();
}
}

`SimpleService继承了Service类,并重写了onBind方法,该方法是必须重写的,但是由于此时是启动状态的服务,则该方法无须实现,返回null即可,只有在绑定状态的情况下才需要实现该方法并返回一个IBinder的实现类(这个后面会详细说),接着重写了onCreate、onStartCommand、onDestroy三个主要的生命周期方法:

2.2 onBind()

当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,必须返回 一个IBinder 接口的实现类,供客户端用来与服务进行通信。无论是启动状态还是绑定状态,此方法必须重写,但在启动状态的情况下直接返回 null。

2.3 onCreate()

2.4 onStartCommand()

当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果自己实现此方法,则需要在服务工作完成后,通过调用 stopSelf() 或 stopService() 来停止服务。(在绑定状态下,无需实现此方法。)

2.5 onDestroy()

当服务不再使用且将被销毁时,系统将调用此方法。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等,这是服务接收的最后一个调用。

3. 项目练习

3.1 音乐播放器

要想播放音乐,我们可以使用android中的自带MediaPlayer对象,只需要将音乐的路径添加到其中,就可以进行播放,同时使用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
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package com.example.musiccontroller;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;

public class MusicControlService extends Service {
private MediaPlayer mp; // MediaPlayer实例,用于播放音乐
int resId = R.raw.music; // 默认音乐资源ID
private static final String TAG = "MusicControlService"; // 日志标签

// Handler用于更新音乐播放进度
private Handler handler = new Handler();
private Runnable updateProgressRunnable = new Runnable() {
@Override
public void run() {
// 如果MediaPlayer正在播放,继续更新进度
if (mp != null && mp.isPlaying()) {
// 这里可以添加代码以更新UI或处理其他逻辑
handler.postDelayed(this, 1000); // 每秒更新一次
}
}
};

@Nullable
@Override
public IBinder onBind(Intent intent) {
// 返回一个Binder对象,允许客户端绑定到这个服务
return new Controller();
}

// Binder类,提供服务的公开方法
public class Controller extends Binder {
// 设置音乐资源
public void setRes(int res) {
resId = res; // 更新资源ID
if (mp != null) {
mp.release(); // 释放当前MediaPlayer资源
mp = MediaPlayer.create(MusicControlService.this, resId); // 创建新的MediaPlayer实例
}
}

// 播放音乐
public void play() {
if (mp != null && !mp.isPlaying()) {
mp.start(); // 开始播放音乐
handler.post(updateProgressRunnable); // 启动进度更新的Runnable
Log.d(TAG, "音乐开始播放");
} else {
Log.e(TAG, "MediaPlayer 为空或已在播放状态");
}
}

// 暂停音乐
public void pause() {
if (mp != null && mp.isPlaying()) {
mp.pause(); // 暂停播放
handler.removeCallbacks(updateProgressRunnable); // 停止更新进度
Log.d(TAG, "音乐已暂停");
}
}

// 停止音乐播放
public void stop() {
if (mp != null) {
mp.stop(); // 停止播放
stopSelf(); // 停止服务
handler.removeCallbacks(updateProgressRunnable); // 停止更新进度
}
}

// 检查音乐是否在播放
public boolean isPlaying() {
return mp != null && mp.isPlaying();
}

// 获取音乐总时长
public int getDuration() {
return mp != null ? mp.getDuration() : 0;
}

// 获取当前播放位置
public int getCurrentPosition() {
return mp != null ? mp.getCurrentPosition() : 0;
}
}

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "服务创建成功");
mp = MediaPlayer.create(this, resId); // 初始化MediaPlayer
handler.post(updateProgressRunnable); // 启动进度更新的Runnable
}

@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "服务销毁");
if (mp != null) {
mp.release(); // 释放MediaPlayer资源
}
mp = null; // 清空MediaPlayer引用
handler.removeCallbacks(updateProgressRunnable); // 停止更新进度
}
}

3.2 在通知栏同步显示歌曲

这里就需要我们使用到NotificationChannel这个通知类了同时使用上述的binder对进度条进行同步:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package com.example.musiccontroller;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.widget.RemoteViews;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

public class MusicControlService extends Service {
private MediaPlayer mp; // MediaPlayer实例,用于播放音乐
int resId = R.raw.music; // 默认音乐资源ID
private static final String CHANNEL_ID = "MusicPlayerChannel"; // 通知频道ID
private static final int NOTIFICATION_ID = 1; // 通知ID
private static final String TAG = "MusicControlService"; // 日志标签

// Handler用于更新音乐播放进度
private Handler handler = new Handler();
private Runnable updateProgressRunnable = new Runnable() {
@Override
public void run() {
// 如果MediaPlayer正在播放,更新通知并每秒调用自己
if (mp != null && mp.isPlaying()) {
updateNotification(); // 更新通知内容
handler.postDelayed(this, 1000); // 每秒更新一次
}
}
};

@Nullable
@Override
public IBinder onBind(Intent intent) {
// 返回一个Binder对象,允许客户端绑定到这个服务
return new Controller();
}

// Binder类,提供服务的公开方法
public class Controller extends Binder {
// 设置音乐资源
public void setRes(int res) {
resId = res; // 更新资源ID
if (mp != null) {
mp.release(); // 释放当前MediaPlayer资源
mp = MediaPlayer.create(MusicControlService.this, resId); // 创建新的MediaPlayer实例
}
}

// 播放音乐
public void play() {
if (mp != null && !mp.isPlaying()) {
mp.start(); // 开始播放音乐
updateNotification(); // 更新通知显示为播放状态
handler.post(updateProgressRunnable); // 启动进度更新的Runnable
Log.d(TAG, "音乐开始播放");
} else {
Log.e(TAG, "MediaPlayer 为空或已在播放状态");
}
}

// 暂停音乐
public void pause() {
if (mp != null && mp.isPlaying()) {
mp.pause(); // 暂停播放
updateNotification(); // 更新通知显示为暂停状态
handler.removeCallbacks(updateProgressRunnable); // 停止更新进度
Log.d(TAG, "音乐已暂停");
}
}

// 停止音乐播放
public void stop() {
if (mp != null) {
mp.stop(); // 停止播放
stopForeground(true); // 停止前台服务
stopSelf(); // 停止服务
handler.removeCallbacks(updateProgressRunnable); // 停止更新进度
}
}

// 检查音乐是否在播放
public boolean isPlaying() {
return mp != null && mp.isPlaying();
}

// 获取音乐总时长
public int getDuration() {
return mp != null ? mp.getDuration() : 0;
}

// 获取当前播放位置
public int getCurrentPosition() {
return mp != null ? mp.getCurrentPosition() : 0;
}
}

@SuppressLint("ForegroundServiceType")
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "服务创建成功");
createNotificationChannel(); // 创建通知频道
mp = MediaPlayer.create(this, resId); // 初始化MediaPlayer
startForeground(NOTIFICATION_ID, createNotification()); // 启动前台服务并显示通知
Log.d(TAG, "前台服务已启动,通知已创建");
handler.post(updateProgressRunnable); // 启动进度更新的Runnable
}

@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "服务销毁");
if (mp != null) {
mp.release(); // 释放MediaPlayer资源
}
mp = null; // 清空MediaPlayer引用
handler.removeCallbacks(updateProgressRunnable); // 停止更新进度
}

// 创建通知频道(Android 8.0及以上)
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel serviceChannel = new NotificationChannel(
CHANNEL_ID,
"音乐播放器频道", // 频道名称
NotificationManager.IMPORTANCE_LOW // 设置重要性为低
);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(serviceChannel); // 创建频道
Log.d(TAG, "通知频道创建成功");
} else {
Log.e(TAG, "通知管理器为空,无法创建频道");
}
}
}

// 创建通知
private Notification createNotification() {
Log.d(TAG, "创建通知");

// 定义自定义通知布局
RemoteViews notificationLayout = new RemoteViews(getPackageName(), R.layout.notification_layout);

// 填充通知布局的文本信息
notificationLayout.setTextViewText(R.id.notification_title, "音乐播放器");
notificationLayout.setTextViewText(R.id.notification_text, mp != null && mp.isPlaying() ? "正在播放音乐" : "音乐已暂停");

// 更新进度条
if (mp != null) {
int progress = (int) ((mp.getCurrentPosition() / (float) mp.getDuration()) * 100);
notificationLayout.setProgressBar(R.id.notification_progress, 100, progress, false);
}

// 创建点击通知后打开应用的Intent
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

// 播放操作的Intent
Intent playIntent = new Intent(this, MusicControlService.class);
playIntent.setAction("ACTION_PLAY"); // 设置操作标识为播放
PendingIntent playPendingIntent = PendingIntent.getService(this, 1, playIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

// 暂停操作的Intent
Intent pauseIntent = new Intent(this, MusicControlService.class);
pauseIntent.setAction("ACTION_PAUSE"); // 设置操作标识为暂停
PendingIntent pausePendingIntent = PendingIntent.getService(this, 2, pauseIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

// 创建通知构建器
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.hmbb) // 通知小图标
.setContentIntent(pendingIntent) // 点击通知时打开的Intent
.setCustomContentView(notificationLayout) // 设置自定义布局
.addAction(android.R.drawable.ic_media_play, "播放", playPendingIntent) // 添加播放按钮
.addAction(android.R.drawable.ic_media_pause, "暂停", pausePendingIntent) // 添加暂停按钮
.setPriority(NotificationCompat.PRIORITY_LOW); // 设置通知优先级为低

return builder.build(); // 返回构建好的通知
}

// 更新通知内容
private void updateNotification() {
Log.d(TAG, "更新通知");
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (manager != null) {
// 使用 createNotification 获取更新后的通知
Notification notification = createNotification();
manager.notify(NOTIFICATION_ID, notification); // 发送更新后的通知
} else {
Log.e(TAG, "更新通知时通知管理器为空");
}
}
}

这里使用了一个自定的布局notificationLayout,是为了将通知栏的样式构建的更加好看:

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
z<!-- res/layout/notification_layout.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">

<TextView
android:id="@+id/notification_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black"
android:text="音乐播放器" />

<TextView
android:id="@+id/notification_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@android:color/darker_gray" />

<ProgressBar
android:id="@+id/notification_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
style="?android:attr/progressBarStyleHorizontal" />
</LinearLayout>

4. 权限开启

1
2
3
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

  • 描述: 这个权限允许应用程序运行前台服务。前台服务通常会显示一个持续的通知,以让用户知道服务正在运行。
  • 用途: 适用于需要在后台执行长时间操作的服务,例如播放音乐、下载文件或处理实时数据。

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

  • 描述: 这个权限允许应用程序发送通知到系统通知栏。
  • 用途: 在 Android 13(API 级别 33)及以上版本中,应用需要显式请求此权限才能发送通知。它用于通知用户应用的状态或重要事件。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

  • 描述: 这个权限允许应用程序在前台服务中进行媒体播放。
  • 用途: 特别适用于需要在前台服务中播放音频或视频的应用,例如音乐播放器或视频播放器。它使得媒体播放可以在用户界面不活跃的情况下持续进行。

3.2 启动事务

1
2
3
4
5
<service
android:name=".MusicControlService"
android:enabled="true"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />

android:enabled="true"

  • 描述: 该属性指示服务是否可以被系统启用。设置为 true 表示服务可以被启动(默认值为 true)。

android:foregroundServiceType="mediaPlayback"

  • 描述: 这个属性指示该服务是一个前台服务,并且其主要功能是媒体播放。这在 Android 9(API 级别 28)及以上版本中是必需的,以便系统和用户了解服务的意图。
  • 用途: 它有助于系统优化资源和管理服务的优先级,确保媒体播放的流畅性。