安卓基础:后台任务
26安卓基础:后台任务
https://developer.android.com/training/best-background
后台处理指南
目录后台处理面临的挑战为您的工作选择合适的解决方案WorkManager前台服务AlarmManagerDownloadManager
每个 Android 应用都有一个主线程,负责处理界面(包括测量和绘制视图)、协调用户互动以及接收生命周期事件。如果有太多工作在主线程中进行,则应用可能会挂起或运行速度变慢,从而导致用户体验不佳。任何长时间运行的计算和操作(例如解码位图、访问磁盘或执行网络请求)都应在单独的后台线程上完成。一般来说,任何所需时间超过几毫秒的任务都应该分派到后台线程。在用户与应用积极互动时,可能需要执行几项这样的任务。要了解如何在用户积极使用应用时在后台线程而非主界面线程上运行任务,请查看线程处理解决方案指南。
即使在用户没有积极使用应用时,应用可能也需要运行一些任务(例如,定期与后端服务器同步或定期从应用内提取新内容)。即使在用户完成与应用的互动后,应用可能也需要立即运行一些服务至结束。本指南将帮助您了解在这些使用情形下哪种解决方案最能满足您的需求。
后台处理面临的挑战
后台任务会使用设备的有限资源,例如 RAM 和电池电量。如果处理不当,可能会导致用户体验不佳。
为了最大限度地延长电池续航时间并强制推行良好的应用行为,Android 会在应用(或前台服务通知)对用户不可见时,限制后台工作。
- Android 6.0(API 级别 23)引入了低电耗模式和应用待机模式。低电耗模式会在屏幕处于关闭状态且设备处于静止状态时限制应用行为。应用待机模式会将未使用的应用置于一种特殊状态,进入这种状态后,应用的网络访问、作业和同步会受到限制。
- Android 7.0(API 级别 24)限制了隐式广播,并引入了随时随地使用低电耗模式。
- Android 8.0(API 级别 26)进一步限制了后台行为,例如在后台获取位置信息和释放缓存的唤醒锁定。
- Android 9(API 级别 28)引入了应用待机存储分区,通过它,系统会根据应用使用模式动态确定应用资源请求的优先级。
请务必了解您的任务需求并选择合适的解决方案,以便根据系统最佳做法安排后台作业。
为您的工作选择合适的解决方案
- 工作可以延迟,还是需要立即执行?例如,如果您需要从网络中提取一些数据以对用户点击按钮的操作作出响应,则必须立即执行该工作。但是,如果您希望将日志上传到服务器,则可以延迟执行该工作,而不会影响应用的性能或用户期望。
- 工作是否依赖系统条件?您可能希望仅在设备满足特定条件(例如连接到电源、连接到互联网等)时运行作业。例如,您的应用可能需要定期压缩存储的数据。为避免对用户造成影响,您会希望仅在设备充电和处于闲置状态时运行该作业。
- 作业是否需要在确切的时间运行?日历应用可能会允许用户为事件设置在特定时间触发的提醒。用户希望在正确的时间看到提醒通知。在其他情况下,应用可能不会关注作业的确切运行时间。应用可能会有常规要求,例如“必须先运行作业 A,然后运行作业 B、作业 C”,但不要求作业在特定时间运行。
图 1. 执行后台工作的最佳方式是什么?
WorkManager
对于可延迟的工作以及预计即使您的设备或应用重启也会运行的工作,请使用 WorkManager。WorkManager 是一个 Android 库,可在满足工作的条件(例如网络可用性和电源)时妥善运行可延迟的后台工作。
WorkManager 提供向后兼容的 API(兼容 API 级别 14 及更高级别),利用 JobScheduler
API(API 级别 23 及更高级别)帮助优化更低级别设备上的电池续航时间、批量作业以及 AlarmManager
和 BroadcastReceiver
的组合。
前台服务
对于需要立即运行并且必须执行完毕的由用户发起的工作,请使用前台服务。使用前台服务可告知系统应用正在执行重要任务,不应被终止。前台服务通过通知栏中的不可关闭通知向用户显示。
AlarmManager
如果您需要在确切的时间运行某项作业,请使用 AlarmManager
。AlarmManager
会在您指定的时间启动应用(如有必要),以便运行该作业。但是,如果您的作业不需要在确切的时间运行,则 WorkManager
是更好的选择;WorkManager
能更好地平衡系统资源。例如,如果您需要大约每小时运行一次某项作业,但不需要在特定时间运行该作业,则应使用 WorkManager
设置周期性作业。
DownloadManager
如果您的应用执行长时间运行的 HTTP 下载,请考虑使用 DownloadManager。客户端可能会请求将 URI 下载到位于应用进程之外的特定目标文件中。内容下载管理器会在后台执行下载操作,它负责处理 HTTP 互动,在下载失败或连接发生更改以及系统重新启动后重新尝试下载。
将运算发送到多个线程
将长时间运行的数据密集型运算拆分为在多个线程上运行的较小运算,通常可以提高运算的运行速度和效率。如果设备的 CPU 包含多个处理器(内核),系统可以并行运行线程,而不是让每个子运算等待机会运行。例如,当解码多个图片文件以在缩略图屏幕上显示时,如果在一个线程上解码一个文件,运行速度将会大大提高。
本指南介绍了如何借助线程池对象在 Android 应用中设置和使用多个线程,如何定义要在线程上运行的代码,以及如何在这类线程与界面线程之间进行通信。
课程
-
了解如何通过定义实现
Runnable
接口的类来编写在独立Thread
上运行的代码。 -
了解如何创建管理
Thread
对象池和Runnable
对象队列的对象。此对象称为ThreadPoolExecutor
。 -
了解如何在线程池中的线程上运行
Runnable
。 -
了解如何从线程池中的线程与界面线程进行通信。
更多信息
如需详细了解 Android 上的多线程运算,请参阅以下指南:
示例应用
如需尝试本指南中的概念,请下载 ThreadSample
。
指定要在线程上运行的代码
目录定义实现 Runnable 的类实现 run() 方法更多信息应用示例
本指南介绍如何实现 Runnable
类,该类在单独的线程上运行其 Runnable.run()
方法中的代码。您还可以将 Runnable
传递给另一个对象,然后该对象可将此 Runnable 附加到线程并运行它。执行特定操作的一个或多个 Runnable
对象有时被称作“任务”。
Thread
和 Runnable
属于基本类,它们本身的功能有限。不过,它们是强大的 Android 类(例如 HandlerThread
、AsyncTask
和 IntentService
)的基础。Thread
和 Runnable
也是 ThreadPoolExecutor
类的基础。该类会自动管理线程和任务队列,甚至还能并行运行多个线程。
定义实现 Runnable 的类
创建实现 Runnable
的类非常简单。例如:
class PhotoDecodeRunnable : Runnable {
...
override fun run() {
/*
* Code you want to run on the thread goes here
*/
...
}
...
}
实现 run() 方法
在类中,Runnable.run()
方法中包含所执行的代码。通常,Runnable
中允许包含任何内容。但请记住,Runnable
不会在界面线程上运行,因此它不能直接修改界面对象(例如 View
对象)。要与界面线程进行通信,您必须使用与界面线程进行通信一课中介绍的方法。
在 run()
方法的开头,使用 THREAD_PRIORITY_BACKGROUND
调用 Process.setThreadPriority()
,将线程设置为使用后台优先级。这种方法可以减少 Runnable
对象的线程和界面线程之间的资源竞争。
您还应该调用 Thread.currentThread()
,以在 Runnable
本身中存储对 Runnable
对象的 Thread
的引用。
以下代码段展示了如何设置 run()
方法:
class PhotoDecodeRunnable : Runnable {
...
/*
* Defines the code to run for this task.
*/
override fun run() {
// Moves the current Thread into the background
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND)
...
/*
* Stores the current Thread in the PhotoTask instance,
* so that the instance
* can interrupt the Thread.
*/
photoTask.setImageDecodeThread(Thread.currentThread())
...
}
...
}
创建管理多个线程的管理器
指南介绍了如何指定要在线程上运行的代码以及如何定义在单独的线程上执行的任务。如果您只想运行该任务一次,那么您只需要了解上述内容就足够了。如果您要针对不同的数据集重复运行某个任务,但需要逐次运行,则 IntentService
符合您的需求。要想在资源可用时自动运行任务,并/或允许多个任务同时运行,您需要提供一组托管线程。为此,需要使用 ThreadPoolExecutor
实例,该实例会在其线程池中的线程空闲下来时运行队列中的任务。要运行任务,您只需将其添加到队列中即可。
线程池可以运行一个任务的多个并行实例,因此您应确保代码是线程安全的。将可供多个线程访问的变量封装在 synchronized
块中。这种方法可以防止一个线程在写入变量时其他线程去读取该变量。通常,这种情况会出现在静态变量中,但也会出现在仅实例化一次的任何对象中。要详细了解相关内容,请参阅进程和线程概览指南。
定义线程池类
在其自己的类中实例化 ThreadPoolExecutor
。在该类中,执行以下操作:
-
为线程池使用静态变量
您可能只想为应用使用单个线程池实例,以便获得受限 CPU 或网络资源的单个控制点。如果您有不同的
Runnable
类型,您可能想要一个类型有一个线程池,但其中的每个线程池都可以是单个实例。例如,您可以将此作为全局字段声明的一部分进行添加(对于 Kotlin,我们可以创建一个对象):// Creates a single static instance of PhotoManager object PhotoManager { ... }
-
使用私有构造函数
将构造函数设为私有可确保它是单例的,这意味着您不必将对类的访问封装在
synchronized
块中(对于 Kotlin 而言,则无需使用私有构造函数,因为这会被定义为一个对象,因而只会初始化一次):object PhotoManager { ... }
-
通过调用线程池类中的方法来启动任务。
在线程池类中定义将任务添加到线程池队列的方法。例如:
object PhotoManager { ... fun startDownload(imageView: PhotoView, downloadTask: DownloadTask, cacheFlag: Boolean) = decodeThreadPool.execute(downloadTask.getHTTPDownloadRunnable()) ... }
-
在构造函数中实例化
Handler
并将其附加到应用的界面线程。应用可通过
Handler
安全地调用界面对象(例如View
对象)的方法。大多数界面对象只能通过界面线程安全地进行更改。与界面线程通信一课中对此方法进行了更详细的介绍。例如:object PhotoManager { ... private val handler = object : Handler(Looper.getMainLooper()) { /* * handleMessage() defines the operations to perform when * the Handler receives a new Message to process. */ override fun handleMessage(msg: Message?) { ... } ... } }
确定线程池参数
了解了总体类结构后,您就可以开始定义线程池了。要实例化 ThreadPoolExecutor
对象,您需要以下值:
-
初始池大小和最大池大小
要分配给池的初始线程数量以及所允许的最大数量。一个线程池可具有的线程数量主要取决于设备中的可用内核数量。此数量可从系统环境获得:此数量可能并不反映设备中的真实内核数量;某些设备的 CPU 会根据系统负载停用一个或多个内核。对于这些设备,
availableProcessors()
会返回活跃内核的数量,该数量可能少于内核的总数。object PhotoManager { ... /* * Gets the number of available cores * (not always the same as the maximum number of cores) */ private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors() }
-
保持活跃的时间和时间单位
线程在关闭前保持空闲状态的时间。该时间由时间单位值(
TimeUnit
中定义的常数之一)解释。 -
任务队列
ThreadPoolExecutor
从中获取Runnable
对象的传入队列。为了在线程上启动代码,线程池管理器会从先进先出队列中获取Runnable
对象,并将其附加到线程。您可在创建线程池时,使用任何实现BlockingQueue
接口的队列类来提供此队列对象。为满足应用的要求,您可以从可用的队列实现中进行选择。要详细了解这些类,请参阅ThreadPoolExecutor
的类概览。本例中使用LinkedBlockingQueue
类:object PhotoManager { ... // Instantiates the queue of Runnables as a LinkedBlockingQueue private val decodeWorkQueue: BlockingQueue<Runnable> = LinkedBlockingQueue<Runnable>() ... }
创建线程池
为了创建线程池,可调用 ThreadPoolExecutor()
来实例化线程池管理器。这会创建并管理一组受约束的线程。由于初始池大小和最大池大小是相同的,因此 ThreadPoolExecutor
在实例化时会创建所有线程对象。例如:
object PhotoManager {
...
// Sets the amount of time an idle thread waits before terminating
private const val KEEP_ALIVE_TIME = 1L
// Sets the Time Unit to seconds
private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS
// Creates a thread pool manager
private val decodeThreadPool: ThreadPoolExecutor = ThreadPoolExecutor(
NUMBER_OF_CORES, // Initial pool size
NUMBER_OF_CORES, // Max pool size
KEEP_ALIVE_TIME,
KEEP_ALIVE_TIME_UNIT,
decodeWorkQueue
)
或者,如果您不想管理有关确定线程池大小的细节,您可能会发现使用创建单个线程 executor 的 Executor 工厂方法或工作窃取 executor 更为方便。
在线程池线程上运行代码
目录在线程池中的线程上运行任务中断正在运行的代码更多信息示例应用
上一篇指南介绍了如何为多个线程创建管理器,以及如何定义类来管理线程池以及在线程池上运行的任务。本课将介绍如何在线程池上运行任务。如需实现此目的,您需要将任务添加到线程池的工作队列中。当有线程可用时,ThreadPoolExecutor
会从队列中获取任务并在该线程上运行该任务。
本课还将介绍如何停止正在运行的任务。如果某个任务启动后,您发现没有必要执行这项工作,可能想停止该任务。您可以取消任务运行的线程,而不必浪费处理器时间。例如,如果您正在从网上下载图片并使用了缓存,则当任务检测到缓存中已存在该图片时,您可能想停止该任务。根据您编写应用的方式,您可能无法在开始下载之前检测到这一情况。
在线程池中的线程上运行任务
如需在特定线程池中的线程上启动任务对象,请将 Runnable
传递给 ThreadPoolExecutor.execute()
。此调用会将该任务添加到线程池的工作队列中。当有空闲线程变得可用时,管理器会接受等待时间最长的任务并在该线程上运行它:
object PhotoManager {
fun handleState(photoTask: PhotoTask, state: Int) {
when (state) {
DOWNLOAD_COMPLETE ->
// Decodes the image
decodeThreadPool.execute(photoTask.getPhotoDecodeRunnable())
...
}
...
}
...
}
当 ThreadPoolExecutor
在线程上启动 Runnable
时,它会自动调用对象的 run()
方法。
中断正在运行的代码
如需停止任务,您需要中断任务的线程。如需为执行此操作做准备,您需要在创建任务时存储任务线程的句柄。例如:
object PhotoManager {
fun handleState(photoTask: PhotoTask, state: Int) {
when (state) {
DOWNLOAD_COMPLETE ->
// Decodes the image
decodeThreadPool.execute(photoTask.getPhotoDecodeRunnable())
...
}
...
}
...
}
当 ThreadPoolExecutor
在线程上启动 Runnable
时,它会自动调用对象的 run()
方法。
中断正在运行的代码
如需停止任务,您需要中断任务的线程。如需为执行此操作做准备,您需要在创建任务时存储任务线程的句柄。例如:
class PhotoDecodeRunnable : Runnable {
// Defines the code to run for this task
override fun run() {
/*
* Stores the current Thread in the
* object that contains PhotoDecodeRunnable
*/
photoTask.setImageDecodeThread(Thread.currentThread())
...
}
...
}
如需中断线程,请调用 Thread.interrupt()
。请注意,Thread
对象由系统控制,系统可以在应用进程之外修改它们。因此,在中断线程之前您需要先锁定对它的访问,方法是将访问权限置于 synchronized
块中。例如:
object PhotoManager {
fun cancelAll() {
/*
* Creates and populates an array of Runnables with the Runnables in the queue
*/
val runnableArray: Array<Runnable> = decodeWorkQueue.toTypedArray()
/*
* Iterates over the array of Runnables and interrupts each one's Thread.
*/
synchronized(this) {
// Iterates over the array of tasks
runnableArray.map { (it as? PhotoDecodeRunnable)?.mThread }
.forEach { thread ->
thread?.interrupt()
}
}
}
...
}
大多数情况下,Thread.interrupt()
会立即停止线程。但是,它只会停止等待中的线程,而不会中断 CPU 或网络密集型任务。为避免减慢或锁定系统,您应该在尝试操作之前先测试是否存在待处理的中断请求:
/*
* Before continuing, checks to see that the Thread hasn't
* been interrupted
*/
if (Thread.interrupted()) return
...
// Decodes a byte array into a Bitmap (CPU-intensive)
BitmapFactory.decodeByteArray(imageBuffer, 0, imageBuffer.size, bitmapOptions)
...
与界面线程通信
目录在界面线程上定义 Handler将数据从任务移到界面线程将数据存储在任务对象中沿着对象层次结构往上发送状态
上一篇指南介绍了如何在线程池中的线程上运行代码,以及如何在 ThreadPoolExecutor
管理的线程上启动任务。最后一课,我们将为您介绍如何将数据从任务发送到界面 (UI) 线程上运行的对象。此功能允许任务执行后台工作,然后将结果移至位图等界面元素。
每个应用都有自己的特殊线程来运行界面对象(比如 View
对象),该线程称为界面线程。只有在界面线程上运行的对象才能访问该线程上的其他对象。由于在线程池中的线程上运行的任务不在界面线程上运行,因此它们不能访问界面对象。要将数据从后台线程移动到界面线程,请使用在界面线程上运行的 Handler
。
在界面线程上定义 Handler
Handler
是 Android 系统线程管理框架的一部分。Handler
对象会接收消息并运行代码来处理消息。通常情况下,会为新线程创建 Handler
,但也可创建连接到现有线程的 Handler
。将 Handler
连接到界面线程时,处理消息的代码会在界面线程上运行。
在创建线程池的类的构造过程中实例化 Handler
对象,并将该对象存储在全局变量中。通过使用 Handler(Looper)
构造函数实例化该对象,将其连接到界面线程。此构造函数使用 Looper
对象,该对象是 Android 系统线程管理框架的另一部分。当您基于特定的 Looper
实例实例化 Handler
时,该 Handler
将和 Looper
运行在同一个线程上。例如:
object PhotoManager {
...
private val handler: Handler = Handler(Looper.getMainLooper())
...
}
在 Handler
内部,替换 handleMessage()
方法。当 Android 系统收到它所管理的线程的新消息时,就会调用此方法。特定线程的所有 Handler
对象都会收到同一条消息。例如:
object PhotoManager {
private val handler: Handler = object : Handler(Looper.getMainLooper()) {
/*
* handleMessage() defines the operations to perform when
* the Handler receives a new Message to process.
*/
override fun handleMessage(inputMessage: Message) {
// Gets the image task from the incoming Message object.
val photoTask = inputMessage.obj as PhotoTask
...
}
}
...
}
下一节介绍如何指示 Handler
移动数据。
将数据从任务移到界面线程
要将数据从在后台线程上运行的任务对象移动到在界面线程上运行的对象,首先需要在任务对象中存储对数据和界面对象的引用。接下来,将任务对象和状态代码传递给实例化 Handler
的对象。在此对象中,将包含状态和任务对象的 Message
发送给 Handler
。由于 Handler
在界面线程上运行,因此它可以将数据移动到界面对象。
将数据存储在任务对象中
例如,这是一个在后台线程上运行的 Runnable
,它将对 Bitmap
进行解码并将其存储在其父对象 PhotoTask
中。Runnable
还会存储状态代码 DECODE_STATE_COMPLETED
。
const val DECODE_STATE_COMPLETED: Int = ...
// A class that decodes photo files into Bitmaps
class PhotoDecodeRunnable(
private val photoTask: PhotoTask,
// Gets the downloaded byte array
private var imageBuffer: ByteArray = photoTask.getByteBuffer()
) : Runnable {
...
// Runs the code for this task
override fun run() {
...
// Tries to decode the image buffer
BitmapFactory.decodeByteArray(
imageBuffer,
0,
imageBuffer.size,
bitmapOptions
)?.also { returnBitmap ->
...
// Sets the ImageView Bitmap
photoTask.image = returnBitmap
}
// Reports a status of "completed"
photoTask.handleDecodeState(DECODE_STATE_COMPLETED)
...
}
...
}
PhotoTask
还包含显示 Bitmap
的 ImageView
的句柄。即使对 Bitmap
和 ImageView
的引用位于同一对象中,也不能将 Bitmap
分配给 ImageView
,因为您当前不在界面线程上运行。
相反,下一步是将此状态发送给 PhotoTask
对象。
沿着对象层次结构往上发送状态
PhotoTask
是层次结构中的下个更上层对象。该对象中存放着对解码数据以及将显示该数据的 View
对象的引用。PhotoTask 对象从 PhotoDecodeRunnable
接收状态代码,并将其传递给存放线程池并实例化 Handler
的对象:
// Gets a handle to the object that creates the thread pools
class PhotoTask() {
...
private val photoManager: PhotoManager = PhotoManager.getInstance()
...
fun handleDecodeState(state: Int) {
// Converts the decode state to the overall state.
val outState: Int = when(state) {
PhotoDecodeRunnable.DECODE_STATE_COMPLETED -> PhotoManager.TASK_COMPLETE
...
}
...
// Calls the generalized state method
handleState(outState)
}
...
// Passes the state to PhotoManager
private fun handleState(state: Int) {
/*
* Passes a handle to this task and the
* current state to the class that created
* the thread pools
*/
PhotoManager.handleState(this, state)
}
...
}
将数据移动到界面
PhotoManager
对象从 PhotoTask
对象接收状态代码和 PhotoTask
对象的句柄。由于状态为 TASK_COMPLETE
,因此创建一个包含状态和任务对象的 Message
,并将其发送给 Handler
:
object PhotoManager {
...
// Handle status messages from tasks
fun handleState(photoTask: PhotoTask, state: Int) {
when(state) {
...
TASK_COMPLETE -> { // The task finished downloading and decoding the image
/*
* Creates a message for the Handler
* with the state and the task object
*/
handler.obtainMessage(state, photoTask)?.apply {
sendToTarget()
}
}
...
}
...
}
最后,Handler.handleMessage()
检查每一条传入 Message
的状态代码。如果状态代码为 TASK_COMPLETE
,则任务已完成,并且 Message
中的 PhotoTask
对象会同时包含 Bitmap
和 ImageView
。由于 Handler.handleMessage()
在界面线程上运行,因此它可以安全地将 Bitmap
移到 ImageView
:
object PhotoManager {
...
private val handler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(inputMessage: Message) {
// Gets the image task from the incoming Message object.
val photoTask = inputMessage.obj as PhotoTask
// Gets the ImageView for this task
val localView: PhotoView = photoTask.getPhotoView()
...
when (inputMessage.what) {
...
TASK_COMPLETE -> localView.setImageBitmap(photoTask.image)
...
else -> super.handleMessage(inputMessage)
}
...
}
...
}
...
...
}
...
}
服务概览
目录在服务和线程之间进行选择基础知识使用清单文件声明服务创建启动服务
Service
是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。
以下是三种不同的服务类型:
-
前台
前台服务执行一些用户能注意到的操作。例如,音频应用会使用前台服务来播放音频曲目。前台服务必须显示通知。即使用户停止与应用的交互,前台服务仍会继续运行。
-
后台
后台服务执行用户不会直接注意到的操作。例如,如果应用使用某个服务来压缩其存储空间,则此服务通常是后台服务。注意:如果您的应用面向 API 级别 26 或更高版本,当应用本身未在前台运行时,系统会对运行后台服务施加限制。在诸如此类的大多数情况下,您的应用应改为使用计划作业。
-
绑定
当应用组件通过调用
bindService()
绑定到服务时,服务即处于绑定状态。绑定服务会提供客户端-服务器接口,以便组件与服务进行交互、发送请求、接收结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。仅当与另一个应用组件绑定时,绑定服务才会运行。多个组件可同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。
虽然本文档分开概括讨论启动服务和绑定服务,但您的服务可同时以这两种方式运行,换言之,它既可以是启动服务(以无限期运行),亦支持绑定。唯一的问题在于您是否实现一组回调方法:onStartCommand()
(让组件启动服务)和 onBind()
(实现服务绑定)。
无论服务是处于启动状态还是绑定状态(或同时处于这两种状态),任何应用组件均可像使用 Activity 那样,通过调用 Intent
来使用服务(即使此服务来自另一应用)。不过,您可以通过清单文件将服务声明为私有服务,并阻止其他应用访问该服务。使用清单文件声明服务部分将对此做更详尽的阐述。
注意:服务在其托管进程的主线程中运行,它既不创建自己的线程,也不在单独的进程中运行(除非另行指定)。如果服务将执行任何 CPU 密集型工作或阻止性操作(例如 MP3 播放或联网),则应通过在服务内创建新线程来完成这项工作。通过使用单独的线程,您可以降低发生“应用无响应”(ANR) 错误的风险,而应用的主线程仍可继续专注于运行用户与 Activity 之间的交互。
在服务和线程之间进行选择
简单地说,服务是一种即使用户未与应用交互也可在后台运行的组件,因此,只有在需要服务时才应创建服务。
如果您必须在主线程之外执行操作,但只在用户与您的应用交互时执行此操作,则应创建新线程。例如,如果您只是想在 Activity 运行的同时播放一些音乐,则可在 onCreate()
中创建线程,在 onStart()
中启动线程运行,然后在 onStop()
中停止线程。您还可考虑使用 AsyncTask
或 HandlerThread
,而非传统的 Thread
类。如需了解有关线程的详细信息,请参阅进程和线程文档。
请记住,如果您确实要使用服务,则默认情况下,它仍会在应用的主线程中运行,因此,如果服务执行的是密集型或阻止性操作,则您仍应在服务内创建新线程。
基础知识
如要创建服务,您必须创建 Service
的子类(或使用它的一个现有子类)。在实现中,您必须重写一些回调方法,从而处理服务生命周期的某些关键方面,并提供一种机制将组件绑定到服务(如适用)。以下是您应重写的最重要的回调方法:
-
onStartCommand()
当另一个组件(如 Activity)请求启动服务时,系统会通过调用
startService()
来调用此方法。执行此方法时,服务即会启动并可在后台无限期运行。如果您实现此方法,则在服务工作完成后,您需负责通过调用stopSelf()
或stopService()
来停止服务。(如果您只想提供绑定,则无需实现此方法。) -
onBind()
当另一个组件想要与服务绑定(例如执行 RPC)时,系统会通过调用
bindService()
来调用此方法。在此方法的实现中,您必须通过返回IBinder
提供一个接口,以供客户端用来与服务进行通信。请务必实现此方法;但是,如果您并不希望允许绑定,则应返回 null。 -
onCreate()
首次创建服务时,系统会(在调用
onStartCommand()
或onBind()
之前)调用此方法来执行一次性设置程序。如果服务已在运行,则不会调用此方法。 -
onDestroy()
当不再使用服务且准备将其销毁时,系统会调用此方法。服务应通过实现此方法来清理任何资源,如线程、注册的侦听器、接收器等。这是服务接收的最后一个调用。
如果组件通过调用 startService()
启动服务(这会引起对 onStartCommand()
的调用),则服务会一直运行,直到其使用 stopSelf()
自行停止运行,或由其他组件通过调用 stopService()
将其停止为止。
如果组件通过调用 bindService()
来创建服务,且未调用 onStartCommand()
,则服务只会在该组件与其绑定时运行。当该服务与其所有组件取消绑定后,系统便会将其销毁。
只有在内存过低且必须回收系统资源以供拥有用户焦点的 Activity 使用时,Android 系统才会停止服务。如果将服务绑定到拥有用户焦点的 Activity,则它其不太可能会终止;如果将服务声明为在前台运行,则其几乎永远不会终止。如果服务已启动并长时间运行,则系统逐渐降低其在后台任务列表中的位置,而服务被终止的概率也会大幅提升—如果服务是启动服务,则您必须将其设计为能够妥善处理系统执行的重启。如果系统终止服务,则其会在资源可用时立即重启服务,但这还取决于您从 onStartCommand()
返回的值。如需了解有关系统会在何时销毁服务的详细信息,请参阅进程和线程文档。
下文将介绍如何创建 startService()
和 bindService()
服务方法,以及如何通过其他应用组件使用这些方法。
使用清单文件声明服务
如同对 Activity 及其他组件的操作一样,您必须在应用的清单文件中声明所有服务。
如要声明服务,请添加 service
元素作为 application
元素的子元素。下面是示例:
<manifest ... >
...
<application ... >
<service android:name=".ExampleService" />
...
</application>
</manifest>
如需了解有关使用清单文件声明服务的详细信息,请参阅 `` 元素参考文档。
您还可在 `` 元素中加入其他属性,以定义一些特性,如启动服务及其运行时所在进程需要的权限。android:name
属性是唯一必需的属性,用于指定服务的类名。发布应用后,请保此类名不变,以避免因依赖显式 Intent 来启动或绑定服务而破坏代码的风险(请阅读博文 Things That Cannot Change [不能更改的内容])。
注意:为确保应用的安全性,在启动 Service
时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务会响应 Intent,而用户也无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService()
,则系统会抛出异常。
您可以通过添加 android:exported
属性并将其设置为 false
,确保服务仅适用于您的应用。这可以有效阻止其他应用启动您的服务,即便在使用显式 Intent 时也如此。
注意:用户可以查看其设备上正在运行的服务。如果他们发现自己无法识别或信任的服务,则可以停止该服务。为避免用户意外停止您的服务,您需要在应用清单的 `` 元素中添加 android:description
。请在描述中用一个短句解释服务的作用及其提供的好处。
创建启动服务
启动服务由另一个组件通过调用 startService()
启动,这会导致调用服务的 onStartCommand()
方法。
服务启动后,其生命周期即独立于启动它的组件。即使系统已销毁启动服务的组件,该服务仍可在后台无限期地运行。因此,服务应在其工作完成时通过调用 stopSelf()
来自行停止运行,或者由另一个组件通过调用 stopService()
来将其停止。
应用组件(如 Activity)可通过调用 startService()
方法并传递 Intent
对象(指定服务并包含待使用服务的所有数据)来启动服务。服务会在 onStartCommand()
方法接收此 Intent
。
例如,假设某 Activity 需要将一些数据保存到在线数据库中。该 Activity 可以启动一个协同服务,并通过向 startService()
传递一个 Intent,为该服务提供要保存的数据。服务会通过 onStartCommand()
接收 Intent,连接到互联网并执行数据库事务。事务完成后,服务将自行停止并销毁。
注意:默认情况下,服务与服务声明所在的应用运行于同一进程,并且运行于该应用的主线程中。如果服务在用户与来自同一应用的 Activity 进行交互时执行密集型或阻止性操作,则会降低 Activity 性能。为避免影响应用性能,请在服务内启动新线程。
通常,您可以扩展两个类来创建启动服务:
-
Service
这是适用于所有服务的基类。扩展此类时,您必须创建用于执行所有服务工作的新线程,因为服务默认使用应用的主线程,这会降低应用正在运行的任何 Activity 的性能。
-
IntentService
这是
Service
的子类,其使用工作线程逐一处理所有启动请求。如果您不要求服务同时处理多个请求,此类为最佳选择。实现onHandleIntent()
,该方法会接收每个启动请求的 Intent,以便您执行后台工作。
下文介绍如何使用其中任一类来实现服务。
扩展 IntentService 类
由于大多数启动服务无需同时处理多个请求(实际上,这种多线程情况可能很危险),因此最佳选择是利用 IntentService
类实现服务。
IntentService
类会执行以下操作:
- 创建默认的工作线程,用于在应用的主线程外执行传递给
onStartCommand()
的所有 Intent。 - 创建工作队列,用于将 Intent 逐一传递给
onHandleIntent()
实现,这样您就永远不必担心多线程问题。 - 在处理完所有启动请求后停止服务,因此您永远不必调用
stopSelf()
。 - 提供
onBind()
的默认实现(返回 null)。 - 提供
onStartCommand()
的默认实现,可将 Intent 依次发送到工作队列和onHandleIntent()
实现。
如要完成客户端提供的工作,请实现 onHandleIntent()
。不过,您还需为服务提供小型构造函数。
以下是 IntentService
的实现示例:
/**
* A constructor is required, and must call the super [android.app.IntentService.IntentService]
* constructor with a name for the worker thread.
*/
class HelloIntentService : IntentService("HelloIntentService") {
/**
* The IntentService calls this method from the default worker thread with
* the intent that started the service. When this method returns, IntentService
* stops the service, as appropriate.
*/
override fun onHandleIntent(intent: Intent?) {
// Normally we would do some work here, like download a file.
// For our sample, we just sleep for 5 seconds.
try {
Thread.sleep(5000)
} catch (e: InterruptedException) {
// Restore interrupt status.
Thread.currentThread().interrupt()
}
}
}
您只需要一个构造函数和一个 onHandleIntent()
实现即可。
如果您还决定重写其他回调方法(如 onCreate()
、onStartCommand()
或 onDestroy()
),请确保调用超类实现,以便 IntentService
能够妥善处理工作线程的生命周期。
例如,onStartCommand()
必须返回默认实现,即如何将 Intent 传递给 onHandleIntent()
:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()
return super.onStartCommand(intent, flags, startId)
}
除 onHandleIntent()
之外,您无需从中调用超类的唯一方法就是 onBind()
。只有在服务允许绑定时,您才需实现该方法。
在下一部分中,您将了解如何在扩展 Service
基类时实现同类服务,此类包含更多代码,但如需同时处理多个启动请求,则更适合使用该基类。
扩展服务类
借助 IntentService
,您可以非常轻松地实现启动服务。但是,若要求服务执行多线程(而非通过工作队列处理启动请求),则可通过扩展 Service
类来处理每个 Intent。
为进行比较,以下示例代码展示了 Service
类的实现,该类执行的工作与上述使用 IntentService
的示例完全相同。换言之,对于每个启动请求,其均使用工作线程来执行作业,且每次仅处理一个请求。
class HelloService : Service() {
private var serviceLooper: Looper? = null
private var serviceHandler: ServiceHandler? = null
// Handler that receives messages from the thread
private inner class ServiceHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(msg: Message) {
// Normally we would do some work here, like download a file.
// For our sample, we just sleep for 5 seconds.
try {
Thread.sleep(5000)
} catch (e: InterruptedException) {
// Restore interrupt status.
Thread.currentThread().interrupt()
}
// Stop the service using the startId, so that we don't stop
// the service in the middle of handling another job
stopSelf(msg.arg1)
}
}
override fun onCreate() {
// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block. We also make it
// background priority so CPU-intensive work will not disrupt our UI.
HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
// Get the HandlerThread's Looper and use it for our Handler
serviceLooper = looper
serviceHandler = ServiceHandler(looper)
}
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()
// For each start request, send a message to start a job and deliver the
// start ID so we know which request we're stopping when we finish the job
serviceHandler?.obtainMessage()?.also { msg ->
msg.arg1 = startId
serviceHandler?.sendMessage(msg)
}
// If we get killed, after returning from here, restart
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
// We don't provide binding, so return null
return null
}
override fun onDestroy() {
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
}
}
如您所见,相较于使用 IntentService
,此示例需要执行更多工作。
但是,由于 onStartCommand()
的每个调用均有您自己处理,因此您可以同时执行多个请求。此示例并未这样做,但若您希望如此,则可为每个请求创建新线程,然后立即运行这些线程(而非等待上一个请求完成)。
请注意,onStartCommand()
方法必须返回整型数。整型数是一个值,用于描述系统应如何在系统终止服务的情况下继续运行服务。IntentService
的默认实现会为您处理此情况,但您可以对其进行修改。从 onStartCommand()
返回的值必须是以下常量之一:
-
START_NOT_STICKY
如果系统在
onStartCommand()
返回后终止服务,则除非有待传递的挂起 Intent,否则系统不会重建服务。这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。 -
START_STICKY
如果系统在
onStartCommand()
返回后终止服务,则其会重建服务并调用onStartCommand()
,但不会重新传递最后一个 Intent。相反,除非有挂起 Intent 要启动服务,否则系统会调用包含空 Intent 的onStartCommand()
。在此情况下,系统会传递这些 Intent。此常量适用于不执行命令、但无限期运行并等待作业的媒体播放器(或类似服务)。 -
START_REDELIVER_INTENT
如果系统在
onStartCommand()
返回后终止服务,则其会重建服务,并通过传递给服务的最后一个 Intent 调用onStartCommand()
。所有挂起 Intent 均依次传递。此常量适用于主动执行应立即恢复的作业(例如下载文件)的服务。
如需详细了解这些返回值,请查阅每个常量的链接参考文档。有关此服务实现类型的扩展示例,请参阅 GitHub 上的 MessagingService 示例中的 MessagingService
类。
启动服务
您可以通过将 Intent
传递给 startService()
或 startForegroundService()
,从 Activity 或其他应用组件启动服务。Android 系统会调用服务的 onStartCommand()
方法,并向其传递 Intent
,从而指定要启动的服务。
注意:如果您的应用面向 API 级别 26 或更高版本,除非应用本身在前台运行,否则系统不会对使用或创建后台服务施加限制。如果应用需要创建前台服务,则其应调用 startForegroundService()
。此方法会创建后台服务,但它会向系统发出信号,表明服务会将自行提升至前台。创建服务后,该服务必须在五秒内调用自己的 startForeground()
方法。
例如,Activity 可以结合使用显式 Intent 与 startService()
,从而启动上文中的示例服务 (HelloService
):
Intent(this, HelloService::class.java).also { intent ->
startService(intent)
}
startService()
方法会立即返回,并且 Android 系统会调用服务的 onStartCommand()
方法。如果服务尚未运行,则系统首先会调用 onCreate()
,然后调用 onStartCommand()
。
如果服务亦未提供绑定,则应用组件与服务间的唯一通信模式便是使用 startService()
传递的 Intent。但是,如果您希望服务返回结果,则启动服务的客户端可以为广播(通过 getBroadcast()
获得)创建一个 PendingIntent
,并将其传递给启动服务的 Intent
中的服务。然后,服务便可使用广播传递结果。
多个服务启动请求会导致多次对服务的 onStartCommand()
进行相应的调用。但是,如要停止服务,只需一个服务停止请求(使用 stopSelf()
或 stopService()
)即可。
停止服务
启动服务必须管理自己的生命周期。换言之,除非必须回收内存资源,否则系统不会停止或销毁服务,并且服务在 onStartCommand()
返回后仍会继续运行。服务必须通过调用 stopSelf()
自行停止运行,或由另一个组件通过调用 stopService()
来停止它。
一旦请求使用 stopSelf()
或 stopService()
来停止服务,系统便会尽快销毁服务。
如果服务同时处理多个对 onStartCommand()
的请求,则您不应在处理完一个启动请求之后停止服务,因为您可能已收到新的启动请求(在第一个请求结束时停止服务会终止第二个请求)。为避免此问题,您可以使用 stopSelf(int)
确保服务停止请求始终基于最近的启动请求。换言之,在调用 stopSelf(int)
时,您需传递与停止请求 ID 相对应的启动请求 ID(传递给 onStartCommand()
的 startId
)。此外,如果服务在您能够调用 stopSelf(int)
之前收到新启动请求,则 ID 不匹配,服务也不会停止。
注意:为避免浪费系统资源和消耗电池电量,请确保应用在工作完成之后停止其服务。如有必要,其他组件可通过调用 stopService()
来停止服务。即使为服务启用绑定,如果服务收到对 onStartCommand()
的调用,您始终仍须亲自停止服务。
如需了解有关服务生命周期的详细信息,请参阅下方管理服务生命周期的相关部分。
创建绑定服务
绑定服务允许应用组件通过调用 bindService()
与其绑定,从而创建长期连接。此服务通常不允许组件通过调用 startService()
来启动它。
如需与 Activity 和其他应用组件中的服务进行交互,或需要通过进程间通信 (IPC) 向其他应用公开某些应用功能,则应创建绑定服务。
如要创建绑定服务,您需通过实现 onBind()
回调方法返回 IBinder
,从而定义与服务进行通信的接口。然后,其他应用组件可通过调用 bindService()
来检索该接口,并开始调用与服务相关的方法。服务只用于与其绑定的应用组件,因此若没有组件与该服务绑定,则系统会销毁该服务。您不必像通过 onStartCommand()
启动的服务那样,以相同方式停止绑定服务。
如要创建绑定服务,您必须定义指定客户端如何与服务进行通信的接口。服务与客户端之间的这个接口必须是 IBinder
的实现,并且服务必须从 onBind()
回调方法返回该接口。收到 IBinder
后,客户端便可开始通过该接口与服务进行交互。
多个客户端可以同时绑定到服务。完成与服务的交互后,客户端会通过调用 unbindService()
来取消绑定。如果没有绑定到服务的客户端,则系统会销毁该服务。
实现绑定服务有多种方法,并且此实现比启动服务更为复杂。出于这些原因,请参阅另一份文档绑定服务,了解关于绑定服务的详细介绍。
向用户发送通知
处于运行状态时,服务可以使用 Toast 通知或状态栏通知来通知用户所发生的事件。
Toast 通知是指仅在当前窗口界面显示片刻的消息。状态栏通知在状态栏中提供内含消息的图标,用户可通过选择该图标来执行操作(例如启动 Activity)。
通常,当某些后台工作(例如文件下载)已经完成,并且用户可对其进行操作时,状态栏通知是最佳方法。当用户从展开视图中选定通知时,该通知即可启动 Activity(例如显示已下载的文件)。
如需了解详细信息,请参阅 Toast 通知或状态栏通知开发者指南。
在前台运行服务
前台服务是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。前台服务必须为状态栏提供通知,将其放在运行中的标题下方。这意味着除非将服务停止或从前台移除,否则不能清除该通知。
注意:请限制应用使用前台服务。
只有当应用执行的任务需供用户查看(即使该任务未直接与应用交互)时,您才应使用前台服务。因此,前台服务必须显示优先级为 PRIORITY_LOW
或更高的状态栏通知,这有助于确保用户知道应用正在执行的任务。如果某操作不是特别重要,因而您希望使用最低优先级通知,则可能不适合使用服务;相反,您可以考虑使用计划作业。
每个运行服务的应用都会给系统带来额外负担,从而消耗系统资源。如果应用尝试使用低优先级通知隐藏其服务,则可能会降低用户正在主动交互的应用的性能。因此,如果某个应用尝试运行拥有最低优先级通知的服务,则系统会在抽屉式通知栏的底部调用出该应用的行为。
例如,应将通过服务播放音乐的音乐播放器设置为在前台运行,因为用户会明确意识到其操作。状态栏中的通知可能表示正在播放的歌曲,并且其允许用户通过启动 Activity 与音乐播放器进行交互。同样,如果应用允许用户追踪其运行,则需通过前台服务来追踪用户的位置。
注意:如果应用面向 Android 9(API 级别 28)或更高版本并使用前台服务,则其必须请求 FOREGROUND_SERVICE
权限。这是一种普通权限,因此,系统会自动为请求权限的应用授予此权限。
如果面向 API 级别 28 或更高版本的应用试图创建前台服务但未请求 FOREGROUND_SERVICE
,则系统会抛出 SecurityException
。
如要请求让服务在前台运行,请调用 startForeground()
。此方法采用两个参数:唯一标识通知的整型数和用于状态栏的 Notification
。此通知必须拥有 PRIORITY_LOW
或更高的优先级。下面是示例:
val pendingIntent: PendingIntent =
Intent(this, ExampleActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, 0)
}
val notification: Notification = Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(getText(R.string.notification_title))
.setContentText(getText(R.string.notification_message))
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.ticker_text))
.build()
startForeground(ONGOING_NOTIFICATION_ID, notification)
如要从前台移除服务,请调用 stopForeground()
。此方法采用布尔值,指示是否需同时移除状态栏通知。此方法不会停止服务。但是,如果您在服务仍运行于前台时将其停止,则通知也会随之移除。
如需了解有关通知的详细信息,请参阅创建状态栏通知。
管理服务的生命周期
服务的生命周期比 Activity 的生命周期要简单得多。但是,密切关注如何创建和销毁服务反而更加重要,因为服务可以在用户未意识到的情况下运行于后台。
服务生命周期(从创建到销毁)可遵循以下任一路径:
-
启动服务
该服务在其他组件调用
startService()
时创建,然后无限期运行,且必须通过调用stopSelf()
来自行停止运行。此外,其他组件也可通过调用stopService()
来停止此服务。服务停止后,系统会将其销毁。 -
绑定服务
该服务在其他组件(客户端)调用
bindService()
时创建。然后,客户端通过IBinder
接口与服务进行通信。客户端可通过调用unbindService()
关闭连接。多个客户端可以绑定到相同服务,而且当所有绑定全部取消后,系统即会销毁该服务。(服务不必自行停止运行。)
这两条路径并非完全独立。您可以绑定到已使用 startService()
启动的服务。例如,您可以使用 Intent
(标识要播放的音乐)来调用 startService()
,从而启动后台音乐服务。随后,当用户需稍加控制播放器或获取有关当前所播放歌曲的信息时,Activity 可通过调用 bindService()
绑定到服务。此类情况下,在所有客户端取消绑定之前,stopService()
或 stopSelf()
实际不会停止服务。
实现生命周期回调
与 Activity 类似,服务也拥有生命周期回调方法,您可通过实现这些方法来监控服务状态的变化并适时执行工作。以下框架服务展示了每种生命周期方法:
class ExampleService : Service() {
private var startMode: Int = 0 // indicates how to behave if the service is killed
private var binder: IBinder? = null // interface for clients that bind
private var allowRebind: Boolean = false // indicates whether onRebind should be used
override fun onCreate() {
// The service is being created
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// The service is starting, due to a call to startService()
return mStartMode
}
override fun onBind(intent: Intent): IBinder? {
// A client is binding to the service with bindService()
return mBinder
}
override fun onUnbind(intent: Intent): Boolean {
// All clients have unbound with unbindService()
return mAllowRebind
}
override fun onRebind(intent: Intent) {
// A client is binding to the service with bindService(),
// after onUnbind() has already been called
}
override fun onDestroy() {
// The service is no longer used and is being destroyed
}
}
注意:与 Activity 生命周期回调方法不同,您不需要调用这些回调方法的超类实现。
图 2. 服务生命周期。左图显示使用 startService()
创建的服务的生命周期,右图显示使用 bindService()
创建的服务的生命周期。
图 2 展示服务的典型回调方法。尽管该图分开介绍通过 startService()
创建的服务和通过 bindService()
创建的服务,但请记住,无论启动方式如何,任何服务均有可能允许客户端与其绑定。因此,最初使用 onStartCommand()
(通过客户端调用 startService()
)启动的服务仍可接收对 onBind()
的调用(当客户端调用 bindService()
时)。
通过实现这些方法,您可以监控服务生命周期的以下两种嵌套循环:
-
服务的整个生命周期贯穿调用 onCreate() 和返回 onDestroy() 之间的这段时间。与 Activity 类似,服务也在 onCreate() 中完成初始设置,并在 onDestroy() 中释放所有剩余资源。例如,音乐播放服务可以在 onCreate() 中创建用于播放音乐的线程,然后在 onDestroy() 中停止该线程。
服务的活动生命周期从调用 onStartCommand() 或 onBind() 开始。每种方法均会获得 Intent 对象,该对象会传递至 startService() 或 bindService()。
对于启动服务,活动生命周期与整个生命周期会同时结束(即便是在
onStartCommand()
返回之后,服务仍然处于活动状态)。对于绑定服务,活动生命周期会在onUnbind()
返回时结束。
注意:尽管您需通过调用 stopSelf()
或 stopService()
来停止绑定服务,但该服务并没有相应的回调(没有 onStop()
回调)。除非服务绑定到客户端,否则在服务停止时,系统会将其销毁(onDestroy()
是接收到的唯一回调)。
创建后台服务
目录处理传入的 intent在清单中定义 intent 服务
IntentService
类为在单个后台线程上运行操作提供了一个简单明了的结构。这使它能够在不影响界面响应速度的情况下处理长时间运行的操作。此外,IntentService
不受大多数界面生命周期事件的影响,因此它能够在会关闭 AsyncTask
的情况下继续运行
IntentService
有一些限制:
- 它无法直接与您的界面互动。要在界面中显示其结果,您必须将结果发送到
Activity
。 - 工作请求依序运行。如果某个操作在
IntentService
中运行,并且您向其发送了另一个请求,则该请求会等待第一个操作完成。 - 在
IntentService
上运行的操作无法中断。
但在大多数情况下,执行简单后台操作的首选方式是 IntentService
。
本指南介绍了如何执行以下操作:
- 创建您自己的
IntentService
子类。 - 创建所需的回调方法
onHandleIntent()
。 - 在清单文件中定义
IntentService
。
处理传入的 intent
要为您的应用创建 IntentService
组件,请定义一个扩展 IntentService
的类,并在其中定义替换 onHandleIntent()
的方法。例如:
class RSSPullService : IntentService(RSSPullService::class.simpleName)
override fun onHandleIntent(workIntent: Intent) {
// Gets data from the incoming Intent
val dataString = workIntent.dataString
...
// Do work here, based on the contents of dataString
...
}
}
请注意,onStartCommand()
等常规 Service
组件的其他回调由 IntentService
自动调用。在 IntentService
中,您应该避免替换这些回调。
要详细了解如何创建 IntentService
,请参阅扩展 IntentService 类。
在清单中定义 intent 服务
IntentService
还需要应用清单中的条目。 将此条目作为 <application>
元素的子元素 <service>
提供:
<application
android:icon="@drawable/icon"
android:label="@string/app_name">
...
<!--
Because android:exported is set to "false",
the service is only available to this app.
-->
<service
android:name=".RSSPullService"
android:exported="false"/>
...
</application>
属性 android:name
指定 IntentService
的类名称。
请注意,<service>
元素不包含 intent 过滤器。向服务发送工作请求的 Activity
使用显式 Intent
,因此不需要过滤器。这也意味着只有同一应用中的组件或具有相同用户 ID 的其他应用才能访问该服务。
现在,您有了基本的 IntentService
类,可以使用 Intent
对象向其发送工作请求。将工作请求发送到后台服务中描述了构造这些对象并将其发送到 IntentService
的过程。
将工作请求发送到后台服务
目录创建工作请求并将其发送到 JobIntentService
上一课介绍了如何创建 JobIntentService
类。本节课介绍如何通过 Intent
将工作加入队列,以触发 JobIntentService
来运行某项操作。此 Intent
可以选择性地包含要供 JobIntentService
处理的数据。
创建工作请求并将其发送到 JobIntentService
要创建工作请求并将其发送到 JobIntentService
,请创建一个 Intent
并将其加入队列以便通过调用 enqueueWork()
来执行。您可以选择性地将数据添加到 intent(以 intent extra 形式)以供 JobIntentService 处理。如需详细了解如何创建 intent,请参阅 Intent 和 Intent 过滤器中的“构建 intent”部分
以下代码段演示了此过程:
-
为名为RSSPullService的
JobIntentService
创建新的Intent/* * Creates a new Intent to start the RSSPullService * JobIntentService. Passes a URI in the * Intent's "data" field. */ serviceIntent = Intent().apply { putExtra("download_url", dataUrl) }
-
调用enqueueWork()
private const val RSS_JOB_ID = 1000 RSSPullService.enqueueWork(context, RSSPullService::class.java, RSS_JOB_ID, serviceIntent)
注意,您可以从 Activity 或 Fragment 中的任意位置发送工作请求。例如,如果您需要先获得用户输入,则可从响应按钮点击或类似手势的回调发送请求。
在您调用 enqueueWork()
后,JobIntentService
会执行在其 onHandleWork()
方法中定义的工作,然后自行停止。
下一步是将工作请求的结果返回给源 Activity 或 Fragment。下一课将向您介绍如何通过 BroadcastReceiver
执行此操作。
报告工作状态
目录从 JobIntentService 报告状态接收来自 JobIntentService 的状态广播
本指南介绍了如何向发送请求的组件报告在后台服务中运行的工作请求的状态。举例来说,您可以在 Activity
对象的界面中报告请求的状态。建议使用 LocalBroadcastManager
发送和接收状态,从而将广播 Intent
对象限定为您自己应用中的组件。
从 JobIntentService 报告状态
要将 JobIntentService
中的工作请求的状态发送到其他组件,请先创建在其扩展数据中包含状态的 Intent
。您可以选择性地向此 Intent
添加操作和数据 URI。
接下来,通过调用 LocalBroadcastManager.sendBroadcast()
发送 Intent
。这会将 Intent
发送到您的应用中已注册接收它的任何组件。要获取 LocalBroadcastManager
的实例,请调用 getInstance()
。
例如:
...
// Defines a custom Intent action
const val BROADCAST_ACTION = "com.example.android.threadsample.BROADCAST"
...
// Defines the key for the status "extra" in an Intent
const val EXTENDED_DATA_STATUS = "com.example.android.threadsample.STATUS"
...
class RSSPullService : JobIntentService() {
...
/*
* Creates a new Intent containing a Uri object
* BROADCAST_ACTION is a custom Intent action
*/
val localIntent = Intent(BROADCAST_ACTION).apply {
// Puts the status into the Intent
putExtra(EXTENDED_DATA_STATUS, status)
}
// Broadcasts the Intent to receivers in this app.
LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent)
...
}
下一步是处理发送原始工作请求的组件中的传入广播 Intent
对象。
接收来自 JobIntentService 的状态广播
要接收广播 Intent
对象,请使用 BroadcastReceiver
的子类。在该子类中,实现 BroadcastReceiver.onReceive()
回调方法,供 LocalBroadcastManager
在收到 Intent
时调用。LocalBroadcastManager
将传入的 Intent
传递给 BroadcastReceiver.onReceive()
。
例如:
// Broadcast receiver for receiving status updates from the IntentService.
private class DownloadStateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
...
/*
* Handle Intents here.
*/
...
}
}
定义 BroadcastReceiver
后,您便可以为其定义匹配特定操作、类别和数据的过滤器。为此,请创建一个 IntentFilter
。第一个代码段演示了如何定义过滤器:
// Class that displays photos
class DisplayActivity : FragmentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
super.onCreate(savedInstanceState)
...
// The filter's action is BROADCAST_ACTION
var statusIntentFilter = IntentFilter(BROADCAST_ACTION).apply {
// Adds a data filter for the HTTP scheme
addDataScheme("http")
}
...
要向系统注册 BroadcastReceiver
和 IntentFilter
,请获取 LocalBroadcastManager
的实例并调用其 registerReceiver()
方法。接下来的这个代码段演示了如何注册 BroadcastReceiver
及其 IntentFilter
:
// Instantiates a new DownloadStateReceiver
val downloadStateReceiver = DownloadStateReceiver()
// Registers the DownloadStateReceiver and its intent filters
LocalBroadcastManager.getInstance(this)
.registerReceiver(downloadStateReceiver, statusIntentFilter)
...
单个 BroadcastReceiver
可以处理多个类型的广播 Intent
对象,每个类型都有自己的操作。此功能允许您针对每个操作运行不同的代码,而不必为每个操作单独定义 BroadcastReceiver
。要为同一 BroadcastReceiver
定义另一个 IntentFilter
,请创建 IntentFilter
并重复调用 registerReceiver()
。 例如:
/*
* Instantiates a new action filter.
* No data filter is needed.
*/
statusIntentFilter = IntentFilter(ACTION_ZOOM_IMAGE)
// Registers the receiver with the new filter
LocalBroadcastManager.getInstance(this)
.registerReceiver(downloadStateReceiver, statusIntentFilter)
发送广播 Intent
不会启动或恢复 Activity
。即使在您的应用位于后台时,Activity
的 BroadcastReceiver
也会接收并处理 Intent
对象,但不会强迫您的应用进入前台。如果您希望在应用不可见的情况下向用户通知后台发生的事件,请使用 Notification
切勿启动 Activity
来响应传入的广播 Intent
。
绑定服务概览
目录基础知识绑定到已启动服务创建绑定服务扩展 Binder 类
绑定服务是客户端-服务器接口中的服务器。借助绑定服务,组件(例如 Activity)可以绑定到服务、发送请求、接收响应,以及执行进程间通信 (IPC)。绑定服务通常只在为其他应用组件提供服务时处于活动状态,不会无限期在后台运行。
本文介绍如何创建绑定服务,包括如何绑定到来自其他应用组件的服务。如需了解一般服务的更多信息(例如:如何利用服务传送通知、如何将服务设置为在前台运行等),请参阅服务文档。
基础知识
绑定服务是 Service
类的实现,可让其他应用与其进行绑定和交互。如要为服务提供绑定,您必须实现 onBind()
回调方法。该方法会返回 IBinder
对象,该对象定义的编程接口可供客户端用来与服务进行交互。
绑定到已启动服务
如服务文档中所述,您可以创建同时具有已启动和已绑定两种状态的服务。换言之,可通过调用 startService()
启动服务,让服务无限期运行;此外,还可通过调用 bindService()
让客户端绑定到该服务。
如果您确实允许服务同时具有已启动和已绑定状态,则在启动服务后,如果所有客户端均解绑服务,则系统不会销毁该服务。为此,您必须通过调用 stopSelf()
或 stopService()
显式停止服务。
尽管您通常应实现 onBind()
或 onStartCommand()
,但有时需同时实现这两种方法。例如,音乐播放器可能认为,让其服务无限期运行并同时提供绑定会很有用处。如此一来,Activity 便可通过启动服务来播放音乐,并且即使用户离开应用,音乐也不会停止。然后,当用户返回应用时,Activity 便能绑定到服务,重新获得回放控制权。
如需详细了解为已启动服务添加绑定时的服务生命周期,请参阅管理绑定服务的生命周期部分。
客户端通过调用 bindService()
绑定到服务。调用时,它必须提供 ServiceConnection
的实现,后者会监控与服务的连接。bindService()
的返回值表明所请求的服务是否存在,以及是否允许客户端访问该服务。当创建客户端与服务之间的连接时,Android 系统会调用 ServiceConnection
上的 onServiceConnected()
。onServiceConnected()
方法包含 IBinder
参数,客户端随后会使用该参数与绑定服务进行通信。
您可以同时将多个客户端连接到服务。但是,系统会缓存 IBinder
服务通信通道。换言之,只有在第一个客户端绑定服务时,系统才会调用服务的 onBind()
方法来生成 IBinder
。然后,系统会将同一 IBinder
传递至绑定到相同服务的所有其他客户端,无需再次调用 onBind()
。
当最后一个客户端取消与服务的绑定时,系统会销毁服务(除非 startService()
也启动了该服务)。
针对您的绑定服务实现,其最重要的环节是定义 onBind()
回调方法所返回的接口。下文将为您介绍几种不同方法,以便您定义服务的 IBinder
接口。
创建绑定服务
创建提供绑定的服务时,您必须提供 IBinder
,进而提供编程接口,以便客户端使用此接口与服务进行交互。您可以通过三种方法定义接口:
-
如果服务是供您的自有应用专用,并且在与客户端相同的进程中运行(常见情况),则应通过扩展
Binder
类并从onBind()
返回该类的实例来创建接口。收到Binder
后,客户端可利用其直接访问Binder
实现或Service
中可用的公共方法。如果服务只是您自有应用的后台工作线程,则优先采用这种方法。只有当其他应用或不同进程占用您的服务时,您可以不必使用此方法创建接口。 -
如需让接口跨不同进程工作,您可以使用
Messenger
为服务创建接口。服务可借此方式定义Handler
,以响应不同类型的Message
对象。此Handler
是Messenger
的基础,后者随后可与客户端分享一个IBinder
,以便客户端能利用Message
对象向服务发送命令。此外,客户端还可定义自有Messenger
,以便服务回传消息。这是执行进程间通信 (IPC) 最为简单的方法,因为Messenger
会在单个线程中创建包含所有请求的队列,这样您就不必对服务进行线程安全设计。 -
Android 接口定义语言 (AIDL) 会将对象分解成原语,操作系统可通过识别这些原语并将其编组到各进程中来执行 IPC。对于之前采用
Messenger
的方法而言,其实际上是以 AIDL 作为其底层结构。如上所述,Messenger
会在单个线程中创建包含所有客户端请求的队列,以便服务一次接收一个请求。不过,如果您想让服务同时处理多个请求,则可直接使用 AIDL。在此情况下,您的服务必须达到线程安全的要求,并且能够进行多线程处理。如要直接使用 AIDL,您必须创建定义编程接口的.aidl
文件。Android SDK 工具会利用该文件生成实现接口和处理 IPC 的抽象类,您随后可在服务内对该类进行扩展。
注意:大多数应用不应使用 AIDL 来创建绑定服务,因为它可能需要多线程处理能力,并可能导致更为复杂的实现。因此,AIDL 并不适合大多数应用,本文也不会阐述如何将其用于您的服务。如果您确定自己需要直接使用 AIDL,请参阅 AIDL 文档。
扩展 Binder 类
如果您的服务仅供本地应用使用,且无需跨进程工作,则您可以实现自有 Binder
类,让客户端通过该类直接访问服务中的公共方法。
注意:只有当客户端和服务处于同一应用和进程内(最常见的情况)时,此方法才有效。例如,此方法非常适用于将 Activity 绑定到某个音乐应用的自有服务,进而实现在后台播放音乐。
以下是具体的设置方法:
-
在您的服务中,创建可执行以下某种操作的
Binder
实例:
- 包含客户端可调用的公共方法。
- 返回当前的
Service
实例,该实例中包含客户端可调用的公共方法。 - 返回由服务承载的其他类的实例,其中包含客户端可调用的公共方法。
-
从
onBind()
回调方法返回此Binder
实例。 -
在客户端中,从
onServiceConnected()
回调方法接收Binder
,并使用提供的方法调用绑定服务。
注意:服务和客户端必须在同一应用内,这样客户端才能转换返回的对象并正确调用其 API。服务和客户端还必须在同一进程内,因为此方法不执行任何跨进程编组。
例如,以下服务可让客户端通过 Binder
实现访问服务中的方法:
class LocalService : Service() {
// Binder given to clients
private val binder = LocalBinder()
// Random number generator
private val mGenerator = Random()
/** method for clients */
val randomNumber: Int
get() = mGenerator.nextInt(100)
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
inner class LocalBinder : Binder() {
// Return this instance of LocalService so clients can call public methods
fun getService(): LocalService = this@LocalService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
}
LocalBinder
为客户端提供 getService()
方法,从而检索 LocalService
的当前实例。这样,客户端便可调用服务中的公共方法。例如,客户端可调用服务中的 getRandomNumber()
。
点击按钮时,以下 Activity 会绑定到 LocalService
并调用 getRandomNumber()
:
class BindingActivity : Activity() {
private lateinit var mService: LocalService
private var mBound: Boolean = false
/** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
val binder = service as LocalService.LocalBinder
mService = binder.getService()
mBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
mBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
}
override fun onStart() {
super.onStart()
// Bind to LocalService
Intent(this, LocalService::class.java).also { intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
unbindService(connection)
mBound = false
}
/** Called when a button is clicked (the button in the layout file attaches to
* this method with the android:onClick attribute) */
fun onButtonClick(v: View) {
if (mBound) {
// Call a method from the LocalService.
// However, if this call were something that might hang, then this request should
// occur in a separate thread to avoid slowing down the activity performance.
val num: Int = mService.randomNumber
Toast.makeText(this, "number: $num", Toast.LENGTH_SHORT).show()
}
}
}
上例展示客户端如何使用 ServiceConnection
的实现和 onServiceConnected()
回调绑定到服务。下文将为您更详细地介绍绑定到服务的过程。
注意:在上例中,onStop()
方法取消了客户端与服务的绑定。如附加说明中所述,客户端应在适当时机取消与服务的绑定。
如需查看更多示例代码,请参阅 ApiDemos 中的 LocalService.java
类和 LocalServiceActivities.java
类。
使用 Messenger
如需让服务与远程进程通信,则可使用 Messenger
为您的服务提供接口。借助此方法,您无需使用 AIDL 便可执行进程间通信 (IPC)。
为接口使用 Messenger
比使用 AIDL 更简单,因为 Messenger
会将所有服务调用加入队列。纯 AIDL 接口会同时向服务发送多个请求,服务随后必须执行多线程处理。
对于大多数应用,服务无需执行多线程处理,因此使用 Messenger
即可让服务一次处理一个调用。如果您的服务必须执行多线程处理,请使用 AIDL 来定义接口。
以下是 Messenger
的使用方法摘要:
- 服务实现一个
Handler
,由该类为每个客户端调用接收回调。 - 服务使用
Handler
来创建Messenger
对象(对Handler
的引用)。 Messenger
创建一个IBinder
,服务通过onBind()
使其返回客户端。- 客户端使用
IBinder
将Messenger
(其引用服务的Handler
)实例化,然后使用后者将Message
对象发送给服务。 - 服务在其
Handler
中(具体地讲,是在handleMessage()
方法中)接收每个Message
。
这样,客户端便没有方法来调用服务。相反,客户端会传递服务在其 Handler
中接收的消息(Message
对象)。
以下简单服务实例展示如何使用 Messenger
接口:
/** Command to the service to display a message */
private const val MSG_SAY_HELLO = 1
class MessengerService : Service() {
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
private lateinit var mMessenger: Messenger
/**
* Handler of incoming messages from clients.
*/
internal class IncomingHandler(
context: Context,
private val applicationContext: Context = context.applicationContext
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_SAY_HELLO ->
Toast.makeText(applicationContext, "hello!", Toast.LENGTH_SHORT).show()
else -> super.handleMessage(msg)
}
}
}
/**
* When binding to the service, we return an interface to our messenger
* for sending messages to the service.
*/
override fun onBind(intent: Intent): IBinder? {
Toast.makeText(applicationContext, "binding", Toast.LENGTH_SHORT).show()
mMessenger = Messenger(IncomingHandler(this))
return mMessenger.binder
}
}
请注意,服务会在 Handler
的 handleMessage()
方法中接收传入的 Message
,并根据 what
成员决定下一步操作。
客户端只需根据服务返回的 IBinder
创建 Messenger
,然后利用 send()
发送消息。例如,以下简单 Activity 展示如何绑定到服务并向服务传递 MSG_SAY_HELLO
消息:
class ActivityMessenger : Activity() {
/** Messenger for communicating with the service. */
private var mService: Messenger? = null
/** Flag indicating whether we have called bind on the service. */
private var bound: Boolean = false
/**
* Class for interacting with the main interface of the service.
*/
private val mConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// This is called when the connection with the service has been
// established, giving us the object we can use to
// interact with the service. We are communicating with the
// service using a Messenger, so here we get a client-side
// representation of that from the raw IBinder object.
mService = Messenger(service)
bound = true
}
override fun onServiceDisconnected(className: ComponentName) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mService = null
bound = false
}
}
fun sayHello(v: View) {
if (!bound) return
// Create and send a message to the service, using a supported 'what' value
val msg: Message = Message.obtain(null, MSG_SAY_HELLO, 0, 0)
try {
mService?.send(msg)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
}
override fun onStart() {
super.onStart()
// Bind to the service
Intent(this, MessengerService::class.java).also { intent ->
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
// Unbind from the service
if (bound) {
unbindService(mConnection)
bound = false
}
}
}
请注意,此示例并未说明服务如何对客户端作出响应。如果您想让服务作出响应,还需在客户端中创建一个 Messenger
。收到 onServiceConnected()
回调时,客户端会向服务发送 Message
,并在其 send()
方法的 replyTo
参数中加入客户端的 Messenger
。
如需查看如何提供双向消息传递的示例,请参阅 MessengerService.java
(服务)和 MessengerServiceActivities.java
(客户端)示例。
绑定到服务
应用组件(客户端)可通过调用 bindService()
绑定到服务。然后,Android 系统会调用服务的 onBind()
方法,该方法会返回用于与服务交互的 IBinder
。
绑定为异步操作,并且 bindService()
无需将 IBinder
返回至客户端即可立即返回。如要接收 IBinder
,客户端必须创建一个 ServiceConnection
实例,并将其传递给 bindService()
。ServiceConnection
包含一个回调方法,系统通过调用该方法来传递 IBinder
。
注意:只有 Activity、服务和内容提供程序可以绑定到服务,您无法从广播接收器绑定到服务。
如要从您的客户端绑定到服务,请执行以下步骤:
-
实现
ServiceConnection
。
您的实现必须重写两个回调方法:
-
onServiceConnected()
系统会调用该方法,进而传递服务的
onBind()
方法所返回的IBinder
。 -
onServiceDisconnected()
当与服务的连接意外中断(例如服务崩溃或被终止)时,Android 系统会调用该方法。当客户端取消绑定时,系统不会调用该方法。
-
-
调用
bindService()
,从而传递
ServiceConnection
实现。
注意:如果方法返回“false”,则说明您的客户端未与服务进行有效连接。但是,您的客户端仍应调用
unbindService()
;否则,您的客户端将阻止服务在空闲时关闭。 -
当系统调用
onServiceConnected()
回调方法时,您可以使用接口定义的方法开始调用服务。 -
如要断开与服务的连接,请调用
unbindService()
。
当应用销毁客户端时,如果该客户端仍与服务保持绑定状态,则该销毁会导致客户端取消绑定。更好的做法是在客户端与服务交互完成后,立即取消与该客户端的绑定。这样可以关闭空闲服务。如需详细了解有关绑定和取消绑定的适当时机,请参阅附加说明。
以下示例通过扩展 Binder 类将客户端连接到上文创建的服务,因此它只需将返回的 IBinder
转换为 LocalService
类并请求 LocalService
实例:
var mService: LocalService
val mConnection = object : ServiceConnection {
// Called when the connection with the service is established
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// Because we have bound to an explicit
// service that is running in our own process, we can
// cast its IBinder to a concrete class and directly access it.
val binder = service as LocalService.LocalBinder
mService = binder.getService()
mBound = true
}
// Called when the connection with the service disconnects unexpectedly
override fun onServiceDisconnected(className: ComponentName) {
Log.e(TAG, "onServiceDisconnected")
mBound = false
}
}
如以下示例所示,客户端可将此 ServiceConnection
传递至 bindService()
,从而绑定到服务:
Intent(this, LocalService::class.java).also { intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
-
bindService()
的第一个参数是一个
Intent
,用于显式命名要绑定的服务。
注意:如果您使用 Intent 来绑定到
Service
,请务必使用显式 Intent 来确保应用的安全性。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务会响应该 Intent,并且用户无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用bindService()
,则系统会抛出异常。 -
第二个参数是
ServiceConnection
对象。 -
第三个参数是指示绑定选项的标记。如要创建尚未处于活动状态的服务,此参数应为
BIND_AUTO_CREATE
。其他可能的值为BIND_DEBUG_UNBIND
和BIND_NOT_FOREGROUND
,或者0
(表示无此参数)。
附加说明
以下是一些有关绑定到服务的重要说明:
-
您应该始终捕获
DeadObjectException
异常,系统会在连接中断时抛出此异常。这是远程方法抛出的唯一异常。 -
对象是跨进程计数的引用。
-
如以下示例所述,在匹配客户端生命周期的引入 (bring-up) 和退出 (tear-down) 时刻期间,您通常需配对绑定和取消绑定:
- 如果您只需在 Activity 可见时与服务交互,则应在
onStart()
期间进行绑定,在onStop()
期间取消绑定。 - 当 Activity 在后台处于停止运行状态时,若您仍希望其能接收响应,则可在
onCreate()
期间进行绑定,在onDestroy()
期间取消绑定。请注意,这意味着您的 Activity 在整个运行过程中(甚至包括后台运行期间)均需使用服务,因此如果服务位于其他进程内,则当您提高该进程的权重时,系统便更有可能会将其终止。
注意:通常情况下,不应在 Activity 的
onResume()
和onPause()
期间绑定和取消绑定,因为每次切换生命周期状态时都会发生这些回调,并且您应让这些转换期间的处理工作保持最少。此外,如果您将应用内的多个 Activity 绑定到同一个 Service,并且其中两个 Activity 之间发生了转换,则如果当前 Activity 在下一个 Activity 绑定(恢复期间)之前取消绑定(暂停期间),则系统可能会销毁并重建服务。如需了解 Activity 如何协调其生命周期的 Activity 转换,请参阅 Activity 文档。 - 如果您只需在 Activity 可见时与服务交互,则应在
如需查看更多显示如何绑定到服务的示例代码,请参阅 ApiDemos 中的 RemoteService.java
类。
管理绑定服务的生命周期
当取消服务与所有客户端之间的绑定时,Android 系统会销毁该服务(除非您还使用 onStartCommand()
启动了该服务)。因此,如果您的服务完全是绑定服务,则您无需管理其生命周期,Android 系统会根据它是否绑定到任何客户端代您管理。
不过,如果您选择实现 onStartCommand()
回调方法,则您必须显式停止服务,因为系统现已将其视为已启动状态。在此情况下,服务将一直运行,直到其通过 stopSelf()
自行停止,或其他组件调用 stopService()
(与该服务是否绑定到任何客户端无关)。
此外,如果您的服务已启动并接受绑定,则当系统调用您的 onUnbind()
方法时,如果您想在客户端下一次绑定到服务时接收 onRebind()
调用,则可选择返回 true
。onRebind()
返回空值,但客户端仍在其 onServiceConnected()
回调中接收 IBinder
。下图说明这种生命周期的逻辑。
图 1. 已启动且允许绑定的服务的生命周期。
Android 接口定义语言 (AIDL)
Android 接口定义语言 (AIDL) 与您可能使用过的其他接口语言 (IDL) 类似。您可以利用它定义客户端与服务均认可的编程接口,以便二者使用进程间通信 (IPC) 进行相互通信。在 Android 中,一个进程通常无法访问另一个进程的内存。因此,为进行通信,进程需将其对象分解成可供操作系统理解的原语,并将其编组为可供您操作的对象。编写执行该编组操作的代码较为繁琐,因此 Android 会使用 AIDL 为您处理此问题。
注意:只有在需要不同应用的客户端通过 IPC 方式访问服务,并且希望在服务中进行多线程处理时,您才有必要使用 AIDL。如果您无需跨不同应用执行并发 IPC,则应通过实现 Binder 来创建接口;或者,如果您想执行 IPC,但不需要处理多线程,请使用 Messenger 来实现接口。无论如何,在实现 AIDL 之前,请您务必理解绑定服务。
在开始设计 AIDL 接口之前,请注意,AIDL 接口的调用是直接函数调用。您无需对发生调用的线程做任何假设。实际情况的差异取决于调用是来自本地进程中的线程,还是远程进程中的线程。具体而言:
- 来自本地进程的调用在发起调用的同一线程内执行。如果该线程是您的主界面线程,则其将继续在 AIDL 接口中执行。如果该线程是其他线程,则其便是在服务中执行代码的线程。因此,只有在本地线程访问服务时,您才能完全控制哪些线程在服务中执行(但若出现此情况,您根本无需使用 AIDL,而应通过实现 Binder 类来创建接口)。
- 远程进程的调用分派自线程池,且平台会在您自己的进程内部维护该线程池。您必须为来自未知线程,且多次调用同时发生的传入调用做好准备。换言之,AIDL 接口的实现必须基于完全的线程安全。如果调用来自同一远程对象上的某个线程,则该调用将依次抵达接收器端。
oneway
关键字用于修改远程调用的行为。使用此关键字后,远程调用不会屏蔽,而只是发送事务数据并立即返回。最终接收该数据时,接口的实现会将其视为来自Binder
线程池的常规调用(普通的远程调用)。如果oneway
用于本地调用,则不会有任何影响,且调用仍为同步调用。
定义 AIDL 接口
您必须在 .aidl
文件中使用 Java 编程语言的语法定义 AIDL 接口,然后将其保存至应用的源代码(在 src/
目录中)内,这类应用会托管服务或与服务进行绑定。
在构建每个包含 .aidl
文件的应用时,Android SDK 工具会生成基于该 .aidl
文件的 IBinder
接口,并将其保存到项目的 gen/
目录中。服务必须视情况实现 IBinder
接口。然后,客户端应用便可绑定到该服务,并调用 IBinder
中的方法来执行 IPC。
如要使用 AIDL 创建绑定服务,请执行以下步骤:
-
创建 .aidl 文件
此文件定义带有方法签名的编程接口。
-
实现接口
Android SDK 工具会基于您的
.aidl
文件,使用 Java 编程语言生成接口。此接口拥有一个名为Stub
的内部抽象类,用于扩展Binder
类并实现 AIDL 接口中的方法。您必须扩展Stub
类并实现这些方法。 -
向客户端公开接口
实现
Service
并重写onBind()
,从而返回Stub
类的实现。
注意:如果您在首次发布 AIDL 接口后对其进行更改,则每次更改必须保持向后兼容性,以免中断其他应用使用您的服务。换言之,由于只有在将您的 .aidl
文件复制到其他应用后,才能使其访问服务接口,因而您必须保留对原始接口的支持。
1. 创建 .aidl 文件
AIDL 使用一种简单语法,允许您通过一个或多个方法(可接收参数和返回值)来声明接口。参数和返回值可为任意类型,甚至是 AIDL 生成的其他接口。
您必须使用 Java 编程语言构建 .aidl
文件。每个 .aidl
文件均须定义单个接口,并且只需要接口声明和方法签名。
默认情况下,AIDL 支持下列数据类型:
-
Java 编程语言中的所有原语类型(如
int
、long
、char
、boolean
等) -
String
-
CharSequence
-
List
List
中的所有元素必须是以上列表中支持的数据类型,或者您所声明的由 AIDL 生成的其他接口或 Parcelable 类型。您可选择将List
用作“泛型”类(例如,List<String>
)。尽管生成的方法旨在使用List
接口,但另一方实际接收的具体类始终是ArrayList
。 -
Map
Map
中的所有元素必须是以上列表中支持的数据类型,或者您所声明的由 AIDL 生成的其他接口或 Parcelable 类型。不支持泛型 Map(如Map<String,Integer>
形式的 Map)。尽管生成的方法旨在使用Map
接口,但另一方实际接收的具体类始终是HashMap
。
即使您在与接口相同的包内定义上方未列出的附加类型,亦须为其各自加入一条 import
语句。
定义服务接口时,请注意:
-
方法可带零个或多个参数,返回值或空值。
-
所有非原语参数均需要指示数据走向的方向标记。这类标记可以是
in
、
out
或
inout
(见下方示例)。
原语默认为
in
,不能是其他方向。注意:您应将方向限定为真正需要的方向,因为编组参数的开销较大。
-
生成的
IBinder
接口内包含.aidl
文件中的所有代码注释(import 和 package 语句之前的注释除外)。 -
您可以在 ADL 接口中定义 String 常量和 int 字符串常量。例如:
const int VERSION = 1;
。 -
方法调用由 [transact() 代码](https://developer.android.com/reference/android/os/IBinder#transact(int, android.os.Parcel, android.os.Parcel, int))分派,该代码通常基于接口中的方法索引。由于这会增加版本控制的难度,因此您可以向方法手动配置事务代码:
void method() = 10;
。 -
使用
@nullable
注释可空参数或返回类型。
以下是 .aidl
文件示例:
// IRemoteService.aidl
package com.example.android
// Declare any non-default types here with import statements
/** Example service interface */
internal interface IRemoteService {
/** Request the process ID of this service, to do evil things with it. */
val pid:Int
/** Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
fun basicTypes(anInt:Int, aLong:Long, aBoolean:Boolean, aFloat:Float,
aDouble:Double, aString:String)
}
您只需将 .aidl
文件保存至项目的 src/
目录内,这样在构建应用时,SDK 工具便会在项目的 gen/
目录中生成 IBinder
接口文件。生成文件的名称与 .aidl
文件的名称保持一致,区别在于其使用 .java
扩展名(例如,IRemoteService.aidl
生成的文件名是 IRemoteService.java
)。
如果您使用 Android Studio,增量构建几乎会立即生成 Binder 类。如果您不使用 Android Studio,则 Gradle 工具会在您下一次开发应用时生成 Binder 类。因此,在编写完 .aidl
文件后,您应立即使用 gradle assembleDebug
(或 gradle assembleRelease
)构建项目,以便您的代码能够链接到生成的类。
2. 实现接口
当您构建应用时,Android SDK 工具会生成以 .aidl
文件命名的 .java
接口文件。生成的接口包含一个名为 Stub
的子类(例如,YourInterface.Stub
),该子类是其父接口的抽象实现,并且会声明 .aidl
文件中的所有方法。
注意:Stub
还会定义几个辅助方法,其中最值得注意的是 asInterface()
,该方法会接收 IBinder
(通常是传递给客户端 onServiceConnected()
回调方法的参数),并返回 Stub 接口的实例。如需了解如何进行此转换的更多详情,请参阅调用 IPC 方法部分。
如要实现 .aidl
生成的接口,请扩展生成的 Binder
接口(例如,YourInterface.Stub
),并实现继承自 .aidl
文件的方法。
以下示例展示使用匿名实例实现 IRemoteService
接口(由以上 IRemoteService.aidl
示例定义)的过程:
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int =
Process.myPid()
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// Does nothing
}
}
现在,binder
是 Stub
类的一个实例(一个 Binder
),其定义了服务的远程过程调用 (RPC) 接口。在下一步中,我们会向客户端公开此实例,以便客户端能与服务进行交互。
在实现 AIDL 接口时,您应注意遵守以下规则:
- 由于无法保证在主线程上执行传入调用,因此您一开始便需做好多线程处理的准备,并对您的服务进行适当构建,使其达到线程安全的标准。
- 默认情况下,RPC 调用是同步调用。如果您知道服务完成请求的时间不止几毫秒,则不应从 Activity 的主线程调用该服务,因为这可能会使应用挂起(Android 可能会显示“Application is Not Responding”对话框)— 通常,您应从客户端内的单独线程调用服务。
- 您引发的任何异常都不会回传给调用方。
3. 向客户端公开接口
在为服务实现接口后,您需要向客户端公开该接口,以便客户端进行绑定。如要为您的服务公开该接口,请扩展 Service
并实现 onBind()
,从而返回实现生成的 Stub
的类实例(如前文所述)。以下是向客户端公开 IRemoteService
示例接口的服务示例。
class RemoteService : Service() {
override fun onCreate() {
super.onCreate()
}
override fun onBind(intent: Intent): IBinder {
// Return the interface
return binder
}
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int {
return Process.myPid()
}
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// Does nothing
}
}
}
现在,当客户端(如 Activity)调用 bindService()
以连接此服务时,客户端的 onServiceConnected()
回调会接收服务的 onBind()
方法所返回的 binder
实例。
客户端还必须拥有接口类的访问权限,因此如果客户端和服务在不同应用内,则客户端应用的 src/
目录内必须包含 .aidl
文件(该文件会生成 android.os.Binder
接口,进而为客户端提供 AIDL 方法的访问权限)的副本。
当客户端在 onServiceConnected()
回调中收到 IBinder
时,它必须调用 *YourServiceInterface*.Stub.asInterface(service)
,以将返回的参数转换成 *YourServiceInterface*
类型。例如:
var iRemoteService: IRemoteService? = null
val mConnection = object : ServiceConnection {
// Called when the connection with the service is established
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// Following the example above for an AIDL interface,
// this gets an instance of the IRemoteInterface, which we can use to call on the service
iRemoteService = IRemoteService.Stub.asInterface(service)
}
// Called when the connection with the service disconnects unexpectedly
override fun onServiceDisconnected(className: ComponentName) {
Log.e(TAG, "Service has unexpectedly disconnected")
iRemoteService = null
}
}
通过 IPC 传递对象
您可以通过 IPC 接口,将某个类从一个进程发送至另一个进程。不过,您必须确保 IPC 通道的另一端可使用该类的代码,并且该类必须支持 Parcelable
接口。支持 Parcelable
接口很重要,因为 Android 系统能通过该接口将对象分解成可编组至各进程的原语。
如要创建支持 Parcelable
协议的类,您必须执行以下操作:
-
让您的类实现
Parcelable
接口。 -
实现
writeToParcel
,它会获取对象的当前状态并将其写入Parcel
。 -
为您的类添加
CREATOR
静态字段,该字段是实现Parcelable.Creator
接口的对象。 -
最后,创建声明 Parcelable 类的.aidl文件(遵照下文Rect.aidl文件所示步骤)。
如果您使用的是自定义编译进程,请勿在您的构建中添加
.aidl
文件。此.aidl
文件与 C 语言中的头文件类似,并未经过编译。
AIDL 会在其生成的代码中使用这些方法和字段,以对您的对象进行编组和解编。
例如,下方的 Rect.aidl
文件可创建 Parcelable 类型的 Rect
类:
package android.graphics;
// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;
以下示例展示 Rect
类如何实现 Parcelable
协议。
https://developer.android.com/guide/components/aidl#java-java)
import android.os.Parcel
import android.os.Parcelable
class Rect() : Parcelable {
var left: Int = 0
var top: Int = 0
var right: Int = 0
var bottom: Int = 0
companion object CREATOR : Parcelable.Creator<Rect> {
override fun createFromParcel(parcel: Parcel): Rect {
return Rect(parcel)
}
override fun newArray(size: Int): Array<Rect> {
return Array(size) { Rect() }
}
}
private constructor(inParcel: Parcel) : this() {
readFromParcel(inParcel)
}
override fun writeToParcel(outParcel: Parcel, flags: Int) {
outParcel.writeInt(left)
outParcel.writeInt(top)
outParcel.writeInt(right)
outParcel.writeInt(bottom)
}
private fun readFromParcel(inParcel: Parcel) {
left = inParcel.readInt()
top = inParcel.readInt()
right = inParcel.readInt()
bottom = inParcel.readInt()
}
override fun describeContents(): Int {
return 0
}
}
Rect
类中的编组相当简单。请查看 Parcel
的其他相关方法,了解您可以向 Parcel 写入哪些其他类型的值。
警告:请勿忘记从其他进程中接收数据的安全问题。在本例中,Rect
从 Parcel
读取四个数字,但您需确保:无论调用方目的为何,这些数字均在可接受的值范围内。如需详细了解如何防止应用受到恶意软件侵害、保证应用安全,请参阅安全与权限。
带软件包参数(包含 Parcelable 类型)的方法
如果您的 AIDL 接口包含接收软件包作为参数(预计包含 Parcelable 类型)的方法,则在尝试从软件包读取之前,请务必通过调用 Bundle.setClassLoader(ClassLoader)
设置软件包的类加载器。否则,即使您在应用中正确定义 Parcelable 类型,也会遇到 ClassNotFoundException
。例如,
如果您有 .aidl
文件:
// IRectInsideBundle.aidl
package com.example.android;
/** Example service interface */
interface IRectInsideBundle {
/** Rect parcelable is stored in the bundle with key "rect" */
void saveRect(in Bundle bundle);
}
如下方实现所示,在读取 Rect
之前,ClassLoader
已在 Bundle
中完成显式设置
private val binder = object : IRectInsideBundle.Stub() {
override fun saveRect(bundle: Bundle) {
bundle.classLoader = classLoader
val rect = bundle.getParcelable<Rect>("rect")
process(rect) // Do more with the parcelable
}
}
调用 IPC 方法
如要调用通过 AIDL 定义的远程接口,调用类必须执行以下步骤:
- 在项目的
src/
目录中加入.aidl
文件。 - 声明一个
IBinder
接口实例(基于 AIDL 生成)。 - 实现
ServiceConnection
。 - 调用
Context.bindService()
,从而传入您的ServiceConnection
实现。 - 在
onServiceConnected()
实现中,您将收到一个IBinder
实例(名为service
)。调用*YourInterfaceName*.Stub.asInterface((IBinder)*service*)
,以将返回的参数转换为 YourInterface 类型。 - 调用您在接口上定义的方法。您应始终捕获
DeadObjectException
异常,系统会在连接中断时抛出此异常。您还应捕获SecurityException
异常,当 IPC 方法调用中两个进程的 AIDL 定义发生冲突时,系统会抛出此异常。 - 如要断开连接,请使用您的接口实例调用
Context.unbindService()
。
有关调用 IPC 服务的几点说明:
- 对象是跨进程计数的引用。
- 您可以方法参数的形式发送匿名对象。
如需了解有关绑定服务的详细信息,请阅读绑定服务文档。
以下示例代码摘自 ApiDemos 项目的远程服务示例,展示如何调用 AIDL 创建的服务。
private const val BUMP_MSG = 1
class Binding : Activity() {
/** The primary interface we will be calling on the service. */
private var mService: IRemoteService? = null
/** Another interface we use on the service. */
internal var secondaryService: ISecondary? = null
private lateinit var killButton: Button
private lateinit var callbackText: TextView
private lateinit var handler: InternalHandler
private var isBound: Boolean = false
/**
* Class for interacting with the main interface of the service.
*/
private val mConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// This is called when the connection with the service has been
// established, giving us the service object we can use to
// interact with the service. We are communicating with our
// service through an IDL interface, so get a client-side
// representation of that from the raw service object.
mService = IRemoteService.Stub.asInterface(service)
killButton.isEnabled = true
callbackText.text = "Attached."
// We want to monitor the service for as long as we are
// connected to it.
try {
mService?.registerCallback(mCallback)
} catch (e: RemoteException) {
// In this case the service has crashed before we could even
// do anything with it; we can count on soon being
// disconnected (and then reconnected if it can be restarted)
// so there is no need to do anything here.
}
// As part of the sample, tell the user what happened.
Toast.makeText(
this@Binding,
R.string.remote_service_connected,
Toast.LENGTH_SHORT
).show()
}
override fun onServiceDisconnected(className: ComponentName) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mService = null
killButton.isEnabled = false
callbackText.text = "Disconnected."
// As part of the sample, tell the user what happened.
Toast.makeText(
this@Binding,
R.string.remote_service_disconnected,
Toast.LENGTH_SHORT
).show()
}
}
/**
* Class for interacting with the secondary interface of the service.
*/
private val secondaryConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// Connecting to a secondary interface is the same as any
// other interface.
secondaryService = ISecondary.Stub.asInterface(service)
killButton.isEnabled = true
}
override fun onServiceDisconnected(className: ComponentName) {
secondaryService = null
killButton.isEnabled = false
}
}
private val mBindListener = View.OnClickListener {
// Establish a couple connections with the service, binding
// by interface names. This allows other applications to be
// installed that replace the remote service by implementing
// the same interface.
val intent = Intent(this@Binding, RemoteService::class.java)
intent.action = IRemoteService::class.java.name
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
intent.action = ISecondary::class.java.name
bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE)
isBound = true
callbackText.text = "Binding."
}
private val unbindListener = View.OnClickListener {
if (isBound) {
// If we have received the service, and hence registered with
// it, then now is the time to unregister.
try {
mService?.unregisterCallback(mCallback)
} catch (e: RemoteException) {
// There is nothing special we need to do if the service
// has crashed.
}
// Detach our existing connection.
unbindService(mConnection)
unbindService(secondaryConnection)
killButton.isEnabled = false
isBound = false
callbackText.text = "Unbinding."
}
}
private val killListener = View.OnClickListener {
// To kill the process hosting our service, we need to know its
// PID. Conveniently our service has a call that will return
// to us that information.
try {
secondaryService?.pid?.also { pid ->
// Note that, though this API allows us to request to
// kill any process based on its PID, the kernel will
// still impose standard restrictions on which PIDs you
// are actually able to kill. Typically this means only
// the process running your application and any additional
// processes created by that app as shown here; packages
// sharing a common UID will also be able to kill each
// other's processes.
Process.killProcess(pid)
callbackText.text = "Killed service process."
}
} catch (ex: RemoteException) {
// Recover gracefully from the process hosting the
// server dying.
// Just for purposes of the sample, put up a notification.
Toast.makeText(this@Binding, R.string.remote_call_failed, Toast.LENGTH_SHORT).show()
}
}
// ----------------------------------------------------------------------
// Code showing how to deal with callbacks.
// ----------------------------------------------------------------------
/**
* This implementation is used to receive callbacks from the remote
* service.
*/
private val mCallback = object : IRemoteServiceCallback.Stub() {
/**
* This is called by the remote service regularly to tell us about
* new values. Note that IPC calls are dispatched through a thread
* pool running in each process, so the code executing here will
* NOT be running in our main thread like most other things -- so,
* to update the UI, we need to use a Handler to hop over there.
*/
override fun valueChanged(value: Int) {
handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0))
}
}
/**
* Standard initialization of this activity. Set up the UI, then wait
* for the user to poke it before doing anything.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.remote_service_binding)
// Watch for button clicks.
var button: Button = findViewById(R.id.bind)
button.setOnClickListener(mBindListener)
button = findViewById(R.id.unbind)
button.setOnClickListener(unbindListener)
killButton = findViewById(R.id.kill)
killButton.setOnClickListener(killListener)
killButton.isEnabled = false
callbackText = findViewById(R.id.callback)
callbackText.text = "Not attached."
handler = InternalHandler(callbackText)
}
private class InternalHandler(
textView: TextView,
private val weakTextView: WeakReference<TextView> = WeakReference(textView)
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
BUMP_MSG -> weakTextView.get()?.text = "Received from service: ${msg.arg1}"
else -> super.handleMessage(msg)
}
}
}
}
后台优化
后台进程可能会耗费大量内存和电池电量。例如,某一隐式广播可能会启动许多已注册监听它的后台进程,即使这些进程可能并没有执行很多任务。这会对设备性能和用户体验产生重大影响。
为缓解此问题,Android 7.0(API 级别 24)施加了以下限制:
- 如果以 Android 7.0(API 级别 24)及更高版本为目标平台的应用在清单中声明其广播接收器,则它们不会收到
CONNECTIVITY_ACTION
广播。如果应用使用Context.registerReceiver()
注册BroadcastReceiver
且该 context 仍有效,则它们仍会收到CONNECTIVITY_ACTION
广播。 - 应用无法发送或接收
ACTION_NEW_PICTURE
或ACTION_NEW_VIDEO
广播。此项优化会影响所有应用,而不仅仅是那些以 Android 7.0(API 级别 24)为目标平台的应用。
如果您的应用使用了其中的任何 intent,您应该尽快取消对它们的依赖,以便正确定位到搭载 Android 7.0 或更高版本的设备。Android 框架提供了多种解决方案来缓解对这些隐式广播的需求。例如,JobScheduler
和新的 WorkManager 提供了强大的机制,可在满足指定条件时(例如连接到不按流量计费网络时)调度网络操作。现在,您还可以使用 JobScheduler
来响应对内容提供程序的更改。JobInfo
对象可封装 JobScheduler
用来调度作业的参数。当满足作业条件时,系统会在应用的 JobService
上执行此作业。
在本文档中,我们将学习如何使用 JobScheduler
之类的替代方法让您的应用适应这些新限制。
用户发起的限制
从 Android 9(API 级别 28)开始,如果应用出现 Android Vitals 中描述的一些不良行为,系统会提示用户限制该应用对系统资源的访问。
如果系统发现应用消耗的资源过多,则会通知用户,并为用户提供限制应用操作的选项。 可触发此类通知的行为包括:
- 唤醒锁定过多:屏幕关闭时持续 1 小时的 1 次部分唤醒锁定
- 后台服务过多:应用的目标 API 级别低于 26 且后台服务过多
施加的确切限制由设备制造商决定。例如,在 AOSP 版本中,受限应用不能运行作业、触发闹钟或使用网络,除非该应用在前台运行。(有关确定应用是否位于前台的条件,请参阅后台服务限制。)电源管理限制中列出了具体的限制。
对接收网络活动广播的限制
如果以 Android 7.0(API 级别 24)为目标平台的应用在其清单中注册接收 CONNECTIVITY_ACTION
广播,则不会收到该广播,且依赖于此广播的进程将不会启动。如果应用需要监听网络更改或在设备连接到不按流量计费的网络时执行批量网络活动,这种限制可能会给这些应用带来问题。Android 框架中已经存在多种解决此限制的解决方案,但您需要根据希望应用完成的操作选择合适的解决方案。
注意:当应用正在运行时,通过 Context.registerReceiver()
注册的 BroadcastReceiver
会继续收到这些广播。
连接到不按流量计费的网络时调度网络作业
使用 JobInfo.Builder
类构建 JobInfo
对象时,应用 setRequiredNetworkType()
方法并将 JobInfo.NETWORK_TYPE_UNMETERED
作为作业参数传递。以下代码示例调度一项服务,使其在设备连接到不按流量计费的网络且正在充电时运行:
const val MY_BACKGROUND_JOB = 0
...
fun scheduleJob(context: Context) {
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val job = JobInfo.Builder(
MY_BACKGROUND_JOB,
ComponentName(context, MyJobService::class.java)
)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.setRequiresCharging(true)
.build()
jobScheduler.schedule(job)
}
当满足作业条件时,您的应用会收到一个回调,以运行指定的 JobService.class
中的 onStartJob()
方法。要查看更多 JobScheduler
实现示例,请参阅 JobScheduler 示例应用。
JobStessduler 的一个新替代工具是 JobManager,这个 API 可用来调度无论应用进程是否存在都需要保证完成的后台任务。WorkManager 根据设备 API 级别等因素选择运行工作的适当方式(直接在应用进程中的线程上以及使用 JobScheduler、FirebaseJobDispatcher 或 AlarmManager)。此外,WorkManager 不需要 Play 服务,并且提供多项高级功能,例如将任务链接在一起或检查任务状态。要了解详情,请参阅 WorkManager。
在应用运行时监控网络连接
正在运行的应用仍然可以通过已注册的 BroadcastReceiver
监听 CONNECTIVITY_CHANGE
。不过,ConnectivityManager
API 提供一个更强大的方法,用于仅在满足指定的网络条件时请求回调。
NetworkRequest
对象在 NetworkCapabilities
方面定义网络回调的参数。您需要使用 NetworkRequest.Builder
类创建 NetworkRequest
对象。registerNetworkCallback()
随后将 NetworkRequest
对象传递给系统。满足网络条件时,应用会收到回调,以执行其 ConnectivityManager.NetworkCallback
类中定义的 onAvailable()
方法。
在应用退出或调用 unregisterNetworkCallback()
之前,应用会继续接收回调。
对接收图片和视频广播的限制
在 Android 7.0(API 级别 24)中,应用无法发送或接收 ACTION_NEW_PICTURE
或 ACTION_NEW_VIDEO
广播。当系统必须唤醒多个应用来处理新的图片或视频时,此限制有助于减轻对性能和用户体验的影响。Android 7.0(API 级别 24)通过扩展 JobInfo
和 JobParameters
提供了替代解决方案。
内容 URI 发生更改时触发作业
为了在内容 URI 发生更改时触发作业,Android 7.0(API 级别 24)扩展了 JobInfo
API 并提供了以下方法:
-
JobInfo.TriggerContentUri()
用于封装当内容 URI 发生更改时触发作业所需的参数。
-
JobInfo.Builder.addTriggerContentUri()
将
TriggerContentUri
对象传递给JobInfo
。ContentObserver
会监控封装的内容 URI。如果作业有多个关联的TriggerContentUri
对象,即使系统仅报告了其中一个内容 URI 发生变化,它也会提供回调。添加
TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
标记以在特定 URI 的任何子项发生更改时触发作业。此标记对应于传递到registerContentObserver()
的notifyForDescendants
参数。
注意:TriggerContentUri()
不能与 setPeriodic()
或 setPersisted()
结合使用。要持续监控内容更改,请在应用 JobService
处理完最新回调之前调度新的 JobInfo
。
以下示例代码会调度一项作业,使其在系统报告内容 URI MEDIA_URI
发生更改时触发:
const val MY_BACKGROUND_JOB = 0
...
fun scheduleJob(context: Context) {
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val job = JobInfo.Builder(
MY_BACKGROUND_JOB,
ComponentName(context, MediaContentJob::class.java)
)
.addTriggerContentUri(
JobInfo.TriggerContentUri(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
)
)
.build()
jobScheduler.schedule(job)
}
当系统报告指定内容 URI 发生更改时,您的应用会收到回调,并且会有一个 JobParameters
对象传递给 MediaContentJob.class
中的 onStartJob()
方法。
确定哪些内容授权方触发了作业
Android 7.0(API 级别 24)还扩展了 JobParameters
,以允许您的应用接收有关哪些内容方和 URI 触发了该作业的有用信息:
-
Uri[] getTriggeredContentUris()
返回已触发作业的 URI 数组。如果没有 URI 触发作业(例如,作业因截止日期或某种其他原因而触发),或发生更改的 URI 数量大于 50,则该参数为
null
。 -
String[] getTriggeredContentAuthorities()
返回已触发作业的内容授权方的字符串数组。如果返回的数组不是
null
,请使用getTriggeredContentUris()
检索关于哪些 URI 发生了更改的详细信息。
以下示例代码会替换 JobService.onStartJob()
方法并记录已触发作业的内容授权方和 URI:
override fun onStartJob(params: JobParameters): Boolean {
StringBuilder().apply {
append("Media content has changed:\n")
params.triggeredContentAuthorities?.also { authorities ->
append("Authorities: ${authorities.joinToString(", ")}\n")
append(params.triggeredContentUris?.joinToString("\n"))
} ?: append("(No content)")
Log.i(TAG, toString())
}
return true
}
进一步优化应用
可以对应用进行优化,使其能够在内存不足的设备上或在内存不足的条件下运行,从而改善性能和用户体验。如果取消对后台服务和在清单中注册的隐式广播接收器的依赖,则有助于提高您的应用在此类设备上的运行效率。尽管 Android 7.0(API 级别 24)采取了措施来减少其中的一些问题,但我们建议您优化应用,使其能够在完全不使用这些后台进程的情况下运行。
Android 7.0(API 级别 24)引入了一些额外的 Android 调试桥 (ADB) 命令,供您在这些后台进程停用的情况下测试应用行为:
-
要模拟隐式广播和后台服务不可用的条件,请输入以下命令:
-
$ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND ignore
-
要重新启用隐式广播和后台服务,请输入以下命令:
-
$ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND allow
广播概览
Android 应用与 Android 系统和其他 Android 应用之间可以相互收发广播消息,这与发布-订阅设计模式相似。这些广播会在所关注的事件发生时发送。举例来说,Android 系统会在发生各种系统事件时发送广播,例如系统启动或设备开始充电时。再比如,应用可以发送自定义广播来通知其他应用它们可能感兴趣的事件(例如,一些新数据已下载)。
应用可以注册接收特定的广播。广播发出后,系统会自动将广播传送给同意接收这种广播的应用。
一般来说,广播可作为跨应用和普通用户流之外的消息传递系统。但是,您必须小心,不要滥用在后台响应广播和运行作业的机会,因为这会导致系统变慢,具体如以下视频所述。
关于系统广播
系统会在发生各种系统事件时自动发送广播,例如当系统进入和退出飞行模式时。系统广播会被发送给所有同意接收相关事件的应用。
广播消息本身会被封装在一个 Intent
对象中,该对象的操作字符串会标识所发生的事件(例如 android.intent.action.AIRPLANE_MODE
)。该 Intent 可能还包含绑定到其 extra 字段中的附加信息。例如,飞行模式 intent 包含布尔值 extra 来指示是否已开启飞行模式。
如需详细了解如何读取 intent 并从 intent 中获取操作字符串,请参阅 Intent 和 Intent 过滤器。
有关系统广播操作的完整列表,请参阅 Android SDK 中的 BROADCAST_ACTIONS.TXT
文件。每个广播操作都有一个与之关联的常量字段。例如,常量 ACTION_AIRPLANE_MODE_CHANGED
的值为 android.intent.action.AIRPLANE_MODE
。每个广播操作的文档都可以在关联的常量字段中找到。
系统广播所发生的更改
随着 Android 平台的发展,它会不定期地更改系统广播的行为方式。如果您的应用以 Android 7.0(API 级别 24)或更高版本为目标平台,或者安装在搭载 Android 7.0 或更高版本的设备上,请注意以下更改。
Android 9
从 Android 9(API 级别 28)开始,NETWORK_STATE_CHANGED_ACTION
广播不再接收有关用户位置或个人身份数据的信息。
此外,如果您的应用安装在搭载 Android 9 或更高版本的设备上,则通过 WLAN 接收的系统广播不包含 SSID、BSSID、连接信息或扫描结果。要获取这些信息,请调用 getConnectionInfo()
。
Android 8.0
从 Android 8.0(API 级别 26)开始,系统对清单声明的接收器施加了额外的限制。
如果您的应用以 Android 8.0 或更高版本为目标平台,那么对于大多数隐式广播(没有明确针对您的应用的广播),您不能使用清单来声明接收器。当用户正在活跃地使用您的应用时,您仍可使用上下文注册的接收器。
Android 7.0
Android 7.0(API 级别 24)及更高版本不发送以下系统广播:
ACTION_NEW_PICTURE
ACTION_NEW_VIDEO
此外,以 Android 7.0 及更高版本为目标平台的应用必须使用 registerReceiver(BroadcastReceiver, IntentFilter)
注册 CONNECTIVITY_ACTION
广播。无法在清单中声明接收器。
接收广播
应用可以通过两种方式接收广播:清单声明的接收器和上下文注册的接收器。
清单声明的接收器
如果您在清单中声明广播接收器,系统会在广播发出后启动您的应用(如果应用尚未运行)。
注意:如果您的应用以 API 级别 26 或更高级别的平台版本为目标,则不能使用清单为隐式广播(没有明确针对您的应用的广播)声明接收器,但一些不受此限制的隐式广播除外。在大多数情况下,您可以使用调度作业来代替。
要在清单中声明广播接收器,请执行以下步骤:
-
在应用清单中指定 `` 元素。
-
<receiver android:name=".MyBroadcastReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.INPUT_METHOD_CHANGED" /> </intent-filter> </receiver>
Intent 过滤器指定您的接收器所订阅的广播操作。
-
创建
BroadcastReceiver
子类并实现onReceive(Context, Intent)
。以下示例中的广播接收器会记录并显示广播的内容:private const val TAG = "MyBroadcastReceiver" class MyBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { StringBuilder().apply { append("Action: ${intent.action}\n") append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n") toString().also { log -> Log.d(TAG, log) Toast.makeText(context, log, Toast.LENGTH_LONG).show() } } } }
系统软件包管理器会在应用安装时注册接收器。然后,该接收器会成为应用的一个独立入口点,这意味着如果应用当前未运行,系统可以启动应用并发送广播。
系统会创建新的 BroadcastReceiver
组件对象来处理它接收到的每个广播。此对象仅在调用 onReceive(Context, Intent)
期间有效。一旦从此方法返回代码,系统便会认为该组件不再活跃。
上下文注册的接收器
要使用上下文注册接收器,请执行以下步骤:
-
创建
BroadcastReceiver
的实例。val br: BroadcastReceiver = MyBroadcastReceiver()
-
创建
IntentFilter
并调用registerReceiver(BroadcastReceiver, IntentFilter)
来注册接收器:val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply { addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED) } registerReceiver(br, filter)
注意:要注册本地广播,请调用
LocalBroadcastManager.registerReceiver(BroadcastReceiver, IntentFilter)
。只要注册上下文有效,上下文注册的接收器就会接收广播。例如,如果您在
Activity
上下文中注册,只要 Activity 没有被销毁,您就会收到广播。如果您在应用上下文中注册,只要应用在运行,您就会收到广播。 -
要停止接收广播,请调用
unregisterReceiver(android.content.BroadcastReceiver)
。当您不再需要接收器或上下文不再有效时,请务必注销接收器。请注意注册和注销接收器的位置,比方说,如果您使用 Activity 上下文在
onCreate(Bundle)
中注册接收器,则应在onDestroy()
中注销,以防接收器从 Activity 上下文中泄露出去。如果您在onResume()
中注册接收器,则应在onPause()
中注销,以防多次注册接收器(如果您不想在暂停时接收广播,这样可以减少不必要的系统开销)。请勿在onSaveInstanceState(Bundle)
中注销,因为如果用户在历史记录堆栈中后退,则不会调用此方法。
对进程状态的影响
BroadcastReceiver
的状态(无论它是否在运行)会影响其所在进程的状态,而其所在进程的状态又会影响它被系统终结的可能性。例如,当进程执行接收器(即当前在运行其 onReceive()
方法中的代码)时,它被认为是前台进程。除非遇到极大的内存压力,否则系统会保持该进程运行。
但是,一旦从 onReceive()
返回代码,BroadcastReceiver 就不再活跃。接收器的宿主进程变得与在其中运行的其他应用组件一样重要。如果该进程仅托管清单声明的接收器(这对于用户从未与之互动或最近没有与之互动的应用很常见),则从 onReceive()
返回时,系统会将其进程视为低优先级进程,并可能会将其终止,以便将资源提供给其他更重要的进程使用。
因此,您不应从广播接收器启动长时间运行的后台线程。onReceive()
完成后,系统可以随时终止进程来回收内存,在此过程中,也会终止进程中运行的派生线程。要避免这种情况,您应该调用 goAsync()
(如果您希望在后台线程中多花一点时间来处理广播)或者使用 JobScheduler
从接收器调度 JobService
,这样系统就会知道该进程将继续活跃地工作。如需了解详情,请参阅进程和应用生命周期。
以下代码段展示了一个 BroadcastReceiver
,它使用 goAsync()
来标记它在 onReceive()
完成后需要更多时间才能完成。如果您希望在 onReceive()
中完成的工作很长,足以导致界面线程丢帧 (>16ms),则这种做法非常有用,这使它尤其适用于后台线程。
private const val TAG = "MyBroadcastReceiver"
class MyBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val pendingResult: PendingResult = goAsync()
val asyncTask = Task(pendingResult, intent)
asyncTask.execute()
}
private class Task(
private val pendingResult: PendingResult,
private val intent: Intent
) : AsyncTask<String, Int, String>() {
override fun doInBackground(vararg params: String?): String {
val sb = StringBuilder()
sb.append("Action: ${intent.action}\n")
sb.append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
return toString().also { log ->
Log.d(TAG, log)
}
}
override fun onPostExecute(result: String?) {
super.onPostExecute(result)
// Must call finish() so the BroadcastReceiver can be recycled.
pendingResult.finish()
}
}
}
发送广播
Android 为应用提供三种方式来发送广播:
sendOrderedBroadcast(Intent, String)
方法一次向一个接收器发送广播。当接收器逐个顺序执行时,接收器可以向下传递结果,也可以完全中止广播,使其不再传递给其他接收器。接收器的运行顺序可以通过匹配的 intent-filter 的 android:priority 属性来控制;具有相同优先级的接收器将按随机顺序运行。sendBroadcast(Intent)
方法会按随机的顺序向所有接收器发送广播。这称为常规广播。这种方法效率更高,但也意味着接收器无法从其他接收器读取结果,无法传递从广播中收到的数据,也无法中止广播。LocalBroadcastManager.sendBroadcast
方法会将广播发送给与发送器位于同一应用中的接收器。如果您不需要跨应用发送广播,请使用本地广播。这种实现方法的效率更高(无需进行进程间通信),而且您无需担心其他应用在收发您的广播时带来的任何安全问题。
以下代码段展示了如何通过创建 Intent 并调用 sendBroadcast(Intent)
来发送广播。
Intent().also { intent ->
intent.setAction("com.example.broadcast.MY_NOTIFICATION")
intent.putExtra("data", "Notice me senpai!")
sendBroadcast(intent)
}
广播消息封装在 Intent
对象中。Intent 的操作字符串必须提供应用的 Java 软件包名称语法,并唯一标识广播事件。您可以使用 putExtra(String, Bundle)
向 intent 附加其他信息。您也可以对 intent 调用 setPackage(String)
,将广播限定到同一组织中的一组应用。
注意:虽然 intent 既用于发送广播,也用于通过 startActivity(Intent)
启动 Activity,但这两种操作是完全无关的。广播接收器无法查看或捕获用于启动 Activity 的 intent;同样,当您广播 intent 时,也无法找到或启动 Activity。
通过权限限制广播
您可以通过权限将广播限定到拥有特定权限的一组应用。您可以对广播的发送器或接收器施加限制。
带权限的发送
当您调用 sendBroadcast(Intent, String)
或 sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
时,可以指定权限参数。接收器若要接收此广播,则必须通过其清单中的 标记请求该权限(如果存在危险,则会被授予该权限)。例如,以下代码会发送广播:
sendBroadcast(Intent("com.example.NOTIFY"), Manifest.permission.SEND_SMS)
要接收此广播,接收方应用必须请求如下权限:
<uses-permission android:name="android.permission.SEND_SMS"/>
您可以指定现有的系统权限(如 SEND_SMS
),也可以使用 `` 元素定义自定义权限。有关权限和安全性的一般信息,请参阅系统权限。
注意:自定义权限将在安装应用时注册。定义自定义权限的应用必须在使用自定义权限的应用之前安装。
带权限的接收
如果您在注册广播接收器时指定了权限参数(通过 registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)
或清单中的 ](https://developer.android.com/guide/topics/manifest/receiver-element) 标记指定),则广播方必须通过其清单中的 [
标记请求该权限(如果存在危险,则会被授予该权限),才能向该接收器发送 Intent。
例如,假设您的接收方应用具有如下所示的清单声明的接收器:
<receiver android:name=".MyBroadcastReceiver"
android:permission="android.permission.SEND_SMS">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE"/>
</intent-filter>
</receiver>
或者您的接收方应用具有如下所示的上下文注册的接收器:
var filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null )
那么,发送方应用必须请求如下权限,才能向这些接收器发送广播:
<uses-permission android:name="android.permission.SEND_SMS"/>
安全注意事项和最佳做法
以下是有关收发广播的一些安全注意事项和最佳做法:
- 如果您不需要向应用以外的组件发送广播,则可以使用支持库中提供的
LocalBroadcastManager
来收发本地广播。LocalBroadcastManager
效率更高(无需进行进程间通信),并且您无需考虑其他应用在收发您的广播时带来的任何安全问题。本地广播可在您的应用中作为通用的发布/订阅事件总线,而不会产生任何系统级广播开销。 - 如果有许多应用在其清单中注册接收相同的广播,可能会导致系统启动大量应用,从而对设备性能和用户体验造成严重影响。为避免发生这种情况,请优先使用上下文注册而不是清单声明。有时,Android 系统本身会强制使用上下文注册的接收器。例如,
CONNECTIVITY_ACTION
广播只会传送给上下文注册的接收器。 - 请勿使用隐式 intent 广播敏感信息。任何注册接收广播的应用都可以读取这些信息。您可以通过以下三种方式控制哪些应用可以接收您的广播:
- 您可以在发送广播时指定权限。
- 在 Android 4.0 及更高版本中,您可以在发送广播时使用
setPackage(String)
指定软件包。系统会将广播限定到与该软件包匹配的一组应用。 - 您可以使用
LocalBroadcastManager
发送本地广播。
- 当您注册接收器时,任何应用都可以向您应用的接收器发送潜在的恶意广播。您可以通过以下三种方式限制您的应用可以接收的广播:
- 您可以在注册广播接收器时指定权限。
- 对于清单声明的接收器,您可以在清单中将 android:exported 属性设置为“false”。这样一来,接收器就不会接收来自应用外部的广播。
- 您可以使用
LocalBroadcastManager
限制您的应用只接收本地广播。
- 广播操作的命名空间是全局性的。请确保在您自己的命名空间中编写操作名称和其他字符串,否则可能会无意中与其他应用发生冲突。
- 由于接收器的
onReceive(Context, Intent)
方法在主线程上运行,因此它会快速执行并返回。如果您需要执行长时间运行的工作,请谨慎生成线程或启动后台服务,因为系统可能会在onReceive()
返回后终止整个进程。如需了解详情,请参阅对进程状态的影响。要执行长时间运行的工作,我们建议:- 在接收器的
onReceive()
方法中调用goAsync()
,并将BroadcastReceiver.PendingResult
传递给后台线程。这样,在从onReceive()
返回后,广播仍可保持活跃状态。不过,即使采用这种方法,系统仍希望您非常快速地完成广播(在 10 秒以内)。为避免影响主线程,它允许您将工作移到另一个线程。 - 使用
JobScheduler
调度作业。如需了解详情,请参阅智能作业调度。
- 在接收器的
- 请勿从广播接收器启动 Activity,否则会影响用户体验,尤其是有多个接收器时。相反,可以考虑显示通知。
隐式广播例外情况
受 Android 8.0(API 级别 26)后台执行限制的影响,以 API 级别 26 或更高级别为目标的应用无法再在其清单中注册用于隐式广播的广播接收器。不过,有几种广播目前不受这些限制的约束。无论应用以哪个 API 级别为目标,都可以继续为以下广播注册监听器。
注意:尽管这些隐式广播仍在后台运行,但您应避免为其注册监听器。
-
ACTION_LOCKED_BOOT_COMPLETED
、ACTION_BOOT_COMPLETED
豁免的原因这些广播仅在首次启动时发送一次,而且许多应用需要接收此广播以调度作业、闹钟等。
-
ACTION_USER_INITIALIZE
、"android.intent.action.USER_ADDED"
、"android.intent.action.USER_REMOVED"
这些广播受特许权限保护,因此大多数普通应用都无法接收它们。
-
"android.intent.action.TIME_SET"
、ACTION_TIMEZONE_CHANGED
、ACTION_NEXT_ALARM_CLOCK_CHANGED
当时间、时区或闹钟发生更改时,时钟应用可能需要接收这些广播以更新闹钟。
-
ACTION_LOCALE_CHANGED
仅在语言区域发生更改时发送,这种情况并不常见。当语言区域发生更改时,应用可能需要更新其数据。
-
ACTION_USB_ACCESSORY_ATTACHED
、ACTION_USB_ACCESSORY_DETACHED
、ACTION_USB_DEVICE_ATTACHED
、ACTION_USB_DEVICE_DETACHED
如果某个应用需要了解这些与 USB 有关的事件,除了为广播进行注册,目前还没有很好的替代方法。
-
ACTION_CONNECTION_STATE_CHANGED
、ACTION_CONNECTION_STATE_CHANGED
、ACTION_ACL_CONNECTED
、ACTION_ACL_DISCONNECTED
如果应用接收到针对这些蓝牙事件的广播,则用户体验不太可能受到影响。
-
ACTION_CARRIER_CONFIG_CHANGED
、TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGED
、"TelephonyIntents.SECRET_CODE_ACTION"
、ACTION_PHONE_STATE_CHANGED
、ACTION_PHONE_ACCOUNT_REGISTERED
、ACTION_PHONE_ACCOUNT_UNREGISTERED
OEM 电话应用可能需要接收这些广播。
-
LOGIN_ACCOUNTS_CHANGED_ACTION
有些应用需要了解登录帐号的更改,以便为新帐号和已更改的帐号设置调度的操作。
-
ACTION_ACCOUNT_REMOVED
具有帐号可见性的应用会在帐号被移除后收到此广播。如果应用只需要对此帐号更改执行操作,则强烈建议应用使用此广播,而不是使用已弃用的
LOGIN_ACCOUNTS_CHANGED_ACTION
。 -
ACTION_PACKAGE_DATA_CLEARED
仅在用户明确清除“设置”中的数据时发送,因此广播接收器不太可能对用户体验造成显著影响。
-
ACTION_PACKAGE_FULLY_REMOVED
某些应用可能需要在其他软件包被移除时更新其存储的数据;对于这些应用来说,除了为此广播进行注册,没有很好的替代方法。注意:其他与软件包相关的广播(例如
ACTION_PACKAGE_REPLACED
)未能免受新限制的约束。这些广播很常见,豁免的话可能会影响性能。 -
ACTION_NEW_OUTGOING_CALL
应用需要接收此广播,以在用户拨打电话时采取相应操作。
-
ACTION_DEVICE_OWNER_CHANGED
此直播的发送频率不高;某些应用需要接收它来了解设备的安全状态已发生更改。
-
ACTION_EVENT_REMINDER
由日历提供程序发送,以向日历应用发布事件提醒。由于日历提供程序并不知道日历应用是什么,因此此广播必须是隐式的。
-
ACTION_MEDIA_MOUNTED
、ACTION_MEDIA_CHECKING
、ACTION_MEDIA_UNMOUNTED
、ACTION_MEDIA_EJECT
、ACTION_MEDIA_UNMOUNTABLE
、ACTION_MEDIA_REMOVED
、ACTION_MEDIA_BAD_REMOVAL
这些广播会在用户与设备的物理互动(安装或移除存储卷)或启动初始化(可用卷装载时)过程中发送,并且通常受用户控制。
-
SMS_RECEIVED_ACTION
、WAP_PUSH_RECEIVED_ACTION
短信接收者应用需要依赖这些广播。
管理设备唤醒状态
当 Android 设备空闲时,它会首先调暗屏幕,然后关闭屏幕,最终关闭 CPU。这可以防止设备的电池电量快速耗尽。不过,有时您的应用可能需要采取不同的行为:
- 游戏或电影应用等应用可能需要使屏幕保持开启状态。
- 其他应用可能不需要屏幕始终处于开启状态,但可能需要 CPU 持续运行,直到某项关键操作完成。
以下课程介绍了如何在必要时使设备保持唤醒状态,而不大量消耗电池电量。
课程
-
了解如何根据需要使屏幕或 CPU 保持唤醒状态,同时最大限度减少对电池续航时间的影响。
-
了解如何使用重复闹钟来安排在应用生命周期之外定期执行操作(即使应用未运行且/或设备处于休眠状态)。
使设备保持唤醒状态
为避免消耗电池电量,处于空闲状态的 Android 设备会快速进入休眠模式。不过,有时应用需要唤醒屏幕或 CPU 并使之保持唤醒状态,以完成某项工作。
所采用的方法取决于应用的需求。但是,一般而言,您应该为应用使用尽可能轻量的方法,以便最大限度减少应用对系统资源的影响。以下几个部分介绍了如何处理设备的默认休眠行为不符合应用要求的情况。
使用唤醒锁定的替代方案
在为您的应用添加唤醒锁定支持之前,请考虑应用用例是否支持以下某种替代解决方案:
- 如果您的应用正在执行长时间运行的 HTTP 下载,请考虑使用
DownloadManager
。 - 如果您的应用同步来自外部服务器的数据,请考虑创建同步适配器。
- 如果您的应用依赖后台服务,请考虑使用 JobScheduler 或 Firebase 云消息传递,以便以特定的时间间隔触发这些服务。
使屏幕保持开启状态
某些应用需要使屏幕保持开启状态,例如游戏或电影应用。要实现此目标,最好的方法是在您的 Activity 中(仅在 Activity 中,切勿在服务或其他应用组件中)使用 FLAG_KEEP_SCREEN_ON
。例如:
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
这种方法的优点是,与唤醒锁定(在使 CPU 保持运行状态部分介绍)不同,它不需要特殊权限,并且平台可以正确管理用户在不同应用之间的切换,您的应用无需担心释放未使用的资源。
实现此目标的另一种方法是,在应用的布局 XML 文件中,使用 android:keepScreenOn
属性:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
...
</RelativeLayout>
使用 android:keepScreenOn="true"
功效等同于 FLAG_KEEP_SCREEN_ON
。您可以使用最适合您的应用的任意一种方法。在 Activity 中以编程方式设置标记的优势在于,您可以选择稍后以编程方式清除该标记,从而使屏幕可以关闭。
注意:除非您不再希望屏幕在应用运行时保持开启状态(例如,如果您希望屏幕在处于不活动状态一段时间后超时),否则不需要清除 FLAG_KEEP_SCREEN_ON
标记。窗口管理器负责确保当应用进入后台或返回前台时,具有正确的行为。但是,如果您想要明确清除该标记,从而使屏幕再次可以关闭,请使用 clearFlags()
:getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
。
使 CPU 保持运行状态
如果您需要使 CPU 保持运行状态,以便在设备进入休眠模式之前完成某项工作,可以使用一项称为“唤醒锁定”的 PowerManager
系统服务功能。唤醒锁定可使应用控制主机设备的电源状态。
创建和持有唤醒锁定会对主机设备的电池续航时间产生重大影响。因此,您应仅在绝对必要时使用唤醒锁定,并持有尽可能短的时间。例如,您绝不需要在 Activity 中使用唤醒锁定。如上所述,如果您希望屏幕在 Activity 中保持开启状态,请使用 FLAG_KEEP_SCREEN_ON
。
使用唤醒锁定的一种合理情形是,某项后台服务需要获取唤醒锁定,以便 CPU 在屏幕关闭时保持运行状态,可以完成相关工作。再次声明,由于这种做法会影响电池续航时间,因此应尽量减少其使用频率。
如需使用唤醒锁定,首先要将 WAKE_LOCK
权限添加到应用的清单文件中:
<uses-permission android:name="android.permission.WAKE_LOCK" />
如果您的应用包含使用服务来完成相关工作的广播接收器,您可以根据使用可使设备保持唤醒状态的广播接收器部分所述,通过 WakefulBroadcastReceiver
管理唤醒锁定。这是首选方法。如果您的应用未采用该模式,您可以使用以下方法直接设置唤醒锁定:
val wakeLock: PowerManager.WakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag").apply {
acquire()
}
}
要释放唤醒锁定,请调用 wakelock.release()
。这会释放您对 CPU 的声明。请务必在应用结束对唤醒锁定的使用后立即将其释放,以避免消耗电池电量。
使用可使设备保持唤醒状态的广播接收器
通过将广播接收器与服务结合使用,您可以管理后台任务的生命周期。
WakefulBroadcastReceiver
是一种特殊类型的广播接收器,负责为您的应用创建和管理 PARTIAL_WAKE_LOCK
。WakefulBroadcastReceiver
将工作看作是 Service
(通常为 IntentService
),同时确保设备不会在转换期间重新进入休眠模式。如果您未在将工作转换为服务的过程中保持唤醒锁定,那么您实际上允许设备在完成工作之前重新进入休眠模式。最终结果是,应用可能直到未来的某个时间点才能完成工作,而这并不是您所希望的。
与其他任何广播接收器一样,使用 WakefulBroadcastReceiver
的第一步是将其添加到清单中:
<receiver android:name=".MyWakefulReceiver"></receiver>
以下代码使用方法 startWakefulService()
方法启动 MyIntentService
。该方法与 startService()
大致相同,区别在于 WakefulBroadcastReceiver
会在服务启动时保持唤醒锁定。通过 startWakefulService()
传递的 intent 包含标识唤醒锁定的其他信息:
class MyWakefulReceiver : WakefulBroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Start the service, keeping the device awake while the service is
// launching. This is the Intent to deliver to the service.
Intent(context, MyIntentService::class.java).also { service ->
WakefulBroadcastReceiver.startWakefulService(context, service)
}
}
}
当服务完成时,会调用 MyWakefulReceiver.completeWakefulIntent()
以释放唤醒锁定。completeWakefulIntent()
方法与其参数具有从 WakefulBroadcastReceiver
传入的同一 intent:
const val NOTIFICATION_ID = 1
class MyIntentService : IntentService("MyIntentService") {
private val notificationManager: NotificationManager? = null
internal var builder: NotificationCompat.Builder? = null
override fun onHandleIntent(intent: Intent) {
val extras: Bundle = intent.extras
// Do the work that requires your app to keep the CPU running.
// ...
// Release the wake lock provided by the WakefulBroadcastReceiver.
MyWakefulReceiver.completeWakefulIntent(intent)
}
}
安排重复闹钟
闹钟(基于 AlarmManager
类)为您提供了一种在应用生命周期之外定时执行操作的方式。例如,您可以使用闹钟来启动长期运行的操作,比如每天启动一次某项服务以下载天气预报。
闹钟具有以下特征:
- 它们可让您按设定的时间和/或间隔触发 intent。
- 您可以将它们与广播接收器结合使用,以启动服务以及执行其他操作。
- 它们在应用外部运行,因此即使应用未运行,或设备本身处于休眠状态,您也可以使用它们来触发事件或操作。
- 它们可以帮助您最大限度地降低应用的资源要求。您可以安排定期执行操作,而无需依赖定时器或持续运行后台服务。
注意:对于肯定是在应用生命周期内发生的定时操作,请考虑结合使用 Handler
类与 Timer
和 Thread
。该方法可让 Android 更好地控制系统资源。
了解利弊
重复闹钟是一种相对简单的机制,灵活性有限。它可能并不是您的应用的理想选择,尤其是当您需要触发网络操作时。设计不合理的闹钟会导致耗电过度,并会使服务器负载显著增加。
在应用生命周期之外触发操作的一个常见情形是与服务器同步数据。在该情形中,您可能会想要使用重复闹钟。但是,如果您有托管应用数据的服务器,那么与 AlarmManager
相比,结合使用 Google 云消息传递 (GCM) 与同步适配器是更好的解决方案。同步适配器可为您提供与 AlarmManager
完全一样的时间安排选项,但灵活性要高得多。例如,同步操作可以基于来自服务器/设备的“新数据”消息(如需了解详情,请参阅运行同步适配器)、用户的活动状态(或不活动状态)、一天当中的时段等等。有关何时以及如何使用 GCM 和同步适配器的详细讨论,请参阅此页面顶部链接的视频。
当设备在低电耗模式下处于空闲状态时,闹钟不会触发。所有已设置的闹钟都会推迟,直到设备退出低电耗模式。如果您需要确保即使设备处于空闲状态您的工作也会完成,可以通过多种选项实现这一目的。您可以使用 setAndAllowWhileIdle()
或 setExactAndAllowWhileIdle()
来确保闹钟会执行。另一个选项是使用新的 WorkManager API,它可用来执行一次性或周期性的后台工作。如需了解详情,请参阅使用 WorkManager 安排任务。
最佳做法
您在设计重复闹钟时所做的每一个选择都会对应用使用(或滥用)系统资源的方式产生影响。例如,假设有一个热门应用会与服务器进行同步。如果同步操作基于时钟时间,并且应用的每个实例都会在晚上 11:00 进行同步,则服务器负载高峰可能会导致高延迟,甚至是“拒绝服务”错误。请遵循以下使用闹钟的最佳做法:
-
为重复闹钟触发的网络请求加入一些随机性(抖动):
- 在闹钟触发时执行本地工作。“本地工作”是指无需连接至服务器或用到来自服务器的数据的任何工作。
- 同时,将包含网络请求的闹钟设置为在某个随机时间段内触发。
-
尽可能降低闹钟触发频率。
-
请勿在不必要的情况下唤醒设备(该行为由闹钟类型决定,如选择闹钟类型中所述)。
-
请勿将闹钟的触发时间设置得过于精确。
使用
setInexactRepeating()
而不是setRepeating()
。当您使用setInexactRepeating()
时,Android 会同步来自多个应用的重复闹钟,并同时触发它们。这可以减少系统必须唤醒设备的总次数,从而减少耗电量。从 Android 4.4(API 级别 19)开始,所有重复闹钟都是不精确的。请注意,尽管setInexactRepeating()
是对setRepeating()
的改进,但如果应用的每个实例都大约在同一时间连接至服务器,仍会使服务器不堪重负。因此,如上所述,对于网络请求,请为您的闹钟添加一些随机性。 -
尽量避免基于时钟时间设置闹钟。
在某个精确的时间触发的重复闹钟不利于规模化。如果可以的话,请使用
ELAPSED_REALTIME
。下一部分详细介绍了不同的闹钟类型。
设置重复闹钟
如上所述,对于安排常规的事件或数据查询而言,重复闹钟是一个不错的选择。重复闹钟具有以下特征:
- 闹钟类型。要了解详情,请参阅选择闹钟类型。
- 触发时间。如果您指定的触发时间为过去的时间,则闹钟会立即触发。
- 闹钟的间隔。例如,每天一次、每小时一次、每 5 分钟一次,等等。
- 闹钟触发的待定 intent。当您设置了使用同一待定 intent 的第二个闹钟时,它会替换原始闹钟。
选择闹钟类型
使用重复闹钟时的首要考虑因素之一是,它应该是哪种类型。
闹钟有两种常规时钟类型:“经过的时间”和“实时时钟”(RTC)。前者使用“自系统启动以来的时间”作为参考,而后者则使用世界协调时间 (UTC)(挂钟时间)。这意味着“经过的时间”类型适合用于设置基于时间流逝情况的闹钟(例如,每 30 秒触发一次的闹钟),因为它不受时区/语言区域的影响。实时时钟类型更适合依赖当前语言区域的闹钟。
这两种类型都有一个“唤醒”版本,该版本会在屏幕处于关闭状态时唤醒设备的 CPU。这可以确保闹钟在预定的时间触发。如果您的应用具有时间依赖项(例如,如果它需要在限定时间内执行特定操作),那么这会非常有用。如果您未使用闹钟类型的唤醒版本,则在您的设备下次处于唤醒状态时,所有重复闹钟都会触发。
如果您只需要让闹钟以特定的时间间隔(例如每半小时)触发,请使用某个“经过的时间”类型。一般而言,它是更好的选择。
如果您需要让闹钟在一天中的某个特定时段触发,请选择基于时钟的实时时钟类型之一。但是请注意,这种方法有一些弊端,即应用可能无法很好地转换到其他语言区域,并且如果用户更改设备的时间设置,可能会导致应用出现意外行为。如上所述,使用实时时钟闹钟类型也无法很好地扩展。如果可以的话,我们建议您使用“经过的时间”闹钟。
以下为类型列表:
ELAPSED_REALTIME
- 基于自设备启动以来所经过的时间触发待定 intent,但不会唤醒设备。经过时间包括设备处于休眠状态期间的任何时间。ELAPSED_REALTIME_WAKEUP
- 唤醒设备,并在自设备启动以来特定时间过去之后触发待定 intent。RTC
- 在指定的时间触发待定 intent,但不会唤醒设备。RTC_WAKEUP
- 唤醒设备以在指定的时间触发待定 intent。
“经过的时间”闹钟示例
以下是使用 ELAPSED_REALTIME_WAKEUP
的一些示例。
在 30 分钟后唤醒设备并触发闹钟,此后每 30 分钟触发一次:
// Hopefully your alarm will have a lower frequency than this!
alarmMgr?.setInexactRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR,
AlarmManager.INTERVAL_HALF_HOUR,
alarmIntent
)
在一分钟后唤醒设备并触发一个一次性(非重复)闹钟:
private var alarmMgr: AlarmManager? = null
private lateinit var alarmIntent: PendingIntent
...
alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
PendingIntent.getBroadcast(context, 0, intent, 0)
}
alarmMgr?.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 60 * 1000,
alarmIntent
)
“实时时钟”闹钟示例
以下是使用 RTC_WAKEUP
的一些示例。
在下午 2:00 左右唤醒设备并触发闹钟,并在每天的同一时间重复一次:
// Set the alarm to start at approximately 2:00 p.m.
val calendar: Calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, 14)
}
// With setInexactRepeating(), you have to use one of the AlarmManager interval
// constants--in this case, AlarmManager.INTERVAL_DAY.
alarmMgr?.setInexactRepeating(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_DAY,
alarmIntent
)
在上午 8:30 准时唤醒设备并触发闹钟,此后每 20 分钟触发一次:
private var alarmMgr: AlarmManager? = null
private lateinit var alarmIntent: PendingIntent
...
alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
PendingIntent.getBroadcast(context, 0, intent, 0)
}
// Set the alarm to start at 8:30 a.m.
val calendar: Calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, 8)
set(Calendar.MINUTE, 30)
}
// setRepeating() lets you specify a precise custom interval--in this case,
// 20 minutes.
alarmMgr?.setRepeating(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
1000 * 60 * 20,
alarmIntent
)
确定所需的闹钟精确度
如上所述,选择闹钟类型通常是创建闹钟的第一步。除闹钟类型之外,进一步的区别在于所需的闹钟精确度。对于大多数应用来说,setInexactRepeating()
是合适的选择。您使用此方法时,Android 会同步多个不精确的重复闹钟,并同时触发它们。这样可以减少耗电量。
对于具有严格时间要求的少数应用(例如,闹钟需要在上午 8:30 准时触发,并在此后每小时准点触发一次),请使用 setRepeating()
。不过,您应尽量避免使用精确的闹钟。
使用 setInexactRepeating()
时,您无法像使用 setRepeating()
那样指定自定义时间间隔。您必须使用时间间隔常量(例如,INTERVAL_FIFTEEN_MINUTES
、INTERVAL_DAY
等)中的某一个。如需查看完整的列表,请参阅 AlarmManager
。
取消闹钟
您可能需要添加取消闹钟的功能,具体取决于您的应用。要取消闹钟,请在闹钟管理器上调用 cancel()
,并传入您不想再触发的 PendingIntent
。例如:
// If the alarm has been set, cancel it.
alarmMgr?.cancel(alarmIntent)
在设备重启时启动闹钟
默认情况下,当设备关机时,所有闹钟都会被取消。为了防止出现这种情况,您可以将应用设计为在用户重启设备时,自动重新启动重复闹钟。这样可以确保 AlarmManager
继续执行其任务,而无需用户手动重新启动闹钟。
具体步骤如下所示:
-
在应用的清单中设置
RECEIVE_BOOT_COMPLETED
权限。这样,您的应用将可以接收系统完成启动后广播ACTION_BOOT_COMPLETED(这种方法仅适用于用户已至少启动过该应用一次的情况):
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
-
实现BroadcastReceiver以接收广播:
class SampleBootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == "android.intent.action.BOOT_COMPLETED") { // Set the alarm here. } } }
-
使用用于过滤ACTION_BOOT_COMPLETED操作的 intent 过滤器将该接收器添加到应用的清单文件中:
<receiver android:name=".SampleBootReceiver" android:enabled="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"></action> </intent-filter> </receiver>
请注意,在清单中,启动接收器设置为
android:enabled="false"
。这意味着除非应用明确启用该接收器,否则系统不会调用它。这可以防止系统不必要地调用启动接收器。您可以按照以下方式启用接收器(例如,如果用户设置了闹钟):val receiver = ComponentName(context, SampleBootReceiver::class.java) context.packageManager.setComponentEnabledSetting( receiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP )
您以这种方式启用接收器后,即使用户重启设备,它也会保持启用状态。也就是说,即使是在设备重新启动后,以编程方式启用接收器也会覆盖清单设置。接收器将保持启用状态,直到您的应用将其停用。您可以按照以下方式停用接收器(例如,如果用户取消闹钟):
val receiver = ComponentName(context, SampleBootReceiver::class.java) context.packageManager.setComponentEnabledSetting( receiver, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP )
低电耗模式和应用待机模式的影响
为了延长设备的电池续航时间,我们在 Android 6.0(API 级别 23)中引入了低电耗模式和应用待机模式。当设备处于低电耗模式时,所有标准闹钟都会推迟,直到设备退出低电耗模式或维护期开始。如果您必须让某个闹钟在低电耗模式下也能触发,可以使用 setAndAllowWhileIdle()
或 setExactAndAllowWhileIdle()
。您的应用将在处于空闲状态时(即用户在一段时间内未使用应用,并且应用没有前台进程时)进入应用待机模式。当应用处于应用待机模式时,闹钟会像设备处于低电耗模式一样被延迟。当应用不再处于空闲状态或者当设备接入电源时,该限制便会解除。如需详细了解这两种模式对应用的影响,请参阅对低电耗模式和应用待机模式进行针对性优化。