创建 Android 服务

本指南讨论 Xamarin.Android 服务,这些服务是 Android 组件,允许在没有活跃用户界面的情况下完成工作。 服务通常用于在后台执行的任务,例如耗时的计算、下载文件、播放音乐等。 其中解释了服务适合的不同方案,并介绍了如何实现这些服务以执行长时间运行的后台任务,并为远程过程调用提供接口。

Android 服务概述

移动应用与桌面应用不同。 桌面设备具有大量资源(例如屏幕占用空间、内存、存储空间和连接的电源),而移动设备并没有这些资源。 这些约束强制移动应用以不同的方式运作。 例如,移动设备上的小屏幕通常意味着一次只显示一个应用(即活动)。 其他活动将被移至后台,并被推送到它们无法进行任何工作的挂起状态。 但是,仅仅因为 Android 应用程序处于后台,并不意味着应用无法继续工作。

Android 应用程序至少由以下四个主要组件之一组成:活动、广播接收器、内容提供程序和服务。 活动是许多出色的 Android 应用程序的基础,因为它们提供了允许用户与应用程序进行交互的 UI。 但是,在执行并发或后台工作时,活动并不总是最佳选择。

Android 中后台工作的主要机制是服务。 Android 服务是一个组件,旨在执行一些无需用户界面的工作。 服务可能会下载文件、播放音乐或对图像应用筛选器。 服务还可用于 Android 应用程序之间的进程间通信 (IPC)。 例如,一个 Android 应用可能使用来自另一个应用的音乐播放器服务,或者某个应用可能会通过服务向其他应用公开数据(例如某个用户的联系信息)。

服务及其执行后台工作的能力对于提供平滑流畅的用户界面至关重要。 所有 Android 应用程序都有一个主线程(也称为 UI 线程),可在其中运行活动。 若要使设备保持响应能力,Android 必须能够以每秒 60 帧的速度更新用户界面。 如果 Android 应用在主线程上执行太多工作,则 Android 会丢帧,这反过来又会导致 UI 出现跳帧(有时也指质量不稳定 (janky))。 这意味着,在 UI 线程上执行的任何工作都应在两帧之间的时间跨度内完成,大约 16 毫秒(每 60 帧 1 秒)。

为了解决此问题,开发人员可以使用活动中的线程来执行阻止 UI 的一些工作。 但是,这可能会导致问题。 Android 很可能会销毁并重新创建活动的多个实例。 但是,Android 不会自动销毁线程,这可能会导致内存泄漏。 其中的主要示例是,当旋转设备时 – Android 将尝试销毁活动实例,然后重新创建一个新实例:

When device rotates, instance 1 is destroyed and instance 2 is created

这是潜在的内存泄漏 – 由活动的第一个实例创建的线程仍将运行。 如果该线程引用了活动的第一个实例,这将阻止 Android 对对象进行垃圾回收。 但是,仍会创建活动的第二个实例(进而可能会创建新线程)。 连续多次旋转设备可能会耗尽所有 RAM 并强制 Android 终止整个应用程序以回收内存。

根据经验,如果要执行的工作的生存期应超过活动,则应创建服务来执行该工作。 但是,如果工作仅适用于活动的上下文,则创建线程来执行该工作可能更合适。 例如,为刚刚添加到照片库应用的照片创建缩略图,可能应该在服务中进行。 但是,线程可能更适合播放一些仅当活动处于前台时才应听到的音乐。

后台工作可以分为两大类:

  1. 长时间运行的任务 – 这是在显式停止之前正在进行的工作。 长时间运行的任务的示例是一个应用,用于流式传输音乐或必须监视从传感器收集的数据。 即使应用程序没有可见的用户界面,这些任务也必须运行。
  2. 定期任务 –(有时称为作业)定期任务是持续时间相对较短(几秒钟)且按计划运行(即每周运行一次,或者也许在未来 60 秒内只运行一次)的任务。 例如,从 Internet 下载文件或为图像生成缩略图。

