前言
Service (服务) 是 Android 为了实现程序后台运行提供的核心解决方案。它的特点是不依赖于任何界面,即使用户将应用切换到了后台,或是关闭了所有 Activity,Service 也能正常运行。当然,如果创建 Service 的应用进程被杀死了,Service 就会立即停止运行。
这里需要注意:Service 并不会自动开启子线程。 所有的代码默认运行在应用的主线程(UI 线程)上。所以我们必须在 Service 的内部手动创建子线程,来执行网络请求或文件读写等耗时操作。否则可能会因阻塞主线程导致应用无响应。
因此,在了解 Service 的用法之前,我们先来看看 Android 的多线程编程。
Android 多线程
如果我们要执行一些耗时操作,比如网络请求,我们就必须将这部分操作放在子线程中执行,否则会导致主线程被阻塞,影响用户体验。
线程的基本用法
定义一个线程最简单、直接的方式是创建一个类继承自 Thread
类,并重写 run()
方法。
kotlin
class MyThread : Thread() {
override fun run() {
println("在子线程中执行耗时操作")
}
}
启动线程只需创建 MyThread
实例,然后调用其 start()
方法即可。
kotlin
// 启动线程
val thread = MyThread()
thread.start()
但使用继承的方式来定义线程,耦合性太高。为了降低耦合性,我们更多会选择实现 Runnable
接口。
kotlin
class MyThread : Runnable {
override fun run() {
println("在子线程中执行耗时操作")
}
}
现在启动线程就变为了:
kotlin
val myThread = MyThread()
val thread = Thread(myThread) // 将实现了 Runnable 接口的 MyThread 实例传入 Thread 类的构造函数中
thread.start()
当然,在 Kotlin 中,使用 Lambda 表达式可以简化代码:
kotlin
val thread = Thread {
println("在子线程中执行耗时操作")
}
thread.start()
而 Kotlin 还提供了一个更便捷的顶层函数 thread
来启动线程:
kotlin
thread {
println("在子线程中执行耗时操作")
}
在子线程中更新UI
和大多数 GUI 框架一样,Android 的 UI 库也是线程不安全 的。任何对 UI 的更新,都必须在主线程中进行,否则应用会因 CalledFromWrongThreadException
异常而崩溃。
我们来验证一下。新建一个名为 AndroidThreadTest
的 Empty Views Activity 项目。在布局中放置一个 TextView
和一个 Button
,前者用于显示文字内容,后者用于修改前者的内容。
activity_main.xml
文件中的代码如下:
xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/changeTextBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Change Text" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Hello world"
android:textSize="20sp" />
</RelativeLayout>
然后在 MainActivity
中,我们给按钮注册点击事件,并在点击事件中,启动子线程修改 TextView
控件的文本。
代码如下:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.changeTextBtn.setOnClickListener {
thread {
// 在子线程更新 UI
binding.textView.text = "Nice to see you~"
}
}
}
}
运行程序并点击按钮,会发现应用崩溃了,报错信息:android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. Expected: main Calling: Thread-2
这是因为我们操作了不属于当前线程的视图,简单来说,就是在子线程中更新 UI 导致的。
但有时,我们又必须在子线程中执行耗时操作,然后根据执行结果来更新 UI,那该怎么办?为此,Android 提供了经典的异步消息处理机制。
解析异步消息处理机制
我们先来看看使用这套机制修复之前的问题代码:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// 表示更新 TextView 的动作
private val updateText = 1
// 创建一个与主线程绑定的 Handler
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// Handler 在主线程处理消息,在这里可以安全更新 UI
if (msg.what == updateText) {
binding.textView.text = "Nice to meet you"
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.changeTextBtn.setOnClickListener {
thread {
// 在子线程中创建并配置 Message
val msg = Message().apply { what = updateText }
// Handler 将消息发送到主线程的 MessageQueue
handler.sendMessage(msg)
}
}
}
}
现在重新运行程序,并点击按钮,可以看到界面中显示的内容被成功替换了。
那么,这段代码是怎么工作的呢?这就要说说异步消息处理机制的四个核心组件了。
-
Message
(消息)用于在线程间传递数据。就比如前面的
what
字段,另外,还可以使用arg1
、arg2
字段来传递整型数据,使用obj
字段传递Object
对象。 -
Handler
(处理者)用于发送和处理消息。发送消息使用
sendMessage()
、post()
方法,在handleMessage()
方法中处理消息。 -
MessageQueue
(消息队列)用于存放所有通过
Handler
发送过来的消息,消息会在消息队列中等待被处理。每个线程中只会有一个MessageQueue
。 -
Looper
(循环器)当调用了
Looper
的loop()
方法后,就会进入无限循环中。每当发现消息队列中存在一条消息,就会将它取出,并分发给Handler
的handleMessage()
方法进行处理。每个线程也只有一个Looper
。
我们来梳理一遍异步消息处理的流程:
首先,在主线程中创建 Handler
对象(会与主线程的 Looper
和 MessageQueue
关联),然后重写其 handleMessage()
方法。当子线程需要操作 UI 时,就创建 Meassage
对象,并通过 Handler
将这条消息发送出去。这条消息会被送到主线程的 MessageQueue
消息队列中等待被处理,而主线程的 Looper
会一直尝试从 MessageQueue
中取出消息并分发回 Handler
的 handleMessage()
方法中。
由于我们创建的是与主线程绑定的 Handler
,所以在 handleMessage()
方法中的代码会在主线程中运行,可以安全更新 UI。
异步消息处理机制流程图:
Kotlin 协程
理解 Handler
机制很重要。但现在,我们更多会使用 Kotlin 协程(Coroutines) 来完成异步编程,这也是官方强烈推荐的首选方案。它代码更加简洁,逻辑更清晰,且能从根本上避免内存泄漏问题。
使用协程,上面更新 UI 的代码可变为:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.changeTextBtn.setOnClickListener {
// 启动一个与 Activity 生命周期绑定的协程
lifecycleScope.launch {
// 使用 withContext 切换到 IO 线程执行耗时操作
val result = withContext(Dispatchers.IO) {
"Nice to see you from Coroutine~"
}
// 自动切回主线程,直接更新 UI
binding.textView.text = result
}
}
}
}
Service 的基本用法
讲完了多线程,我们回头看看 Service
。
定义、注册 Service
前置工作: 新建一个名为 ServiceTest
的 Empty Views Activity 项目。
新建一个 MyService
类,继承自 android.app.Service
。代码如下:
kotlin
class MyService : Service() {
companion object {
private const val TAG = "MyService"
}
// Service 首次创建时调用
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate executed")
}
// 每次通过 startService() 启动 Service 时调用
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand executed")
// 启动子线程执行后台任务
thread {
// 执行耗时操作
Log.d(TAG, "Background task is running.")
// 任务完成后,调用 stopSelf() 来停止 Service
stopSelf()
}
// 返回 START_NOT_STICKY,表示如果 Service 被杀死,不需要系统重新创建
return START_NOT_STICKY
}
// 当 Service 被销毁时调用
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy executed")
}
// 用于绑定的 Service,如果不支持绑定,返回 null
override fun onBind(intent: Intent?): IBinder? {
return null
}
}
onStartCommand
的返回值决定了当 Service
被系统杀死后,系统的行为。
-
START_NOT_STICKY
: 非粘性的,如果Service
被杀死,系统不会重新创建它。适合执行一次性可被中断的任务。 -
START_STICKY
: 粘性的,如果Service
被杀死,系统会重新创建它,但再次调用onStartCommand
时,传递的intent
是null
。 -
START_REDELIVER_INTENT
: 相较于START_STICKY
,会把上次的intent
重新传递过来。适合下载任务。
另外,我们要在 AndroidManifest.xml
文件中注册 Service
。如下所示:
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application ...>
<service
android:name=".MyService"
android:enabled="true"
android:exported="false">
</service>
...
</application>
</manifest>
android:exported
属性表示是否允许其他应用启动或绑定此 Service。一般 Service 只给应用内部使用,所以通常设为 false
。
启动和停止 Service
启动和停止 Service
是通过 Intent
来实现的。
首先,在布局中添加两个按钮,分别用于启动和停止 Service
。activity_main.xml
文件中的代码如下:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/startServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Service" />
<Button
android:id="@+id/stopServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop Service" />
</LinearLayout>
然后,在 MainActivity
中实现按钮的逻辑。代码如下:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.startServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
startService(intent) // 启动Service
}
binding.stopServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
stopService(intent) // 停止Service
}
}
}
怎么验证呢?
你可以通过在 Service 的各个回调中打印日志来验证。运行程序后,点击"启动 Service"按钮,可以在日志中看到:
arduino
D/MyService com.example.servicetest onCreate executed
D/MyService com.example.servicetest onStartCommand executed
D/MyService com.example.servicetest Background task is running.
D/MyService com.example.servicetest onDestroy executed
更直观的是在开发者选项中,点击正在运行的服务来查看。
自 Android 8.0 (API 26)系统起,应用的后台功能被"削"了。只有应用保持在前台可见时,Service 才能保证运行,否则随时可能被系统回收。如果你想要在长时间运行的后台任务,你可以使用前台 Service 或 WorkManager。
Activity 和 Service 进行通信:使用 Binder
我们在 Activity 中启动了 Service 后,Activity 就无法再对 Service 做什么了。如果你要实现控制 Service 和与 Service 进行通信,就要用到刚刚我们没讲到的 onBind()
方法。
例如,我们要实现后台下载功能。可以在 Activity 中开始下载以及获取下载进度。
我们可以在 Service 中创建 Binder
对象(实现 IBinder
接口)来管理下载,然后在 onBind()
方法返回此对象。代码如下:
kotlin
class MyService : Service() {
companion object {
private const val TAG = "MyService"
}
private val mBinder = DownloadBinder()
// 定义一个 Binder 内部类
class DownloadBinder : Binder() {
fun startDownload() {
Log.d(TAG, "startDownload executed")
}
fun getProgress(): Int {
Log.d(TAG, "getProgress executed")
return 0
}
}
// 当绑定时返回这个 Binder 实例
override fun onBind(intent: Intent?): IBinder {
return mBinder
}
...
}
在布局文件中新增两个按钮,用于 Activity
绑定和取消绑定 Service
。
xml
<Button
android:id="@+id/bindServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bind Service" />
<Button
android:id="@+id/unbindServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Unbind Service" />
绑定后,Activity 就能调用 Service 中 Binder 提供的方法了。如下所示:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var downloadBinder: MyService.DownloadBinder
private var isBound = false
// 创建 ServiceConnection 实例来接收连接状态的回调
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// 绑定成功后,向下转型 IBinder 并获取 Service 实例
downloadBinder = service as MyService.DownloadBinder
isBound = true
// 现在可以调用 Service 中的方法了
downloadBinder.startDownload()
downloadBinder.getProgress()
}
override fun onServiceDisconnected(name: ComponentName) {
// 连接意外断开时调用(例如 Service 崩溃或被杀死)
isBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.bindServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
binding.unbindServiceBtn.setOnClickListener {
if (isBound) {
unbindService(connection)
isBound = false
}
}
}
}
Context.BIND_AUTO_CREATE
标志表示如果 Service 还未被创建,绑定时会自动创建。onCreate()
方法会执行,但 onStartCommand()
方法不会执行。
现在运行程序,点击"绑定 Service"按钮,可以在日志信息中看到:
D/MyService com.example.servicetest onCreate executed
D/MyService com.example.servicetest startDownload executed
D/MyService com.example.servicetest getProgress executed
接着,点击"解绑 Service"按钮,可以在日志信息中看到:
D/MyService com.example.servicetest onDestroy executed
注意:Service 是应用内全局可用的,并且一个 Service 可以与多个 Activity 进行绑定,都会获得同一个 Binder 实例。
Service 的生命周期
启动模式 (startService
) : 通过调用 Context
的 startService()
方法,可以启动一个 Service 服务,其 onStartCommand()
方法会被回调。如果 Service
实例是首次创建,onCreate()
会在这个方法之前被调用。一旦启动后,Service
会在后台持续运行,直到 stopService()
或 stopSelf()
方法被调用,系统才会销毁该服务(回调 onDestroy()
方法)。
绑定模式 (bindService
) : 我们也可以通过 bindService()
与 Service 服务建立一个持久的连接,此时 onBind()
方法会被回调。同样地,如果是首次创建的话,会先调用 onCreate()
方法。我们可以在调用处获取 onBind()
返回的 IBinder
对象,从而实现与服务的通信。只要还有一个客户端与服务保持着绑定关系,服务就会一直存活。
混合模式的生命周期 : 如果一个服务既被 startService()
启动、又被 bindService()
绑定了,它的生命周期由两者共同决定。只有当所有启动请求都已被停止(stopService/stopSelf
),并且所有客户端都已解绑(unbindService
),这两个条件同时满足时,Service
才会被销毁。