Android编程权威指南(第二版)学习笔记(二十六)—— 第26章 后台服务

本章主要讲了 Android 的一大组件:服务。使用 IntentService 作为后台服务,用 AlarmManager 定时启动,以及应用通知的发出,还介绍了新的 JobScheduler 及其使用。

GitHub 地址:
完成第26章

Activity 就是 Android 应用的前台。所有应用代码都专注于提供良好的用户视觉体验。服务就是 Android 应用的后台,用户无需关心后台发生的一切。即使前台关闭,activity 消失好久了,后台服务依然可以持续不断地工作。
服务最关键的特性就是:用户离开当前应用后(打开其他应用或退回主屏幕),服务依然可以在后台运行。

1. 服务的使用

1.1 服务的能与不能

与 activity 一样,服务是一个有生命周期回调方法的应用组件。这些回调方法同样也会在主 UI 线程上运行。
初始创建的服务不会在后台线程上运行任何代码。而大多数重要服务都需要某种后台线程,IntentService 类提供了一套标准实现代码,所以推荐使用 IntentService 完成本章。

1.2 服务的生命周期

如果是startService(Intent)方法启动的服务,其生命周期很简单,并具有三种生命周期回调方法。

  1. onCreate(...)方法:服务创建时调用。
  2. onStartCommand(Intent,int,int)方法:每次组件通过 startService(Intent)方法
    启动服务时调用一次。它有两个整数参数,一个是标识符集,一个是启动 ID。标识符集用来表示当前 intent 发送究竟是一次重新发送,还是一次从没成功过的发送。每次调用 onStartCommand(Intent,int,int)方法,启动 ID 都会不同。因此,启动 ID 也可用于区分不同的命令。
  3. onDestroy()方法:服务不再需要时调用。通常是在服务停止后。 服务停止时会调用 onDestroy()方法。服务停止的方式取决于服务的类型。
  4. 服务的类型由 onStartCommand(…)方法的返回值确定,可能的服务类型有 Service.START_NOT_STICKY
    START_REDELIVER_INTENTSTART_STICKYIntentService 是一种 non-sticky 服务

1.3 不同类型的服务

  1. non-sticky 服务
    non-sticky 服务在服务自己认为已完成任务时停止。为获得 non-sticky 服务,应返回START_NOT_STICKYSTART_REDELIVER_INTENT。两者区别在于,如果系统需要在服务完成任务之前关闭它,则服务的具体表现会有所不同。START_NOT_STICKY型服务说消亡就消亡了;而START_REDELIVER_INTENT型服务则会在资源不再吃紧时,尝试再次启动服务。

    通过调用 stopSelf()或 stopSelf(int)方法,我们告诉 Android 任务已完成。stopSelf() 是个无条件方法。不管 onStartCommand(…)方法调用多少次,该方法总是会成功停止服务。stopSelf(int)是个有条件的方法。该方法需要来自于 onStartCommand(…)方法的启动 ID。只有在接收到最新启动 ID 后,该方法才会停止服务。(这也是 IntentService 的后台工作原理。)

  2. sticky 服务
    sticky 服务会持续运行,直到外部组件调用 Context.stopService(Intent)方法让它停止。 为获得 sticky 服务,应返回 START_STICKY。
    sticky 服务启动后会持续运行,除非某个组件调用 Context.stopService(Intent)方法停止它。如因某种原因需终止服务,可传入一个 null intent 给 onStartCommand(…)方法,实现服务的重启。sticky 服务适用于长时间运行的服务,如音乐播放器这种启动后一直保持运行状态,直到用户主动停止的服务。

1.3 服务的使用

一个最基本的 IntentService 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PollService extends IntentService {

private static final String TAG = "PollService";

// 外界获取服务的实例
public static Intent newIntent(Context context) {
return new Intent(context, PollService.class);
}

public PollService() {
super(TAG);
}

// 服务主要执行代码的地方
@Override
protected void onHandleIntent(Intent intent) {
Log.i(TAG, "Received an intent: " + intent);
}
}

在外界使用 Context.startService(Intent) 即可开启服务。

2. 使用 AlarmManager 定时启动服务

一个基本的定时启动代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 首先获取服务启动的 intent
Intent i = PollService.newIntent(context);
// 将其放入 PendingIntent 中
PendingIntent pi = PendingIntent.getService(context, 0, i, 0);
// 获取 AlarmManager 服务
AlarmManager alarmManager = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
// 如果开启服务
if (isOn) {
// 将这个 PendingIntent 放到 AlarmManager 中定时启动
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime(), POLL_INTERVAL, pi);
} else {
// 如果没有开启服务,就让 AlarmManager 撤销该定时器
alarmManager.cancel(pi);
// 它自己也要撤销
pi.cancel();
}

2.1 PendingIntent

