有时候需要给Android应用添加背景音乐的功能,例如一些小游戏之类的应用。在应用处于前台可见时,需要播放背景音乐,当应用处于后台不可见时(如按了home键或进入其它应用或该应用被销毁时)背景音乐也要随之暂停或停止。
Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。
可以通过扩展Service或IntentService来实现服务功能。背景音乐需要在整个应用期间持续播放,停止服务的控制权要交给应用组件,而由于IntentService会在完成任务后自行调用stopSelf()来停止服务,所以不适合此处。这里需要扩展Service来实现功能。
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* BackgroundMusic
* 后台背景音乐服务(常应用于小游戏),要求
* 1.应用不可见时(未销毁),播放暂停,服务保持
* 2.应用恢复可见时,播放继续(不是重新开始)
* 3.应用退出或被销毁时,服务停止
*/
public class BgmService extends Service {
public static final String APP_TAG = "SUDOKU_TAG";
public static final String ACTION_MUSIC_PLAY= "com.jack..action.ACTION_MUSIC_PLAY";
public static final String ACTION_MUSIC_PAUSE= "com.jack..action.ACTION_MUSIC_PAUSE";
private MediaPlayer mediaPlayer;
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private final class ServiceHandler extends Handler{
private ServiceHandler(Looper looper){
super(looper);
}
//播放 暂停
@Override
public void handleMessage(@NonNull Message msg) {
Intent intent = (Intent) msg.obj;
if (ACTION_MUSIC_PLAY.equals(intent.getAction())) {
if(mediaPlayer==null) {
mediaPlayer = MediaPlayer.create(BgmService.this,R.raw.bgm72);//create包含prepare()
mediaPlayer.setLooping(true);
}
mediaPlayer.start();//播放或恢复播放
}else if (ACTION_MUSIC_PAUSE.equals(intent.getAction())) {
if(mediaPlayer!=null) {
mediaPlayer.pause();
}
}
//mediaPlayer.setOnPreparedListener(BgmService.this);
//mediaPlayer.prepareAsync(); // prepare async to not block main thread
}
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("BgmServiceHandlerThread");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v(APP_TAG," BgmService "+intent.getAction());
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
Log.v(APP_TAG," BgmService onDestroy");
super.onDestroy();
mServiceLooper.quit();
//mediaPlayer非常消耗资源,务必销毁之
if (mediaPlayer != null) mediaPlayer.release();
}
@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}
}
播放媒体文件是个耗时的过程,必须要开启另外的线程来完成播放任务。此次是播放本地的媒体资源,如果是网络资源,需要实现MediaPlayer.OnPreparedListener接口,当资源准备就绪后才mediaPlayer.start()。
mediaPlayer.prepare();是个阻塞的方法。
背景音乐服务需要在应用销毁时停止,如果不做处理,服务会一直存在直到被android系统杀掉。当然也可以通过一个开关配置项来控制背景音乐是否播放。
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, BgmService.class);
intent.setAction(BgmService.ACTION_MUSIC_PLAY);
startService(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
stopService(new Intent(this, BgmService.class));
}
...
}
MainActivity销毁就表示应用销毁了,这样就保证了应用销毁时背景音乐关闭。
那还有个难题,如何控制暂停呢?当应用不可见处于后台时(如按下Home键或进入其它应用),怎样让背景音乐暂停呢,然后应用重新回到前台时背景音乐恢复播放呢?
可能第一想到的是在Activity的onStop()方法中暂停服务。如下:
@Override
protected void onStop() {
super.onStop();
Intent intent = new Intent(this, BgmService.class);
intent.setAction(BgmService.ACTION_MUSIC_PAUSE);
startService(intent);
}
显然这是可行的,当它只能保证该Activity不可见(stop状态)时可以暂停播放,但是一个应用包含非常多的Activity,你需要在每个Activity中的onStop()方法都加上上面的代码,甚至onStart()方法也要加上开启服务的代码。显然这样是不可行的方法。因为背景音乐服务是和应用状态相关的,所以这个控制开启或暂停的逻辑应该和应用有关,而不是和应用的某个Activity有关。所以这里用Application.ActivityLifecycleCallbacks来实现才是最佳实现方式。
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* 应用前后台状态监听帮助类,仅在Application中使用
*/
public class AppFrontBackHelper {
private OnAppStatusListener mOnAppStatusListener;
public AppFrontBackHelper() {
}
/**
* 注册状态监听,仅在Application中使用
*/
public void register(Application application, OnAppStatusListener listener){
mOnAppStatusListener = listener;
application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks);
}
public void unRegister(Application application){
application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
}
private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
//打开的Activity数量统计
private int activityStartCount = 0;
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
activityStartCount++;
//数值从0变到1说明是从后台切到前台
if (activityStartCount == 1){
//从后台切到前台
if(mOnAppStatusListener != null){
mOnAppStatusListener.onFront();
}
}
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
activityStartCount--;
//数值从1到0说明是从前台切到后台
if (activityStartCount == 0){
//从前台切到后台
if(mOnAppStatusListener != null){
mOnAppStatusListener.onBack();
}
}
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
};
public interface OnAppStatusListener{
void onFront();
void onBack();
}
}
通过给应用注册ActivityLifecycleCallbacks 监听,对每个Acitivity的周期回调的监控,来达到目的。这里只要监听onActivityStarted和onActivityStopped两个方法。
import android.app.Application;
import androidx.preference.PreferenceManager;
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
AppFrontBackHelper helper = new AppFrontBackHelper();
helper.register(MyApp.this, new AppFrontBackHelper.OnAppStatusListener() {
@Override
public void onFront() {
//应用切到前台处理
Intent intent = new Intent(MyApp.this, BgmService.class);
intent.setAction(action);
MyApp.this.startService(intent);
}
@Override
public void onBack() {
//应用切到后台处理
MyApp.this.stopService(new Intent(MyApp.this, BgmService.class));
}
});
}
}
在清单中申明MyApp
最后,在MainActivity中只需要onDestroy的销毁服务,onStart和onStop不需要了。
至此已经实现了背景音乐随应用状态改变而播放和暂停了。通过完成此功能,你会对
Service和Activity的生命周期有更深层的了解。
《道德经》第十一章:
三十辐共一毂,当其无,有车之用。埏埴以为器,当其无,有器之用。凿户牖以为室,当其无,有室之用。故有之以为利,无之以为用。
译文:三十根辐条汇集到一根毂中的孔洞当中,有了车毂中空的地方,才有车的作用。揉和陶土做成器皿,有了器具中空的地方,才有器皿的作用。开凿门窗建造房屋,有了门窗四壁内的空虚部分,才有房屋的作用。所以,“有”给人便利,“无”发挥了它的作用。