背景
先从日常用户体验说起,用过苹果的iOS系统都知道,凡是音频播放,在下滑菜单都能看到是哪个应用在播放,音频的标题,用户还可以直接在下滑菜单操作,而安卓手机则不然,因为Android系统使用本文介绍的Service进行后台音乐播放,而Service不提供和用户交互接口,因此在安卓手机上,当用户打开音乐程序并后台播放音乐,想关闭音乐只有再打开该播放音乐程序或者直接杀死所有程序才能关闭音乐,如果用户手机上有多个音乐程序(国内用户大概率会这样),有时候可能会忘记打开的是哪个音乐播放程序,勤快点的一个一个打开播放程序,哦,对了,顺便得看一遍开屏广告,懒一点的直接杀死所有程序,不过,有时候直接杀死所有程序方式也没用。
导致这些不好的用户体验,多是因为使用Service不当所致,为了更好的使用Service,还是得对其进去了解。
1.什么是Service
官方对Service定义如下:
服务是可以在后台执行长时间运行的操作的应用组件,并且不提供用户界面。另一个应用程序组件可以启动服务,并且即使用户切换到另一个应用程序时,Service也可以在后台继续运行。此外,组件可以绑定到服务以与其进行交互,甚至可以执行进程间通信(IPC)。例如,一项服务可以从后台处理网络事务,播放音乐,执行文件I/O或与内容提供者进行交互。
从官方的定义,我们可以总结出Service的特性
- Service是没有UI的Android组件,用户是无法直接和其进行交互;
- Service用于在后台执行长时间运行的操作。除非明确停止或销毁服务,否则它们将无限期运行,直到系统资源短缺而被Android终止;
- Service可以由任何其他应用程序组件启动。组件甚至可以实际上绑定到服务以执行进程间通信;
官方文档还提到Service不是什么:
- Service不是独立的进程。除非另有说明,Service并没有自己独立的进程,而是和所属的应用程序在同一进程中运行。Service是以一个独立程序的形式安装,这样容易让人误认为Service在运行的时候也是一个独立的程序,因此有自己的进程,实际上不是的,Service是在调用它的程序的进程里;
- Service也不是线程,意味着启动一个Service并没开启另外一个线程。
2.什么时候使用Service
Service的特性是在后台运行,因此去执行需要长期运行的任务都可采用Service,这样任务放在后台运行,用户可以去做其他操作,例如开始提到的音乐播放,还有下载文件,或者提供外置设备的接口,例如外置打印机调用接口等等。
3.如何使用Service
3.1 Service生命周期
不同于Activity,Service具有两种生命周期形式,分别成为Started状态和Bound状态,两种状态生命周期见图1,他们的区别如下;
-
Started
其他组件通过startService启动Service,一旦启动后,Service可以无限地运行下去,除非Service自身调用stopSelf()方法或者其他组件调用stopService()方法来停止它,当Service被停止时,系统会销毁它,这种状态下,Service和调用它的组件相对独立,一般情况下,两者之间没有太多的交互,形同陌路。Android手机常发生的程序关闭了音乐还在播放的窘态正是Service进入这个状态所导致的。
-
Bound
其他组件调用bindService来创建,与Started状态不同的是,在这个状态下,组件可以通过IBinder接口和Service进行通信,组件想解绑,则通过unbindService()方法来关闭连接,一个Service可以同时和多个客户绑定,当多个组件都解除绑定之后或者组件销毁后,系统会销毁。和Started形同陌路的状态相比,Bound状态下Service和调用它的组件不仅感情亲密,而且进退共存亡,播放音频的程序如果bindService()调用Service,则不会出现程序已经销毁,音频还在播放的情况。
问题1.什么时候使用startService启动Service比较合适, 什么时候使用bindService绑定Service比较合适?
- 如果只是要启动Service长时间在后台执行一个任务,期间不需要和它交互,则使用startService比较合适;
- 如果启动service之后要与它进行交互则使用bindService比较合适;
问题2.想让Service长期在后台执行,期间又想和其进行交互怎么办?
这个需求在日常使用中经常碰到,例如在后台播放音乐时,想获取当前播放音乐的信息。这个时候,可以两种开启Service方式混着用,先startService,然后再bindService。在这种情况下,要终止Service,必须调用了stopSelf或者某个组件调用了stopService,而且所有绑定到该Service的组件都已解绑或者被销毁,理解起来并不复杂,即有怎么样的开始,就得怎么样结束,即以startService开始,必须使用stopSelf或者stopService结束,使用bindService开始,必须使用unbindService结束。
3.2 代码示例
3.2.1 startService方式
1.继承Service定义一个MyService类,并重写方法父类的方法
package com.pm.servicedemo;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class MyService extends Service {
public static final String TAG = "MyService";
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "执行onCreate()");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "执行onStartCommand()");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "执行onDestroy()");
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
2.在AndroidManifest.xml中注册Service,否则无法使用Service
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pm.servicedemo">
<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/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
//在这里注册Service服务
<service android:name=".MyService">
</service>
</application>
</manifest>
3.修改activity_main.xml布局文件,在其中添加两个按钮,分别用于开启Service和停止Service
<?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">
<TextView
android:id="@+id/tvLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnStartService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="104dp"
android:layout_marginTop="36dp"
android:text="开启Service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:layout_marginEnd="79dp"
android:text="停止Service"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
4.在MainActivity添加开启和停止Service逻辑,具体来说,使用Intent对象来开启和停止Service
package com.pm.servicedemo;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
Button bStart;
Button bStop;
TextView tvShow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvShow=(TextView)findViewById(R.id.tvLog);
bStart = (Button) findViewById(R.id.btnStartService);
bStop = (Button) findViewById(R.id.btnStop);
bStart.setOnClickListener(this);
bStop.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnStartService:
tvShow.setText("启动服务");
//构建Intent对象,并调用startService()启动Service
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent);
break;
case R.id.btnStop:
tvShow.setText("停止服务");
//构建Intent对象,并调用stopService()停止Service
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent);
break;
default:
break;
}
}
}
完成以上步骤后,这样就可以开始运行了,界面效果如下:
1.点击开启时,logcat打印的log如下:
com.pm.servicedemo D/MyService: 执行onCreate()
com.pm.servicedemo D/MyService: 执行onStartCommand()
此时Service已经创建,如果此时再次点击开启,则只执行onStartCommand,不会再执行onCreate(),logcat只增加一行打印信息:
com.pm.servicedemo D/MyService: 执行onStartCommand()
2.点击停止时,logcat打印的log如下:
com.pm.servicedemo D/MyService: 执行onDestroy()
以上是startService创建Service方式,接下来看bindService方式。
3.2.2 bindService方式
1.在已创建MyService基础上,新建一个子类继承自Binder类、重写父类的onBind和onUnbind,以提供给Activity绑定该服务,并实现一个虚拟的播放音乐函数。
package com.pm.servicedemo;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class MyService extends Service {
public static final String TAG = "MyService";
private MyBinder mBinder = new MyBinder();
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "执行onCreate()");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "执行onStartCommand()");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "执行onDestroy()");
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "执行onBind()");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
Log.d(TAG, "执行onUnbind()");
return super.onUnbind(intent);
}
//新建一个子类继承自Binder类
class MyBinder extends Binder {
public void playMusic() {
Log.d(TAG,"播放音乐");
}
}
}
2.修改activity_main.xml布局文件,添加绑定Service和解绑Service按钮,用于给用户操作绑定和解绑操作,布局内容如下:
<?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">
<TextView
android:id="@+id/tvLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnStartService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="104dp"
android:layout_marginTop="36dp"
android:text="开启Service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:layout_marginEnd="56dp"
android:text="停止Service"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnBindService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="绑定Service"
app:layout_constraintBottom_toBottomOf="@+id/btnUnbindService"
app:layout_constraintStart_toStartOf="@+id/btnStartService" />
<Button
android:id="@+id/btnUnbindService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginTop="108dp"
android:layout_marginEnd="56dp"
android:text="解绑Service"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/btnStartService"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.修改MainActivity,实现绑定Service和解绑Service逻辑,代码如下:
package com.pm.servicedemo;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
Button bStart;
Button bStop;
Button bBind;
Button bUnbind;
TextView tvShow;
private MyService.MyBinder myBinder;
//创建ServiceConnection的匿名类,该连接用于本Activity和Service沟通
private ServiceConnection connection = new ServiceConnection() {
//重写onServiceConnected()方法和onServiceDisconnected()方法
@Override
public void onServiceDisconnected(ComponentName name) {
}
//在Activity与Service解除关联的时候调用
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//实例化Service的内部类myBinder
myBinder = (MyService.MyBinder) service;
//在Activity调用Service类的方法
myBinder.playMusic();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvShow=(TextView)findViewById(R.id.tvLog);
bStart = (Button) findViewById(R.id.btnStartService);
bStop = (Button) findViewById(R.id.btnStop);
bBind = (Button) findViewById(R.id.btnBindService);
bUnbind = (Button) findViewById(R.id.btnUnbindService);
bStart.setOnClickListener(this);
bStop.setOnClickListener(this);
bBind.setOnClickListener(this);
bUnbind.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnStartService:
tvShow.setText("启动服务");
//构建Intent对象,并调用startService()启动Service
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent);
break;
case R.id.btnStop:
tvShow.setText("停止服务");
//构建Intent对象,并调用stopService()停止Service
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent);
break;
case R.id.btnBindService:
tvShow.setText("绑定服务");
//仍然需要构建Intent对象
Intent bindIntent = new Intent(this, MyService.class);
//参数说明
//第一个参数:Intent对象
//第二个参数:ServiceConnection实例
//第三个参数:标志位
//这里传入BIND_AUTO_CREATE表示在Activity和Service建立关联后自动创建Service
//这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行
bindService(bindIntent, connection, BIND_AUTO_CREATE);
break;
case R.id.btnUnbindService:
tvShow.setText("解绑服务");
//不需要构建Intent对象
//调用unbindService()解绑服务
//参数为ServiceConnection实例
unbindService(connection);
break;
default:
break;
}
}
}
从代码可以看出,首先需要创建一个ServiceConnection匿名类,从名字可以知道,这是一个Activity和Service的连接,类似一个通讯机制,类中的onServiceConnected()方法和onServiceDisconnected()方法分别用于绑定和解绑,而绑定后Activity指挥Service操作则通过MyBinder里的方法。
绑定中,除了连接类,仍然是构建出了一个Intent对象,然后调用bindService()方法将Activity和Service进行绑定。这和上面的startService一样,bindService方式比startService方式更紧密正是因为多了一个ServiceConnection。
我们注意到还有第三个参数BIND_AUTO_CREATE,该参数是一个标志位,BIND_AUTO_CREATE表示在Activity和Service建立关联后自动创建Service,这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行。
到这里代码准备好了,现在来测试一下:
1.点击绑定Service按钮,logcat打印如下信息:
com.pm.servicedemo D/MyService: 执行onCreate()
com.pm.servicedemo D/MyService: 执行onBind()
com.pm.servicedemo D/MyService: 播放音乐
2.此时再点击一遍绑定Service按钮则不会新增信息打印出来,因为已经绑定了服务;
3.点击解绑Service打印信息如下,表示执行了解绑并销毁Service:
com.pm.servicedemo D/MyService: 执行onUnbind()
com.pm.servicedemo D/MyService: 执行onDestroy()
4.如果先点击开启Service,再点击绑定Service,根据上面的讨论,应该是先执行startService生命周期,然后在进入bindService生命周期,logcat打印信息如下,说明我们的理解是对的
com.pm.servicedemo D/MyService: 执行onCreate()
com.pm.servicedemo D/MyService: 执行onStartCommand()
com.pm.servicedemo D/MyService: 执行onBind()
com.pm.servicedemo D/MyService: 播放音乐
如果想销毁Service,还是前面讨论的,怎样开始,就得怎样结束,如果直接点停止Service按钮,logcat不会打印销毁服务的信息,证明销毁不成功,需先点解绑Service按钮(也只显示onUnbind(),没有onDestory()),再点停止Service按钮打印onDestory(),说明此时才正常销毁Service
com.pm.servicedemo D/MyService: 执行onUnbind()
com.pm.servicedemo D/MyService: 执行onDestroy()