PendingIntent 是一种 token 对象。调用 PendingIntent.getService(…)方法获取 PendingIntent 时,我们告诉操作系统:“请记住, 我需要使用 startService(Intent)方法发送这个 intent。”随后,调用 PendingIntent 对象的 send()方法时,操作系统会按照要求发送原来封装的 intent。
PendingIntent 真正精妙的地方在于,将 PendingIntent token 交给其他应用使用时,它是代表当前应用发送 token 对象的。另外,PendingIntent 本身存在于操作系统而不是 token 里。如果不顾及别人感受的话,也可以在交给别人一个 PendingIntent 对象后,立即撤销它,让 send()方法什么也做不了。**如果使用同一个 intent 请求 PendingIntent 两次,得到的 PendingIntent 仍会是同一个。**我们可借此测试某个 PendingIntent 是否已存在,或撤销已发出的 PendingIntent。

PendingIntent.getService(...) 方法打包了启动服务的方法的调用。它有四个参数:一个用来发送 intent 的 Context,一个区分 PendingIntent 来源的请求代码,一个待发送的 Intent 对象以及一组用来决定如何创建 PendingIntent 的标志符。

2.2 使用 AlarmManager

我们用 AlarmManager.setInexactRepeating(…) 方法开启了定时启动,该方法同样具有四个参数: 一个描述定时器时间基准的常量,定时器启动的时间,定时器循环的时间间隔以及一个到时要发送的 PendingIntent。

  1. AlarmManager.ELAPSED_REALTIME 是基准时间值 , 这表明我们是以 SystemClock. elapsedRealtime()走过的时间来确定何时启动时间的。也就是说,经过一段指定的时间,就启动定时器。假如使用 AlarmManager.RTC,启动基准时间就是当前时刻(例如,System. currentTimeMillis())。也就是说,一旦到了某个固定时刻,就启动定时器。
  2. 时间间隔由我们自己确定,不过推荐使用 AlarmManager 自身定义的常量。

2.3 获取定时器激活状态

由于我们在代码中撤销定时器的同时也撤销了 PendingIntent,所以通过发送一个 PendingIntent.FLAG_NO_CREATE 标志给 getService 方法可以获取这个 PendingIntent 存在状态。

3. 通知

如果服务需要与用户沟通,通知信息(notification)总是一个不错的选择。通知信息是指显示在通知抽屉上的消息条目,用户可向下滑动屏幕读取。 想要发送通知信息,首先要创建 Notification 对象。

Notification 需使用构造对象来创建。完整的 Notification 至少应包括:

  • 在 Lollipop 之前的设备上,首次显示通知信息时,在状态栏上显示的 ticker text(Lollipop
    之后,ticker text 不再显示在状态栏上,但仍与可访问性服务相关);
  • 在状态栏上显示的图标(在 Lollipop 之前的设备上,图标在 ticker text 消失后出现);
  • 代表通知信息自身,在通知抽屉中显示的视图;
  • 待触发的 PendingIntent,用户点击抽屉中的通知信息时触发。

完成 Notification 对象的创建后,可调用 NotificationManager 系统服务的 notify(int, Notification)方法发送它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Resources resources = getResources();
Intent i = PhotoGalleryActivity.newIntent(this);
PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);

Notification notification = new NotificationCompat.Builder(this)
.setTicker(resources.getString(R.string.new_pictures_title))
.setSmallIcon(android.R.drawable.ic_menu_report_image)
.setContentTitle(resources.getString(R.string.new_pictures_title))
.setContentText(resources.getString(R.string.new_pictures_text))
.setContentIntent(pi)
.setAutoCancel(true)
.build();

NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(this);
notificationManager.notify(0, notification);
  1. 首先,调用 setTicker(CharSequence)和 setSmallIcon (int)方法,配置 ticker text 和小图标。
  2. 然后配置 Notification 在下拉抽屉中的外观。图标的值来自于 setSmallIcon(int) 方法 , 而设置标题和显示文字则需分别调用 setContentTitle (CharSequence)和 setContentText(CharSequence)方法。
  3. 接下来,须指定用户点击 Notification 消息时所触发的动作行为。这里使用的是 PendingIntent。用户在下拉抽屉中点击 Notification 消息时,传入 setContentIntent(PendingIntent)方法的 PendingIntent 会被触发。
  4. 调用 setAutoCancel (true)方法可调整上述行为。一旦执行了 setAutoCancel(true)设置方法,用户点击 Notification 消息时,该消息就会从消息抽屉中删除。
  5. 最后,从当前 context 中取出一个 NotificationManagerCompat 实例,然后调用 Notifi- cationManagerCompat.notify(…)方法贴出消息。传入的整数参数是通知消息的标识符,在整个应用中该值应该是唯一的。如果使用同一 ID 发送两条消息,则第二条消息会替换掉第一条消息。在实际开发中,这也是进度条或其他动态视觉效果的实现方式。

GitHub Page: kniost.github.io
简书:http://www.jianshu.com/u/723da691aa42