绑定Service
绑定 Service 是激活 Service 的一种方式,称之为“绑定模式”。而上一篇文章中说到的startService
是启动 Service,是激活 Service 的一种方式,称之为“启动模式”。
绑定模式激活的 Service 组件,可以用于实现组件间通信。
组件:Activity、Service、BroadcastReceiver、ContentProvider
控件:TextView、ImageView 等可操控的view
观察 WorkService 中的代码,你会发现一直有一个onBind()
方法我们都没有使用到,这个方法其实就是用于和 Activity 建立关联的,修改 WorkService 中的代码
public class WorkService extends Service {
public WorkService() {
super.onCreate();
Log.d("Service", "WorkService@" + hashCode() + ".onCreate()");
}
@Override
public IBinder onBind(Intent intent) {
InnerIBinder binder = new InnerIBinder();
Log.d("Service", "WorkService@" + hashCode() + ".onBind()");
return binder;
}
private class InnerIBinder extends Binder {
}
}
要想绑定成功,在 WorkService 中的onBind()
方法需要返回一个非 null 的值。其中 IBinder 是接口(从名字上也很容易看出来 Interface),所以我们写一个内部 InnerIBinder 来实现这个接口。
InnerIBinder 类继承自 Binder 类
在 activity_main.xml 中增加一个 Button 来用来绑定 Service
<Button
android:id="@+id/btn_bind_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="绑定Service" />
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btnBindService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnBindService = findViewById(R.id.btn_bind_service);
btnBindService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
//绑定Service
Intent service = new Intent(this, WorkService.class);
ServiceConnection conn = new InnerServiceConnection();
int flags = BIND_AUTO_CREATE;//该取值表示:绑定Service时自动创建Service对象
bindService(service, conn, flags);
Log.d("Service", "MainActivity.onClick()->使用bindService激活了Service组件");
}
private class InnerServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("Service", "MainActivity.onServiceConnected()->当Activity和Service连接好后执行这个方法");
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
}
运行程序,点击 绑定按钮,Logcat 输出:
【开发步骤总结】
1、创建 WorkService
类,继承自 android.app.Service
2、AndroidManifest.xml
注册 WorkService
3、WorkService
创建 InnerBinder
,继承自 android.os.Binder
,同时在 onBind()
方法中创建 InnerBinder
对象,并做为方法返回值
4、MainActivity
中创建内部类InnerServiceConnection
,实现 android.content.ServiceConnection
接口
5、当需要绑定 Service 时,调用 bindService()
方法。第1个参数为 Intent
对象,用于指定被激活的 Service 组件;第2个参数是ServiceConnection
对象,用我们刚才写的InnerServiceConnection
即可;第3个参数使用常量 BIND_AUTO_CREATE
。(Tips:第3个参数是一个标志位,这里传入BIND_AUTO_CREATE
表示在 Activity 和 Service 建立关联后自动创建 Service,这会使得 WorkService 中的onCreate()
方法得到执行,但onStartCommand()
方法不会执行)
解除绑定Service
当与 Service 实现绑定关系的 Activity 退出时,应该在退出之前通过unbindService(ServiceConnection conn)
方法解除绑定。可以在 MainActivity 中的onDestroy
方法中解除绑定,其中参数 ServiceConnection conn
是之前绑定时的 conn。
@Override
protected void onDestroy() {
//解除与Service的绑定
unbindService(conn);
super.onDestroy();
}
一旦解除绑定 Service 就会被销毁,可以在 WorkService 中重写onDestroy
方法,打印日志观察下
@Override
public void onDestroy() {
super.onDestroy();
Log.d("Service","WorkService@"+hashCode()+".onDestroy()");
}
当我们按下模拟器的返回键时,程序退出,Logcat 打印日志如下:
Activity调用Service中方法
修改 WorkService onBind()
方法中的打印信息,增加打印 binder 的信息
@Override
public IBinder onBind(Intent intent) {
InnerIBinder binder = new InnerIBinder();
Log.d("Service", "WorkService@" + hashCode() + ".onBind(),binder->" + binder.hashCode());
return binder;
}
修改 MainActivity 中 InnerServiceConnection 类中方法 onServiceConnected
增加打印参数 service
private class InnerServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("Service", "MainActivity.onServiceConnected(),service->" + service.hashCode());
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
重新运行程序,点击按钮绑定 Service,Log 如下
可以发现 binder 和 service 的 hashCode 是相同的。也就说明 MainActivity 拿到了 WorkService 的 InnerIBinde r的 binder 对象,所以,如果在 InnerIBinder 中写一个方法,MainActivity中也是可以调用的。
修改 activity_main.xml 中的布局,增加一个按钮 来调用 InnerBinder 的方法
<Button
android:id="@+id/btn_call_getname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="调用InnerBinder的getName方法" />
修改 WorkService 中的代码,在 InnerIBinder 中增加 getName()
方法
public class WorkService extends Service {
......
public class InnerIBinder extends Binder {
public String getName() {
return "WorkService 的 getName 方法";
}
}
......
}
修改 MainActivity 中代码
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
......
private Button btnCallGetName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
......
btnCallGetName = findViewById(R.id.btn_call_getname);
btnCallGetName.setOnClickListener(this);
}
ServiceConnection conn;
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
//绑定Service
......
break;
case R.id.btn_call_getname:
String name = binder.getName();
Log.d("Service", "MainActivity.onClick()->调用了getName方法得到了name:" + name);
break;
}
}
private WorkService.InnerIBinder binder;
private class InnerServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
binder = (WorkService.InnerIBinder) service;
Log.d("Service", "MainActivity.onServiceConnected(),service->" + service.hashCode());
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
@Override
protected void onDestroy() {
......
}
}
运行程序,点击 绑定 Service 按钮
点击调用 getName 方法的按钮
绑定模式的Service周期
绑定模式的Service周期
onCreate()
第一次绑定 Service 时被调用onBind()
第一次绑定 Service 时被调用onDestroy()
当 Service 被解除绑定时调用
绑定Service时通常不会被执行的方法
service 中的onRebind()
是重新绑定时调用的方法,当组件与 service 断开连接时, service 就会被销毁,当再次绑定时,会重新创建新的 service,不属于重新绑定。仅当解除绑定时,service 没有被销毁再次绑定时,才属于“再次绑定”,这种情况只会发生在先使用启动模式激活 Service,再使用绑定模式激活 Service,解除绑定,然后再次绑定才会发生(我不知道我自己在说什么)
ServiceConnection 中的onServiceDisconnected()
是当组件与 Service 断开连接时被调用的方法,该方法只会在意外被断开时被调用,正常解除绑定时,不会回调该方法。
优化
Activity 调用 Service中方法的代码功能可以实现,但是设计思路上写的不够好。
其中InnerIBinder
是个内部类,内部类的定位是在当前这个外部类的内部来使用的,也就是在 WorkService 中来使用的。但是我们对于 InnerIBinder 这个类的使用已经超出了 WorkService ,因为我们在 MainActivity 中也使用了。但是如果 InnerIBinder 不是一个内部类,它就很可能很难调用 WorkService 中的一些方法,例如:
WorkService 中有一个 play()
方法,和其他一些全局变量,在 InnerIBinder 类中可以写一个 callPlay()
方法调用 play()
方法
public void callPlay(){
play();
}
写成内部类可以很方便的调用一些成员和方法,内部类就是为当前外部类服务,其他类不应该去用。这种情况下,我们可以用到接口
public interface IMusicPlayer {
void callPlay();
String getName();
}
在写 InnerIBinder 时实现这个接口
public class InnerIBinder extends Binder implements IMusicPlayer {
@Override
public void callPlay() {
}
@Override
public String getName() {
return "WorkServic的getName方法";
}
}
MainActivity 中用的时候,改为:
private IMusicPlayer binder;
private class InnerServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
binder = (IMusicPlayer) service;
Log.d("Service", "MainActivity.onServiceConnected(),service->" + service.hashCode());
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
Service和Thread的关系
在 MainActivity 的onCreate()
方法里加入一行打印当前线程 id 的语句:
Log.d("Service", "MainActivity thread id is " + Thread.currentThread().getId());
然后在 WorkService 的onCreate()
方法里也加入一行打印当前线程 id 的语句:
@Override
public void onCreate() {
super.onCreate();
Log.d("Service", "WorkService thread id is " + Thread.currentThread().getId());
}
页面添加一个启动 Service 的按钮,点击按钮执行启动 Service 代码
startService(new Intent(this,WorkService.class));
查看 Logcat 输出内容:
可以看到它们的线程 id 完全是一样的,由此证实了 Service 是运行在主线程里的,也就是说如果你在 Service 里编写了非常耗时的代码,程序必定会出现 ANR。应该在 Service 中开启线程去执行耗时任务,这样就可以有效地避免 ANR 的出现。
其实大家不要把后台和子线程联系在一起就行了,这是两个完全不同的概念。Android 的后台就是指,它的运行是完全不依赖 UI 的。即使 Activity 被销毁,或者程序被关闭,只要进程还在,Service 就可以继续运行。比如说一些应用程序,始终需要与服务器之间始终保持着心跳连接,就可以使用 Service 来实现。
你可能又会问,前面不是刚刚验证过 Service 是运行在主线程里的么?在这里一直执行着心跳连接,难道就不会阻塞主线程的运行吗?当然会,但是我们可以在 Service 中再创建一个子线程,然后在这里去处理耗时逻辑就没问题了。
既然在 Service 里也要创建一个子线程,那为什么不直接在 Activity 里创建呢?这是因为 Activity 很难对 Thread 进行控制,当 Activity 被销毁之后,就没有任何其它的办法可以再重新获取到之前创建的子线程的实例。而且在一个 Activity 中创建的子线程,另一个 Activity 无法对其进行操作。
但是 Service 就不同了,所有的 Activity 都可以与 Service 进行关联,然后可以很方便地操作其中的方法,即使 Activity 被销毁了,之后只要重新与 Service 建立关联,就又能够获取到原有的 Service 中 Binder 的实例。因此,使用 Service 来处理后台任务,Activity 就可以放心地 finish,完全不需要担心无法对后台任务进行控制的情况。
一个比较标准的Service就可以写成:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
// 开始执行后台任务
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
class MyBinder extends Binder {
public void startDownload() {
new Thread(new Runnable() {
@Override
public void run() {
// 执行具体的下载任务
}
}).start();
}
}
创建前台Service
Service 几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是 Service 的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的 Service。
如果你希望 Service 可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台 Service。
前台 Service 和普通 Service 最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。
当然有时候你也可能不仅仅是为了防止 Service 被回收才使用前台 Service,有些项目由于特殊的需求会要求必须使用前台 Service,比如说墨迹天气,它的 Service 在后台更新天气数据的同时,还会在系统状态栏一直显示当前天气的信息。
如何才能创建一个前台 Service?修改 WorkService 中的代码
@Override
public void onCreate() {
super.onCreate();
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = new Notification.Builder(this);
builder.setContentInfo("Content info")
.setContentText("Content text")//设置通知内容
.setContentTitle("Content title")//设置通知标题
.setContentIntent(pendingIntent)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setSmallIcon(R.mipmap.ic_launcher_round)//不能缺少的一个属性
.setSubText("Subtext")
.setTicker("滚动消息......")
.setWhen(System.currentTimeMillis());//设置通知时间,默认为系统发出通知的时间,通常不用设置
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("001", "my_channel", NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(true); //是否在桌面icon右上角展示小红点
channel.setLightColor(Color.GREEN); //小红点颜色
channel.setShowBadge(true); //是否在久按桌面图标时显示此渠道的通知
manager.createNotificationChannel(channel);
builder.setChannelId("001");
}
Notification n = builder.build();
startForeground(1, n);
Log.d("Service", "onCreate() executed");
}
重新运行一下程序,并点击 启动 Service 或 绑定 Service 按钮,WorkService 就会以前台 Service 的模式启动了,并且在系统状态栏会弹出一个通栏图标,下拉状态栏后可以看到通知的详细内容
参考:
Android Service完全解析,关于服务你所需知道的一切