Service 指南:从 Handler 机制到 Kotlin 协程

前言

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 异常而崩溃。

我们来验证一下。新建一个名为 AndroidThreadTestEmpty 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 字段,另外,还可以使用 arg1arg2 字段来传递整型数据,使用 obj 字段传递 Object 对象。

  • Handler(处理者)

    用于发送和处理消息。发送消息使用 sendMessage()post() 方法,在 handleMessage() 方法中处理消息。

  • MessageQueue(消息队列)

    用于存放所有通过 Handler 发送过来的消息,消息会在消息队列中等待被处理。每个线程中只会有一个 MessageQueue

  • Looper(循环器)

    当调用了 Looperloop() 方法后,就会进入无限循环中。每当发现消息队列中存在一条消息,就会将它取出,并分发给 HandlerhandleMessage() 方法进行处理。每个线程也只有一个 Looper

我们来梳理一遍异步消息处理的流程:

首先,在主线程中创建 Handler 对象(会与主线程的 LooperMessageQueue 关联),然后重写其 handleMessage() 方法。当子线程需要操作 UI 时,就创建 Meassage 对象,并通过 Handler 将这条消息发送出去。这条消息会被送到主线程的 MessageQueue 消息队列中等待被处理,而主线程的 Looper 会一直尝试从 MessageQueue 中取出消息并分发回 HandlerhandleMessage() 方法中。

由于我们创建的是与主线程绑定的 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

前置工作: 新建一个名为 ServiceTestEmpty 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 时,传递的 intentnull

  • 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 来实现的。

首先,在布局中添加两个按钮,分别用于启动和停止 Serviceactivity_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 才能保证运行,否则随时可能被系统回收。如果你想要在长时间运行的后台任务,你可以使用前台 ServiceWorkManager

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) : 通过调用 ContextstartService() 方法,可以启动一个 Service 服务,其 onStartCommand() 方法会被回调。如果 Service 实例是首次创建,onCreate() 会在这个方法之前被调用。一旦启动后,Service 会在后台持续运行,直到 stopService()stopSelf() 方法被调用,系统才会销毁该服务(回调 onDestroy() 方法)。

绑定模式 (bindService) : 我们也可以通过 bindService() 与 Service 服务建立一个持久的连接,此时 onBind() 方法会被回调。同样地,如果是首次创建的话,会先调用 onCreate() 方法。我们可以在调用处获取 onBind() 返回的 IBinder 对象,从而实现与服务的通信。只要还有一个客户端与服务保持着绑定关系,服务就会一直存活。

混合模式的生命周期 : 如果一个服务既被 startService() 启动、又被 bindService() 绑定了,它的生命周期由两者共同决定。只有当所有启动请求都已被停止(stopService/stopSelf),并且所有客户端都已解绑(unbindService),这两个条件同时满足时,Service 才会被销毁。

相关推荐
踢球的打工仔18 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人18 小时前
安卓socket
android
安卓理事人1 天前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学1 天前
Android M3U8视频播放器
android·音视频
q***57741 天前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober1 天前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿1 天前
关于ObjectAnimator
android
zhangphil1 天前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我1 天前
从头写一个自己的app
android·前端·flutter
lichong9511 天前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端