前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android 进阶4:Service 的一些细节

Android 进阶4:Service 的一些细节

作者头像
张拭心 shixinzhang
发布2018-01-05 15:34:58
1.2K0
发布2018-01-05 15:34:58
举报
文章被收录于专栏:拭心的安卓进阶之路

Service 简介

Service 的概念相信大家都知道:后台运行服务,它可以在后台执行长时间运行操作而不提供用户界面。

由于 Service 在用户切换到其他应用时依然可以运行,它一般被用来进行后台播放音乐、网络请求、文件 I/O 或者其他服务。

正如我们看到的,很多时候 Service 所做的工作和我们为了避免 ANR 另开一个线程所做的任务很相似,那在做这些任务时该选择开启服务还是线程呢?

影响这个选择的关键是:这个任务是否在用户离开当前页面、应用后仍在执行?

  • 如果你希望这个异步任务在用户退出时就结束,那就可以考虑使用 AsyncTask 或者 HandlerThread 等线程工作类,在 onDestroy() 时关闭线程
  • 如果你希望用户退出后任务仍在进行,则选择 Service 或者 IntentService 等服务

注意:默认情况下,服务在其调用组件所在进程的主线程中运行,它既不创建自己的线程,也不在单独的进程中运行。

所以如果我们选择在 Service 中做耗时操作,也需要新开启一个线程执行,避免 ANR。

Service 的两种启动方式

Service 有两种状态,这两种状态对应着两种启动方式:

  1. 启动状态
    • 调用 startService() 方法启动
    • 启动状态下的 Service 将会在后台一直运行,即使主应用退出后依旧在运行 (放心我会努力活下去)
    • 直到自身通过调用 stopSelf() 结束工作,或者由另一个组件通过调用 stopService() 来停止
    • 这种状态下的 Service 一般只负责执行任务,不会直接将结果返回给调用方
    • 比如后台下载数据或者处理文件
  2. 绑定状态
    • 调用 bindService() 启动
    • 绑定状态下的服务可以和调用组件交互,比如发送请求、获取结果
    • 这种情况下就可能涉及到 IPC
    • 一个服务可以绑定多个组件,有绑定的组件才会运行,绑定的组件全部取消绑定后就销毁(同生共死)

①启动模式的代码:

代码语言:javascript
复制
Intent intent = new Intent(this, HelloService.class);
startService(intent);

startService() 的方式启动服务时,传递 intent 是组件与服务唯一的通信方式。如果还需要返回结果,有两种选择:

  1. 再调用 bindService() 绑定服务
  2. 为传递的 intent 中添加一个广播,服务端给广播发送结果

上述第二种方式代码如下:

代码语言:javascript
复制
//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);
   }

②绑定模式启动的代码:

代码语言:javascript
复制
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() 方式启动的服务,除非系统必须回收内存资源,否则不会停止。

为了节约资源,在完成任务后我们需要主动停止服务,停止服务有三个方法:

  1. stopService()
    • Context 的方法,外部组件调用,调用后系统会尽快销毁服务
  2. stopSelf()
    • Service 的方法,效果和 stopService() 一样
  3. stopSelf(int)
    • Service 的方法,它的特别之处在于参数和启动时的 id 一致才会被终止
    • 也就是说如果在终止前又收到新的调用,就不会停止

前台服务

1.管家的抉择 (Android进程生命周期)里我们已经知道:

仅当内存过低且必须回收系统资源以供具有用户焦点的 Activity 使用时,Android 系统才会强制停止服务。 如果将服务绑定到具有用户焦点的 Activity,则它不太可能会终止;如果将服务声明为在前台运行,则它几乎永远不会终止。

为了降低 Service 被回收的可能,有时候我们需要把服务声明为前台的,这样在内存不足时,系统也不会考虑将其终止,因为在系统看来它正在与用户进行交互。

Service 的 startForeground() 可以达到这个目的:

代码语言:javascript
复制
public final void startForeground(int id, Notification notification) {
    try {
        mActivityManager.setServiceForeground(
                new ComponentName(this, mClassName), mToken, id,
                notification, 0);
    } catch (RemoteException ex) {
    }
}

startForeground() 接受两个参数:

  • id:当前提示的表示,不能为 0
  • notification:要展示的提示

下面的代码演示了如何在 Service 中将自己提升为前台级别:

代码语言:javascript
复制
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()

Android 5.0 后需要显式启动 Service

官方文档:

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 启动服务的样例代码:

代码语言:javascript
复制
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 中弹出 Dialog

Service 中可以弹 ToastNotification 来提示用户,这也符合 Service 的特点,默默无闻地后台奉献,没有界面,提示也是比较轻量级的。

比如当某些后台工作(例如文件下载已经完成)且用户现在可以对其进行操作时,状态栏通知是最佳方法。 当用户从展开视图中选定通知时,通知即可启动 Activity(例如查看已下载的文件)。

之前看过一道面试题: Service 中可以弹 Dialog 吗?

官方文档是不可以的,毕竟在其他应用中弹出自己应用的对话框,有些不人性化,官方希望类似的场景采用Notification来解决。

但是一些场景下真有这种需求,于是就有了各种解决方法,这里贴出两种:

1.修改 Dialog 的类型为系统提示

dialog.getWindow().setType((WindowManager.LayoutParams.TYPE_SYSTEM_ALERT))

2.修改 Activity 的主题为 Dialog,然后启动一个 Activity

代码语言:javascript
复制
<style name="AppTheme.Dialg" parent="Theme.AppCompat.Light.Dialog">

</style>

Thanks

https://developer.android.com/guide/components/services.html

https://developer.android.com/guide/components/bound-services.html

https://stackoverflow.com/questions/6099364/how-to-use-pendingintent-to-communicate-from-a-service-to-a-client-activity

http://stackoverflow.com/questions/26530565/android-5-0-l-service-intent-must-be-explicit-in-google-analytics

http://aaronchansunny.github.io/2015/05/13/launch-a-dialog-in-android-service/

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-05-23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Service 简介
  • Service 的两种启动方式
    • 两种状态下服务的生命周期
    • 停止服务
    • 前台服务
    • Android 5.0 后需要显式启动 Service
    • Service 中弹出 Dialog
    • Thanks
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档