本章主要讲了 Android 的一大组件:服务。使用 IntentService 作为后台服务,用 AlarmManager 定时启动,以及应用通知的发出,还介绍了新的 JobScheduler 及其使用。
GitHub 地址:
完成第26章
Activity 就是 Android 应用的前台。所有应用代码都专注于提供良好的用户视觉体验。服务就是 Android 应用的后台,用户无需关心后台发生的一切。即使前台关闭,activity 消失好久了,后台服务依然可以持续不断地工作。
服务最关键的特性就是:用户离开当前应用后(打开其他应用或退回主屏幕),服务依然可以在后台运行。
1. 服务的使用
1.1 服务的能与不能
与 activity 一样,服务是一个有生命周期回调方法的应用组件。这些回调方法同样也会在主 UI 线程上运行。
初始创建的服务不会在后台线程上运行任何代码。而大多数重要服务都需要某种后台线程,IntentService 类提供了一套标准实现代码,所以推荐使用 IntentService 完成本章。
1.2 服务的生命周期
如果是startService(Intent)
方法启动的服务,其生命周期很简单,并具有三种生命周期回调方法。
-
onCreate(...)
方法:服务创建时调用。 -
onStartCommand(Intent,int,int)
方法:每次组件通过 startService(Intent)方法
启动服务时调用一次。它有两个整数参数,一个是标识符集,一个是启动 ID。标识符集用来表示当前 intent 发送究竟是一次重新发送,还是一次从没成功过的发送。每次调用 onStartCommand(Intent,int,int)方法,启动 ID 都会不同。因此,启动 ID 也可用于区分不同的命令。 -
onDestroy()
方法:服务不再需要时调用。通常是在服务停止后。 服务停止时会调用 onDestroy()方法。服务停止的方式取决于服务的类型。 - 服务的类型由 onStartCommand(...)方法的返回值确定,可能的服务类型有 Service.
START_NOT_STICKY
、
START_REDELIVER_INTENT
和START_STICKY
。IntentService 是一种 non-sticky 服务。
1.3 不同类型的服务
-
non-sticky 服务
non-sticky 服务在服务自己认为已完成任务时停止。为获得 non-sticky 服务,应返回START_NOT_STICKY
或START_REDELIVER_INTENT
。两者区别在于,如果系统需要在服务完成任务之前关闭它,则服务的具体表现会有所不同。START_NOT_STICKY
型服务说消亡就消亡了;而START_REDELIVER_INTENT
型服务则会在资源不再吃紧时,尝试再次启动服务。通过调用 stopSelf()或 stopSelf(int)方法,我们告诉 Android 任务已完成。stopSelf() 是个无条件方法。不管 onStartCommand(...)方法调用多少次,该方法总是会成功停止服务。stopSelf(int)是个有条件的方法。该方法需要来自于 onStartCommand(...)方法的启动 ID。只有在接收到最新启动 ID 后,该方法才会停止服务。(这也是 IntentService 的后台工作原理。)
sticky 服务
sticky 服务会持续运行,直到外部组件调用 Context.stopService(Intent)方法让它停止。 为获得 sticky 服务,应返回 START_STICKY。
sticky 服务启动后会持续运行,除非某个组件调用 Context.stopService(Intent)方法停止它。如因某种原因需终止服务,可传入一个 null intent 给 onStartCommand(...)方法,实现服务的重启。sticky 服务适用于长时间运行的服务,如音乐播放器这种启动后一直保持运行状态,直到用户主动停止的服务。
1.3 服务的使用
一个最基本的 IntentService 如下:
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 定时启动服务
一个基本的定时启动代码如下:
// 首先获取服务启动的 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。
- AlarmManager.ELAPSED_REALTIME 是基准时间值 , 这表明我们是以 SystemClock. elapsedRealtime()走过的时间来确定何时启动时间的。也就是说,经过一段指定的时间,就启动定时器。假如使用 AlarmManager.RTC,启动基准时间就是当前时刻(例如,System. currentTimeMillis())。也就是说,一旦到了某个固定时刻,就启动定时器。
- 时间间隔由我们自己确定,不过推荐使用 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)方法发送它。
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);
- 首先,调用 setTicker(CharSequence)和 setSmallIcon (int)方法,配置 ticker text 和小图标。
- 然后配置 Notification 在下拉抽屉中的外观。图标的值来自于 setSmallIcon(int) 方法 , 而设置标题和显示文字则需分别调用 setContentTitle (CharSequence)和 setContentText(CharSequence)方法。
- 接下来,须指定用户点击 Notification 消息时所触发的动作行为。这里使用的是 PendingIntent。用户在下拉抽屉中点击 Notification 消息时,传入 setContentIntent(PendingIntent)方法的 PendingIntent 会被触发。
- 调用 setAutoCancel (true)方法可调整上述行为。一旦执行了 setAutoCancel(true)设置方法,用户点击 Notification 消息时,该消息就会从消息抽屉中删除。
- 最后,从当前 context 中取出一个 NotificationManagerCompat 实例,然后调用 Notifi- cationManagerCompat.notify(...)方法贴出消息。传入的整数参数是通知消息的标识符,在整个应用中该值应该是唯一的。如果使用同一 ID 发送两条消息,则第二条消息会替换掉第一条消息。在实际开发中,这也是进度条或其他动态视觉效果的实现方式。
GitHub Page: kniost.github.io
简书://www.greatytc.com/u/723da691aa42