主要内容:Service服务3种实现 + MediaPlayer编程
Service概述
◼ Service:是一种可长时间运行在后台,而不提供界面的应用组件。
◼ Service可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。
◼ 当应用程序进程被杀掉后,所有依赖于该进程的Service也会停止运行。
◼ Service是Android系统四大组件之一。
Service和Thread区别
◼ 简单地说,Service 和 Thread 没有关系,是两个不同概念。
◼ Thread 是用于开启一个子线程,在这里去执行一些耗时操作就不会阻塞主线程的运行,而 Service 是运行在主线程里的后台程序。(Service 不是子线程)
◼ Why Service?
◼ 如果没有 Service,在 Activity 启动子线程后,子线程的运行就独立于 Activity,当 Activity 被销毁之后,就无法再重新获取到之前创建的子线程(”野线程”)。另外在一个 Activity 中创建的子线程,其他 Activity 无法对其进行操作。
◼ Service 就不同了,所有的 Activity 都可以与 Service 进行绑定,即使 Activity 被销毁了,之后只要重新与 Service 重新绑定,就又能获取原来 Service 实例。
◼ 因此,使用 Service 来处理后台任务(创建子线程),Activity 就可以放心地 finish,完全不需要担心无法对后台任务进行控制的情况。
Service类型
◼ Service类型有3种:
◼ 后台服务:执行用户不会直接注意到的操作(普通启动型)
◼ 前台服务:执行一些用户能注意到的操作(必须显示通知)
◼ 绑定服务:提供客户端-服务端接口,以便组件与服务进行交互
后台服务(也称启动型服务)
◼ 执行用户不会直接注意到的操作
◼ 后台服务优先级相对较低,当系统内存不足时,后台服务有可能被回收
◼ 启动和终止服务:
• 应用组件通过 startService(intent)
启动服务
• 终止服务:在服务内部调用 stopSelf()
,或由其他组件在外部调用 stopService(intent)
来停止 // 必须和 startService(intent) 是同一个 intent
• 无论调用了多少次 startService()
,只需调用一次 stopService()
来停止
前台服务
◼ 前台服务必须显示一个通知,以使用户意识到服务正在后台执行。
◼ 启动和终止服务:
• 组件通过 startForegroundService(intent)
启动服务 + startForeground(通知id, 通知) 发送通知
• 外部终止服务:stopService(intent)
// 会移除前台服务的通知
• 在服务内部主动移除通知:stopForeground(true)
• 除非服务被终止或主动移除通知,否则通知无法关闭 ==强制结束APP也不行==
绑定服务
◼ 绑定服务是客户端-服务器接口中的服务器
◼ 绑定服务通常只在为其他应用组件提供服务时处于活动状态,通常不用无限期在后台运行
◼ 启动和终止服务:
• 组件通过调用 bindService(intent, conn, flag) 与服务绑定
• 外部终止服务:unbindService(conn) 关闭服务 // 用于服务连接对象
• 服务一旦启用,调用者就与服务绑定在一起,调用者一旦退出,服务也就终止
• 多个组件可以同时绑定到一个服务,当全部绑定解除后,服务才被销毁。
• 借助绑定服务,组件可以绑定到服务、发送请求、接收响应,以及执行进程间通信 (IPC)
Service生命周期
Service实现
– 音乐播放服务为例
◼ 创建服务
◼ 服务实现
创建服务
◼ 新建 Module:如 ServiceDemo
◼ MainActivity 界面布局如下
activity_main.xml
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
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="32dp" android:layout_marginTop="100dp" android:text="启动服务" android:textSize="16sp" app:icon="@android:drawable/ic_media_play" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="56dp" android:layout_marginTop="100dp" android:layout_marginEnd="32dp" android:text="结束服务" android:textSize="16sp" app:icon="@android:drawable/ic_media_pause" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/btn_start" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
|
创建 raw 资源文件夹
◼ 在 res 文件夹下新建 raw 资源文件夹
说明:raw 文件夹通常存放资源文件,如音频、视频等,通过 R.raw.资源id 可定位资源
注意:资源文件名要求首字母小写或以下划线开头,不要出现中文和空格,也不要以全数字命名
添加音乐资源
◼ 将音乐文件(mp3格式)复制到 raw 文件夹中
新建服务
服务配置
自动生成的服务框架:
1 2 3 4 5 6 7 8 9 10
| public class MusicService extends Service { public MusicService() { } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } }
|
服务的配置:
1 2 3 4 5
| <service android:name=".MusicService" android:enabled="true" <!-- 如果为false就不能被调用 --> android:exported="true" > </service>
|
服务实现
◼ 3种服务实现方法:
◼ 后台服务实现
◼ 前台服务实现
◼ 绑定服务实现
后台型服务实现
◼ 将服务业务写在不同生命周期回调函数中
◼ 在服务中使用 MediaPlayer 播放音乐
◼ 外部按钮:启动和结束服务
(1)服务添加生命周期回调函数:
先熟悉一下服务的生命周期
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
| public class MusicService extends Service { public MusicService() { }
@Override public void onCreate() { super.onCreate(); Log.d("flag","onCreate"); }
@Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d("flag","onStartCommand"); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Log.d("flag","onDestroy"); } @Override public IBinder onBind(Intent intent) { Log.d("flag","onBind"); throw new UnsupportedOperationException("Not yet implemented"); } }
|
(2)启动和结束服务:
◼ 启动服务:
1 2
| Intent intent = new Intent( MainActivity.this, MusicService.class ); startService(intent);
|
◼ 结束服务:
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
| public class MainActivity extends AppCompatActivity { Button btn_start; Button btn_stop; View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent( MainActivity.this, MusicService.class ); intent.putExtra("msg", "hello"); switch( v.getId() ){ case R.id.btn_start: startService(intent); break; case R.id.btn_stop: stopService(intent); break; } } };
public void initView() { btn_start = (Button) findViewById(R.id.btn_start); btn_stop = (Button) findViewById(R.id.btn_stop); btn_start.setOnClickListener(listener); btn_stop.setOnClickListener(listener); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } }
|
先测试一下:服务的生命周期
(3)在服务中添加音乐播放功能
◼ MediaPlayer:Android内置的音频和视频播放器
◼ MediaPlayer常用方法:
方法名 |
功能描述 |
setDataSource() |
设置要播放的音频文件的位置。 |
prepare() |
在开始播放之前调用这个方法完成准备工作。 |
start() |
开始或继续播放音频。 |
pauseO) |
暂停播放音频。 |
reset() |
将MediaPlayer对象重置到刚刚创建的状态。 |
seekTo() |
从指定的位置开始播放音频。 |
stop() |
停止播放音频。调用这个方法后的MediaPlayer对象无法再播放音频。 |
release() |
释放掉与MediaPlayer对象相关的资源。 |
isPlayingO |
判断当前MediaPlayer是否正在播放音频。 |
getDuration() |
获取载入的音频文件的时长。 |
getCurrentPosition() |
获取音乐播放的当前位置 |
音乐播放具体实现:
MusicService.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
| public class MusicService extends Service { MediaPlayer mediaPlayer; public void init() { if (mediaPlayer == null) { mediaPlayer = MediaPlayer.create( getApplicationContext(), R.raw.she ); mediaPlayer.setLooping(true); } } public void start() { mediaPlayer.seekTo(0); mediaPlayer.start(); }
public void stop() { if (mediaPlayer != null) { mediaPlayer.stop(); mediaPlayer.release(); } } public MusicService() { } @Override public void onCreate() { super.onCreate(); Log.d("flag", "onCreate"); init(); }
@Override public int onStartCommand( Intent intent, int flags, int startId ) { Log.d("flag", "onStartCommand"); Log.d("flag", intent.getStringExtra("msg") ); start(); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Log.d("flag", "onDestroy"); stop(); } @Override public IBinder onBind(Intent intent) { Log.d("flag", "onBind"); throw new UnsupportedOperationException("Not yet implemented"); } }
|
运行结果:
前台服务实现
◼ 前台服务必须显示一个通知,以使用户意识到服务正在后台执行。
◼ 前台服务的启动和结束:
1 2
| Intent intent = new Intent( MainActivity.this, MusicService.class ); startForegroundService(intent);
|
◼ 通知的发送和清除:
1 2
| startForeground(通知ID, notification); stopForeground(true);
|
注:除非服务被终止 stopService() 或 stopForeground(true) 主动移除通知,否则通知无法关闭
具体实现:
(1)新建:MainActivity2 和 MusicService2(服务)
(2)申请2个权限:
• 前台服务的权限:FOREGROUND_SERVICE
• 通知发送的权限:POST_NOTIFICATIONS
AndroidManifest.xml
1 2
| <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
(3)MainActivity2配置:启动程序 + 单例启动模式
AndroidManifest.xml
1 2 3 4 5 6 7 8 9
| <activity android:name=".MainActivity2" android:launchMode="singleInstance" <!-- Activity设置为单例启动模式(默认值是standard)--> android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
|
MainActivity2.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
| public class MainActivity2 extends AppCompatActivity { private final ActivityResultLauncher permissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() { @Override public void onActivityResult(Map<String, Boolean> result) { if (result.get(Manifest.permission.POST_NOTIFICATIONS) != null && result.get(Manifest.permission.FOREGROUND_SERVICE) != null) { if (Objects.requireNonNull(result.get(Manifest.permission.POST_NOTIFICATIONS)).equals(true) && Objects.requireNonNull(result.get(Manifest.permission.FOREGROUND_SERVICE)).equals(true)) {
Log.d("flag", "获得所有相关权限"); } else {
} } } } );
private void requestPermission() { String[] permissions = { Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.FOREGROUND_SERVICE }; permissionLauncher.launch(permissions); } View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent( MainActivity2.this, MusicService2.class ); switch( v.getId() ){ case R.id.btn_start: startForegroundService(intent); break; case R.id.btn_stop: stopService(intent); break; } } };
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView( R.layout.activity_main ); requestPermission(); Button btn_start = (Button) findViewById(R.id.btn_start); Button btn_stop = (Button) findViewById(R.id.btn_stop); btn_start.setOnClickListener(listener); btn_stop.setOnClickListener(listener); } }
|
前台服务的实现:MusicService2
MusicService2.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
| public class MusicService2 extends Service { MediaPlayer mediaPlayer; private static final int NOTIFY_ID = 1; private static final String CHANNEL_ID = "123"; private static final String CHANNEL_Name = "mychannel"; NotificationManager manager; Notification notification; NotificationChannel channel; PendingIntent pendingIntent; Bitmap bitmap;
public void sendNotity() { manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); channel = new NotificationChannel(CHANNEL_ID, CHANNEL_Name, NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(channel); Intent intent = new Intent(MusicService2.this, MainActivity2.class); pendingIntent = PendingIntent.getActivity(MusicService2.this, 0, intent, PendingIntent.FLAG_IMMUTABLE); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.gu); notification = new Notification.Builder(MusicService2.this, CHANNEL_ID) .setContentTitle("提醒") .setContentText("通知内容…") .setWhen(System.currentTimeMillis()) .setShowWhen(true) .setSmallIcon(R.drawable.twitter) .setLargeIcon(bitmap) .setAutoCancel(true) .setContentIntent(pendingIntent) .build(); startForeground(NOTIFY_ID, notification); }
public void init() { if ( mediaPlayer == null ) { mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.she); mediaPlayer.setLooping(true); } } public void start() { mediaPlayer.seekTo(0); mediaPlayer.start(); } public void stop() { if ( mediaPlayer != null ) { mediaPlayer.stop(); mediaPlayer.release(); } } public MusicService2() { }
@Override public void onCreate() { super.onCreate(); sendNotity(); init(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { start(); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); stopForeground(true); stop(); } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } }
|
运行结果:
绑定服务实现
◼ 服务代理框架
◼ 服务的绑定和解除
◼ 服务端实现
◼ 客户端实现
服务代理框架
◼ 什么是代理:
◼ 代理是访问对象和目标对象之间的中介,当无法或不想直接引用目标对象或访问目标对象存在困难时,可以通过代理来间接访问。
◼ 代理模式优点:
◼ 在客户端与目标对象之间起到中介作用和保护目标对象的作用
◼ 代理对象可以扩展目标对象的功能
◼ 能将客户端与目标对象分离,在一定程度上降低了系统耦合度,增加程序扩展性
服务代理框架:假设服务类名为 AService
◼ 服务端:创建 Binder 代理来获取服务实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class AService extends Service { private final IBinder binder = new LocalBinder(); public class LocalBinder extends Binder { public AService getService() { return AService.this; } } @Override public IBinder onBind(Intent intent) { return binder; } }
|
注:IBinder 是接口,Binder 是接口实现类,LocalBinder 自定义类是继承 Binder 并扩展
服务的绑定和解除 (启动和结束绑定服务)
1 2 3 4
| Intent intent = new Intent(MainActivity.this, AService.class); intent.putExtra("data","hello"); bindService( intent, connection, BIND_AUTO_CREATE);
|
BIND_AUTO_CREATE:当 bindService 时,如果服务不存在则自动创建该服务,并执行 onCreate–>onBind,如果服务已存在(已绑定),则只会调用 onBind
1 2 3
| unbindService( connection ); mBound = false;
|
服务绑定图示
服务端实现:新建MusicService3服务(播放、暂停、销毁)
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
| public class MusicService3 extends Service { MediaPlayer mediaPlayer; private final IBinder binder = new LocalBinder(); public class LocalBinder extends Binder { public MusicService3 getService() { return MusicService3.this; } } public void init() { if (mediaPlayer == null) { mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.she); mediaPlayer.setLooping(true); mediaPlayer.seekTo(0); } }
public void start() { if ( !mediaPlayer.isPlaying() ) { mediaPlayer.start(); } } public void pause() { if ( mediaPlayer.isPlaying() ) { mediaPlayer.pause(); } } public void stop() { if ( mediaPlayer != null ) { mediaPlayer.stop(); mediaPlayer.release(); } }
@Override public void onCreate() { super.onCreate(); Log.d("flag", "onCreate"); init(); } @Override public IBinder onBind(Intent intent) { Log.d("flag", "onBind"); Log.d("flag", intent.getStringExtra("data")); return binder; } @Override public boolean onUnbind(Intent intent) { Log.d("flag", "onUnbind"); stop(); return super.onUnbind(intent); } @Override public void onDestroy() { super.onDestroy(); Log.d("flag", "onDestroy"); } }
|
客户端:新建MainActivity3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity3"> <ImageButton android:id="@+id/btn_start" android:layout_width="100dp" android:layout_height="60dp" android:layout_marginStart="8dp" android:layout_marginTop="44dp" android:layout_marginEnd="8dp" android:backgroundTint="@android:color/holo_orange_dark" android:scaleType="fitCenter" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@android:drawable/ic_media_play" /> </androidx.constraintlayout.widget.ConstraintLayout>
|
MainActivity3.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
| public class MainActivity3 extends AppCompatActivity { ImageButton btn_start; private MusicService3 musicService; private boolean mBound = false; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { MusicService3.LocalBinder binder = (MusicService3.LocalBinder) iBinder; musicService = binder.getService(); mBound = true; } @Override public void onServiceDisconnected(ComponentName componentName) { mBound = false; } };
View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_start: boolean isPlaying = musicService.mediaPlayer.isPlaying(); if ( !isPlaying ) { musicService.start(); btn_start.setImageDrawable(getDrawable(android.R.drawable.ic_media_pause)); } else { musicService.pause(); btn_start.setImageDrawable(getDrawable(android.R.drawable.ic_media_play)); } break; } } };
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main3); Log.d("flag", "onCreate"); btn_start = (ImageButton) findViewById(R.id.btn_start); btn_start.setOnClickListener(listener); Intent intent = new Intent(MainActivity3.this, MusicService3.class); intent.putExtra("data", "hello"); bindService( intent, connection, BIND_AUTO_CREATE); } @Override protected void onDestroy() { super.onDestroy(); Log.d("myflag", "onDestroy"); if( mBound ) { unbindService(connection); mBound = false; } } }
|
运行结果:将 MainActivity3 设置为启动程序