Service 的概念相信大家都知道:后台运行服务,它可以在后台执行长时间运行操作而不提供用户界面。
由于 Service 在用户切换到其他应用时依然可以运行,它一般被用来进行后台播放音乐、网络请求、文件 I/O 或者其他服务。
正如我们看到的,很多时候 Service 所做的工作和我们为了避免 ANR 另开一个线程所做的任务很相似,那在做这些任务时该选择开启服务还是线程呢?
影响这个选择的关键是:这个任务是否在用户离开当前页面、应用后仍在执行?
AsyncTask
或者 HandlerThread
等线程工作类,在 onDestroy()
时关闭线程Service
或者 IntentService
等服务注意:默认情况下,服务在其调用组件所在进程的主线程中运行,它既不创建自己的线程,也不在单独的进程中运行。
所以如果我们选择在 Service 中做耗时操作,也需要新开启一个线程执行,避免 ANR。
Service 有两种状态,这两种状态对应着两种启动方式:
startService()
方法启动stopSelf()
结束工作,或者由另一个组件通过调用 stopService()
来停止bindService()
启动①启动模式的代码:
Intent intent = new Intent(this, HelloService.class);
startService(intent);
startService()
的方式启动服务时,传递 intent 是组件与服务唯一的通信方式。如果还需要返回结果,有两种选择:
bindService()
绑定服务上述第二种方式代码如下:
//1. 在启动服务的组件中构建广播的 PendingIntent,以 bundle 的形式添加到 intent 中,然后启动服务
private void starServiceWithBroadcast(){
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(this, RepeatReceiver.class), 0);
Bundle bundle = new Bundle();
bundle.putParcelable("receiver", pendingIntent);
Intent intent = new Intent(getApplicationContext(), MyAidlService.class);
intent.putExtras(bundle);
startService(intent);
}
//2.在你的 Service 中拿出 intent 携带的 PendingIntent,在完成任务后将结果发送出去
public class DataRequestService extends Service {
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
Bundle bundle = msg.getData();
PendingIntent receiver = bundle.getParcelable("receiver");
try {
Intent intent = new Intent();
Bundle b = new Bundle();
intent.putExtras(b);
receiver.send(getApplicationContext(), status, intent);
} catch (CanceledException e) {
e.printStackTrace();
}
}
}
@Override
public void onStart(Intent intent, int startId) {
Bundle bundle = intent.getExtras();
msg.setData(bundle);
mServiceHandler.sendMessage(msg);
}
②绑定模式启动的代码:
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mAidl = IMyAidl.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mAidl = null;
}
};
Intent intent1 = new Intent(getApplicationContext(), MyAidlService.class);
bindService(intent1, mConnection, BIND_AUTO_CREATE);
Service 有时会被独自放置到另外一个进程,这时如果我们的应用想与 Service 进行交互,就需要调用 bindService()
方法,因为这样客户端就可以拿到 Service 的 onBind()
方法返回的接口,然后进行操作。
如官方图所示:
请注意:
onCreate()
只在创建时调用一次,一旦服务启动后,就不会再调用了onStartCommand()
必须返回整型数,它用于表示在服务停止时系统如何处理,有以下三个值: START_NOT_STICKY
: 服务终止时不会重建,比较安全START_STICKY
: 服务终止时重建并调用 onStartCommand()
,但传递的 intent 为空,适用于不需要参数的服务START_REDELIVER_INTENT
: 和START_STICKY
类似,但会将之前接收到的 intent 传递给重建服务的 onStartCommand()
方法,适用于必须立即恢复的紧急任务onBind()
返回一个 IBinder,客户端拿到后就可以和服务通信使用 startService()
方式启动的服务,除非系统必须回收内存资源,否则不会停止。
为了节约资源,在完成任务后我们需要主动停止服务,停止服务有三个方法:
stopService()
stopSelf()
stopService()
一样stopSelf(int)
在 1.管家的抉择 (Android进程生命周期)里我们已经知道:
仅当内存过低且必须回收系统资源以供具有用户焦点的 Activity 使用时,Android 系统才会强制停止服务。 如果将服务绑定到具有用户焦点的 Activity,则它不太可能会终止;如果将服务声明为在前台运行,则它几乎永远不会终止。
为了降低 Service 被回收的可能,有时候我们需要把服务声明为前台的,这样在内存不足时,系统也不会考虑将其终止,因为在系统看来它正在与用户进行交互。
Service 的 startForeground()
可以达到这个目的:
public final void startForeground(int id, Notification notification) {
try {
mActivityManager.setServiceForeground(
new ComponentName(this, mClassName), mToken, id,
notification, 0);
} catch (RemoteException ex) {
}
}
startForeground()
接受两个参数:
下面的代码演示了如何在 Service 中将自己提升为前台级别:
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, ServiceTestActivity.class), 0);
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.icon_ez)
.setTicker(text)
.setWhen(System.currentTimeMillis())
.setContentTitle(getText(R.string.local_service_label))
.setContentText(text)
.setContentIntent(contentIntent)
.build();
startForeground(NOTIFICATION, notification);
要从前台移除服务,请调用 stopForeground()
。
官方文档:
Caution: To ensure your app is secure, always use an explicit intent when starting a Service and do not declare intent filters for your services. Using an implicit intent to start a service is a security hazard because you cannot be certain what service will respond to the intent, and the user cannot see which service starts. Beginning with Android 5.0 (API level 21), the system throws an exception if you call bindService() with an implicit intent. Note: When starting a Service, you should always specify the component name. Otherwise, you cannot be certain what service will respond to the intent, and the user cannot see which service starts.
在 5.0 以后为了确保应用的安全性,系统强制要求使用显式 Intent (不了解的朋友可以看这篇文章)启动或绑定 Service,否则运行时会报错:
java.lang.IllegalArgumentException: Service Intent must be explicit.
同时建议不要为服务声明 Intent 过滤器, 因为它会导致启动哪个服务的不确定性。
除此外还可以为 Service 添加 android:exported
属性并将其设置为 “false”,确保服务仅适用于你的应用。这可以有效阻止其他应用启动您的服务。
使用显式 Intent 启动服务的样例代码:
Intent intent = new Intent();
intent.setComponent(new ComponentName("top.shixinzhang.myapplication", "top.shixinzhang.myapplication.service.MyAidlService"));
bindService(intent, mConnection, BIND_AUTO_CREATE);
Intent intent1 = new Intent(getApplicationContext(), MyAidlService.class);
bindService(intent1, mConnection, BIND_AUTO_CREATE);
Service 中可以弹 Toast
和 Notification
来提示用户,这也符合 Service 的特点,默默无闻地后台奉献,没有界面,提示也是比较轻量级的。
比如当某些后台工作(例如文件下载已经完成)且用户现在可以对其进行操作时,状态栏通知是最佳方法。 当用户从展开视图中选定通知时,通知即可启动 Activity(例如查看已下载的文件)。
之前看过一道面试题: Service 中可以弹 Dialog
吗?
官方文档是不可以的,毕竟在其他应用中弹出自己应用的对话框,有些不人性化,官方希望类似的场景采用Notification来解决。
但是一些场景下真有这种需求,于是就有了各种解决方法,这里贴出两种:
1.修改 Dialog 的类型为系统提示
dialog.getWindow().setType((WindowManager.LayoutParams.TYPE_SYSTEM_ALERT))
2.修改 Activity 的主题为 Dialog,然后启动一个 Activity
<style name="AppTheme.Dialg" parent="Theme.AppCompat.Light.Dialog">
</style>
https://developer.android.com/guide/components/services.html
https://developer.android.com/guide/components/bound-services.html
http://aaronchansunny.github.io/2015/05/13/launch-a-dialog-in-android-service/