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 {
@Nullable @Override public IBinder onBind(Intent intent) { return null; }
@Override public void onCreate() { System.out.println("onCreate invoke"); super.onCreate(); }
@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; int resId = R.raw.music; private static final String TAG = "MusicControlService";
private Handler handler = new Handler(); private Runnable updateProgressRunnable = new Runnable() { @Override public void run() { if (mp != null && mp.isPlaying()) { handler.postDelayed(this, 1000); } } };
@Nullable @Override public IBinder onBind(Intent intent) { return new Controller(); }
public class Controller extends Binder { public void setRes(int res) { resId = res; if (mp != null) { mp.release(); mp = MediaPlayer.create(MusicControlService.this, resId); } }
public void play() { if (mp != null && !mp.isPlaying()) { mp.start(); handler.post(updateProgressRunnable); 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); handler.post(updateProgressRunnable); }
@Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "服务销毁"); if (mp != null) { mp.release(); } mp = null; 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; int resId = R.raw.music; private static final String CHANNEL_ID = "MusicPlayerChannel"; private static final int NOTIFICATION_ID = 1; private static final String TAG = "MusicControlService";
private Handler handler = new Handler(); private Runnable updateProgressRunnable = new Runnable() { @Override public void run() { if (mp != null && mp.isPlaying()) { updateNotification(); handler.postDelayed(this, 1000); } } };
@Nullable @Override public IBinder onBind(Intent intent) { return new Controller(); }
public class Controller extends Binder { public void setRes(int res) { resId = res; if (mp != null) { mp.release(); mp = MediaPlayer.create(MusicControlService.this, resId); } }
public void play() { if (mp != null && !mp.isPlaying()) { mp.start(); updateNotification(); handler.post(updateProgressRunnable); 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); startForeground(NOTIFICATION_ID, createNotification()); Log.d(TAG, "前台服务已启动,通知已创建"); handler.post(updateProgressRunnable); }
@Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "服务销毁"); if (mp != null) { mp.release(); } mp = null; handler.removeCallbacks(updateProgressRunnable); }
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 notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
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 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) .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) { 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 <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)及以上版本中是必需的,以便系统和用户了解服务的意图。
- 用途: 它有助于系统优化资源和管理服务的优先级,确保媒体播放的流畅性。