有四种不同类型的 Android 服务:

  • 绑定服务 – 绑定服务是将一些其他组件(通常为活动)绑定到自身的服务。 绑定服务提供一个接口,该接口允许绑定组件和服务彼此进行交互。 一旦没有更多的客户端绑定到该服务,Android 就会关闭该服务。

  • IntentServiceIntentServiceService 的一个专用的子类,可简化服务创建和使用。 IntentService 旨在处理单个自主调用。 与可以同时处理多个调用的服务不同,IntentService 更像是工作队列处理器 – 工作排成队列,IntentService 在单个工作线程上一次处理一个作业。 通常,IntentService 不绑定到活动或片段。

  • 已启动服务 –“已启动服务”是已经由其他一些 Android 组件(例如活动)启动的服务,并且在后台持续运行,直到明确指示服务停止。 与绑定服务不同,已启动服务没有直接绑定到自身的任何客户端。 出于此原因,设计已启动服务非常重要,以便在必要时可以正常重启这些服务。

  • 混合服务 – 混合服务是具有已启动服务和绑定服务特征的服务。 混合服务可以在组件绑定到该服务时启动,也可以由某些事件启动。 客户端组件可能绑定也可能未绑定到混合服务。 混合服务将一直保持运行,直到它被明确指示停止,或直到没有更多客户端绑定到它。

使用哪种类型的服务在很大程度上取决于应用程序需求。 根据经验,IntentService 或绑定服务足以完成 Android 应用程序必须执行的大多数任务,因此应优先选择这两种服务中的一种。 对于“一次性”任务(例如下载文件)来说,IntentService 是一个不错的选择,而绑定服务则适合在需要频繁与活动/片段进行交互时使用。

虽然大多数服务在后台运行,但有一个称为前台服务的特殊子类别。 这种服务具有更高的优先级(与普通服务相比),可为用户执行某些工作(如播放音乐)。

也可以在同一设备上的独立进程中运行服务,这有时称为远程服务或进程外服务。 这确实需要更多工作量来创建,但当应用程序需要与其他应用程序共享功能时,这非常有用,而且在某些情况下可以改善应用程序的用户体验。

Android 8.0 中的后台执行限制

从 Android 8.0(API 级别 26)开始,Android 应用程序不再能够在后台自由运行。 在前台,应用可以无限制地启动和运行服务。 当应用程序进入后台时,Android 将授予应用一定的时间来启动和使用服务。 该时间过后,应用将无法再启动任何服务,并且启动的任何服务都将被终止。 此时,应用无法执行任何工作。 如果满足以下条件之一,Android 会将应用程序视为处于前台:

  • 有一个可见的活动(已启动或已暂停)。
  • 应用已启动前台服务。
  • 一个应用位于前台,正在使用另一个应用的组件,而该应用本应位于后台。 例如,如果应用程序 A(位于前台)绑定到应用程序 B 提供的服务。随后也会考虑使应用程序 B 位于前台,而不是因为处于后台而被 Android 终止。

在某些情况下,即使应用位于后台,Android 也会唤醒应用并放宽这些限制几分钟,从而允许应用执行一些工作:

  • 应用接收高优先级 Firebase 云消息。
  • 应用接收广播。
  • 应用程序接收并执行 PendingIntent 以响应通知。

现有的 Xamarin.Android 应用程序可能需要更改其执行后台工作的方式,以避免 Android 8.0 上可能出现的任何问题。 下面是 Android 服务的一些实用备选项:

  • 使用 Android 作业计划程序或 Firebase 作业调度程序计划要在后台运行的工作 – 这两个库为应用程序提供了一个框架,用于将后台工作隔离到作业,即离散的工作单元。 然后,应用可以使用操作系统以及有关作业何时可以运行的一些条件来计划作业。
  • 在前台启动服务 – 当应用必须在后台执行某些任务,而用户可能需要定期与该任务进行交互时,前台服务就会非常有用。 前台服务将显示一个持久通知,以便用户知道应用正在运行后台任务,并且还会提供监视任务或与任务进行交互的方法。 例如,播客应用可以向用户回放播客,或者下载播客剧集,以便日后欣赏。
  • 使用高优先级 Firebase 云消息 (FCM) – 当 Android 收到应用的高优先级 FCM 时,它将允许该应用在后台短时间运行服务。 这将是一个很好的替代方法,让后台服务在后台轮询应用。
  • 当应用进入前台时延迟工作 – 如果以前的解决方案都不可行,则应用必须开发自己的方法,以便在应用进入前台时暂停和恢复工作。