Android 进阶4:Service 的一些细节

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
    • 一个服务可以绑定多个组件,有绑定的组件才会运行,绑定的组件全部取消绑定后就销毁(同生共死)

①启动模式的代码:

Intent intent = new Intent(this, HelloService.class);
startService(intent);

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

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

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

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

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

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

前台服务

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() 接受两个参数:

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

下面的代码演示了如何在 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()

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

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

<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/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏散尽浮华

测试网站页面网速的一个简单Python脚本

无聊之余,下面分享一个Python小脚本:测试网站页面访问速度 [root@huanqiu ~]# vim pywww.py #!/usr/bin/python...

20510
来自专栏PHP技术

PHP非阻塞实现方法

如果 PHP 与 Web 服务器使用了 PHP-FPM(FastCGI 进程管理器),那通过 fastcgi_finish_request() 函数能马上结束会...

992
来自专栏一个会写诗的程序员的博客

RESTFeel: 一个企业级的API管理&测试平台。RESTFeel帮助你设计、开发、测试您的APIRESTFeel功能简介:MongoDB configuration:Building From

The build file is configured to download and use an embedded Tomcat server. So t...

894
来自专栏流柯技术学院

Jmeter对基于websocket协议的压力测试

WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。

1353
来自专栏菩提树下的杨过

JMS + jboss EAP 6.2 示例

.Net中如果需要消息队列功能,可以很方便的使用微软自带的MSMQ,对应到Java中,这个功能就是JMS(Java Message Service). 下面以J...

2047
来自专栏包子铺里聊IT

刷题外传之如何优雅的杀掉进程

进程(Process)是 Unix/Linux 系统下编程的核心知识。无论是小 Script 还是大 Daemon,启动后都是以进程的形势在 OS 中存在和执行...

3486
来自专栏Titan框架

在Jetty中使用websocket

在工作中,我们有时候需要使用能与前端实时通信传输以通信,这种技术就是由Socket实现的,而Socket又有短连接和长连接之分,长连接技术就是我们今天要介绍的w...

560
来自专栏技术小黑屋

一些关于加速Gradle构建的个人经验

目前绝大多数的Android项目都是基于Grale了,因为Gradle确实给我们带来了很多便利,然而,在使用了Gradle后,最大的不满就是编译起来太慢了。解决...

591
来自专栏Java Edge

Tomcat架构解析之2 connector BIOHTTP11ProtocolMapperCoyoteAdapter

3285
来自专栏Linyb极客之路

使用lazyInit缩短Spring Boot启动时间

Spring Boot可以进行有助于相关针对项目的设置,包括最常见的默认设置和随时可用的配置,这无疑是很棒的,因为它节省了宝贵的时间 然而,对于框架的新手来说,...

967

扫码关注云+社区