前一章涵盖了大量与安卓服务相关的信息,在这一点上,服务的概念可能看起来有些压倒性。为了加强上一章中的信息,本章将通过一个旨在逐步介绍 started 服务实现概念的 Android Studio 教程来展开。
在本章中,将创建一个示例应用,并将其用作实现安卓服务的基础。在第一种情况下,将使用 IntentService 类创建服务。该示例随后将被扩展以演示服务类的使用。最后,将实现使用服务类时在单独的线程中执行任务所涉及的步骤。本章已经介绍了已启动的服务,下一章名为“安卓本地绑定服务-一个成功的例子”,将重点介绍绑定服务的实现和客户端服务通信。
66.1 创建示例项目
从欢迎屏幕中选择创建新项目快速启动选项,并在生成的新项目对话框中选择空活动模板,然后单击下一步按钮。
在“名称”字段中输入 ServiceExample,并将 com . ebookwidge . service example 指定为包名称。在单击完成按钮之前,将最低 API 级别设置更改为 API 26:安卓 8.0(奥利奥),并将语言菜单更改为 Kotlin。修改项目以支持视图绑定,如第 18.8 节将项目迁移到视图绑定中所述。
66.2 设计用户界面
在项目工具窗口中找到并加载 activity_main.xml 文件(app-> RES-> layout-> activity _ main . XML)。右键点击“你好世界!”文本视图并选择转换视图...选项。在转换对话框中,选择按钮视图,然后单击应用。选择新按钮,将文本更改为“启动服务”,并将字符串提取到名为 start_service 的资源。在新按钮仍处于选中状态的情况下,在属性面板中找到 onClick 属性,并为其分配一个名为 buttonClick 的方法。
66.3 创建服务类
在编写任何代码之前,第一步是向项目中添加一个新类来包含服务。本教程中要演示的第一种服务类型是基于 JobIntentService 类的。如前一章(“安卓服务概述”)中所述,JobIntentService 类的目的是为开发人员提供一种方便的机制,用于创建在调用应用的单独线程中异步执行任务的服务。
通过右键单击项目工具窗口中 app -> java 下的 com . ebookwidge . service example 包名,并选择 New -> Kotlin Class/File 菜单选项,向项目中添加一个新类。在生成的对话框中,命名新类 MyJobIntentService,并从列表中选择类。最后,点击键盘回车键创建新类。
在 AndroidStudio 编辑器中查看新的 MyJobIntentService.kt 文件,它应该如下所示:
package com.ebookfrenzy.serviceexample
class MyJobIntentService {
}
需要修改该类,使其子类化 JobIntentService 类。当子类化 JobIntentService 时,该类必须覆盖 onHandleWork()方法。因此,修改 MyJobIntentService.kt 文件中的代码,使其如下所示:
package com.ebookfrenzy.serviceexample
import android.content.Intent
import androidx.core.app.JobIntentService
class MyJobIntentService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
}
}
此时剩下的就是在 onHandleWork()方法中实现一些代码,这样服务在被调用时就可以真正做一些事情。通常情况下,这将涉及执行一项需要一些时间才能完成的任务,例如下载一个大文件或播放音频。然而,在本例中,处理程序将简单地休眠 30 秒,每 10 秒钟向 AndroidStudio 日志面板输出一条消息:
package com.ebookfrenzy.serviceexample
.
.
import android.util.Log
class MyJobIntentService : JobIntentService() {
private val TAG = "ServiceExample"
override fun onHandleIntent(intent: Intent?) {
Log.i(TAG, "Job Service started")
var i: Int = 0
while (i <= 3) {
try {
Thread.sleep(10000)
i++
} catch (e: Exception) {
}
Log.i(TAG, "Service running")
}
}
}
66.4 将服务添加到清单文件
在调用服务之前,必须先将其添加到所属应用的清单文件中。至少,这包括添加一个元素以及服务的类名和 BIND_JOB_SERVICE 权限请求。
双击当前项目的 AndroidManifest.xml 文件(app-> manifest)将其加载到编辑器中,并修改 xml 以添加服务元素,如下清单所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.serviceexample" >
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ServiceExample" >
<activity android:name=".MainActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyJobIntentService"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
</manifest>
66.5 启动服务
现在,服务已经实现并在清单文件中声明,下一步是添加代码,以便在单击按钮时启动服务。找到 MainActivity.kt 文件并将其加载到编辑器中,然后实现 buttonClick()方法来添加启动服务的代码:
package com.ebookfrenzy.serviceexample
.
.
import android.view.View
import androidx.core.app.JobIntentService.enqueueWork
class MainActivity : AppCompatActivity() {
val SERVICE_ID = 1001
.
.
fun buttonClick(view: View)
{
enqueueWork(this, MyJobIntentService::class.java, SERVICE_ID, intent)
}
}
该代码声明了一个用于标识服务的服务 id(可以是任何整数值)。接下来,实现 buttonClick()方法来调用 JobIntentService 类的 enqueueWork()方法,以提交服务供执行。
66.6 测试内部服务示例
基于服务的示例服务现在已经完成,可以进行测试了。由于服务显示的消息将出现在 Logcat 面板中,因此在 Android Studio 环境中进行配置非常重要。
首先,在点击面板右上角的菜单之前,显示 Logcat 工具窗口(该窗口当前可能为“仅显示选定的应用”)。从该菜单中,选择编辑过滤器配置菜单选项。
在“创建新的日志过滤器”对话框中,命名过滤器服务示例,并在“日志标记”字段中输入在 MyIntentService.kt 中声明的标记值(在上面的代码示例中,这是服务示例)。
更改完成后,单击确定按钮创建过滤器并关闭对话框。现在应该在安卓工具窗口中选择新创建的过滤器。
配置过滤器后,在物理设备或 AVD 模拟器会话上运行应用,单击开始服务按钮。请注意,“作业服务已启动”消息出现在日志面板中。请注意,在应用启动后,可能需要将过滤器菜单设置更改回服务示例:
2021-01-14 14:20:05.630 7193-7265/com.example.serviceexample I/ServiceExample: Job Service started
在接下来的 30 秒内,服务运行消息将每隔 10 秒出现一次。需要注意的是,这个执行是在一个独立于主线程的线程上进行的,让应用可以自由地响应用户并执行其他任务。
66.7 使用服务等级
虽然 JobIntentService 类允许用最少的编码实现一个服务,但是在某些情况下需要服务类的灵活性和同步性。正如在本章中会变得明显的那样,这涉及到一些要实现的额外编程工作。
为了避免一次引入太多的概念,并且为了演示在与调用应用相同的线程中执行耗时的服务任务所固有的风险,这里创建的示例服务不会在新线程中运行服务任务,而是依赖于应用的主线程。服务中新线程的创建和管理将在本教程的下一阶段介绍。
66.8 创建新服务
出于本例的目的,一个新的类将被添加到项目中,该类将从服务类子类化。因此,右键单击项目工具窗口中 app -> java 下列出的包名,并选择新建->服务->服务菜单选项。选择“导出”和“启用”选项,创建一个名为“我的服务”的新类。
创建操作服务的最低要求是实现 onStartCommand()回调方法,该方法将在服务启动时调用。此外,onBind()方法必须返回空值,以向安卓系统表明这不是绑定服务。在本例中,onStartCommand()方法将循环 3 次,每次循环迭代休眠 10 秒。为了完整起见,onCreate()和 onDestroy()方法的存根版本也将在新的 MyService.kt 文件中实现,如下所示:
package . com . ebookwody . service example
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
class MyService : Service() {
private val TAG = "ServiceExample"
override fun onCreate() {
Log.i(TAG, "Service onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "Service onStartCommand " + startId)
var i: Int = 0
while (i <= 3) {
try {
Thread.sleep(10000)
i++
} catch (e: Exception) {
}
Log.i(TAG, "Service running")
}
return Service.START_STICKY
}
override fun onBind(intent: Intent): IBinder {
Log.i(TAG, "Service onBind")
TODO("Return the communication channel to the service.")
}
override fun onDestroy() {
Log.i(TAG, "Service onDestroy")
}
.
.
}
实现服务后,将 AndroidManifest.xml 文件加载到编辑器中,并验证 Android Studio 是否为新服务添加了适当的条目,该条目应如下所示:
<service
android:name=".MyService"
android:enabled="true"
android:exported="true" >
</service>
66.9 启动服务
编辑 MainActivity.kt 文件并修改 buttonClick()方法以启动 MyService 服务:
package com.ebookfrenzy.serviceexample
.
.
import android.content.Intent
class MainActivity : AppCompatActivity() {
.
.
fun buttonClick(view: View)
{
intent = Intent(this, MyService::class.java)
startService(intent)
}
}
buttonClick()方法所做的只是为新服务创建一个意图对象,然后启动它运行。
66.10 运行应用
运行应用,一旦加载,触摸开始服务按钮。在 Logcat 工具窗口中(使用之前创建的 ServiceExample 过滤器),将出现日志消息,指示调用了 buttonClick()方法,并且 onStartCommand()方法中的循环正在执行。
在最后一条循环信息出现之前,尝试再次触摸开始服务按钮。请注意,该按钮没有响应。大约 20 秒后,系统可能会显示一个警告对话框,其中包含消息“ServiceExample 没有响应”。其原因是,应用的主线程当前在执行循环任务时被服务挂起。这不仅会阻止应用对用户做出响应,还会阻止系统做出响应,系统最终会假设应用已经以某种方式锁定。
显然,服务的代码需要修改,以便在独立于主线程的线程中执行任务。
66.11 使用协程执行服务任务
如“线程和异步任务的基本概述”中所述,当安卓应用首次启动时,运行时系统会创建一个线程,默认情况下,所有应用组件都将在该线程中运行。这个线程通常被称为主线程。主线程的主要作用是根据事件处理和与用户界面中视图的交互来处理用户界面。默认情况下,应用中启动的任何附加组件也将在主线程上运行。
这个问题的一个非常简单的解决方案是在一个 Kotlin Coroutine 中执行服务任务。首先编辑 Gradle Scripts-> build . Gradle(Module:service example . app)文件,将以下几行添加到依赖项部分:
dependencies {
.
.
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
.
.
}
进行更改后,单击编辑器面板顶部的“立即同步”链接提交更改。
接下来,按如下方式修改 MyService.kt 文件:
.
.
import kotlinx.coroutines.*
class MyService : Service() {
private val coroutineScope = CoroutineScope(Dispatchers.Default)
.
.
声明作用域后,从 onStartCommand()函数中提取循环代码,并将其放入挂起函数中(还要注意睡眠调用被延迟()挂起函数的调用代替):
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "Service onStartCommand " + startId)
var i: Int = 0
while (i <= 3) {
try {
Thread.sleep(10000)
i++
} catch (e: Exception) {
}
Log.i(TAG, "Service running")
}
return Service.START_STICKY
}
suspend fun performTask(startId: Int) {
Log.i(TAG, "Service onStartCommand " + startId)
var i: Int = 0
while (i <= 3) {
try {
delay(10_000)
i++
} catch (e: Exception) {
}
Log.i(TAG, "Service running " + startId)
}
}
最后,从 onStartCommand()函数中启动挂起功能:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
coroutineScope.launch(Dispatchers.Default) {
performTask(startId)
}
return Service.START_STICKY
}
当应用正在运行时,应该可以多次触摸开始服务按钮。这样做时,Logcat 输出应该指示多个任务同时运行:
I/ServiceExample: Service onStartCommand 1
I/ServiceExample: Service onStartCommand 2
I/ServiceExample: Service onStartCommand 3
I/ServiceExample: Service running 1
I/ServiceExample: Service running 2
I/ServiceExample: Service running 3
随着服务现在处理主线程之外的请求,应用仍然对用户和安卓系统做出响应。
66.12 总结
本章介绍了一个使用 Intentservice 和 Service 类的安卓启动服务的示例实现。该示例还演示了在服务中使用异步任务来避免应用的主线程无响应。