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 才会被销毁。

相关推荐
ii_best24 分钟前
[按键精灵安卓/ios脚本插件开发] 遍历获取LuaAuxLib函数库命令辅助工具
android·ios
峥嵘life2 小时前
Android Java语言转Kotlin语言学习指导实用攻略
android·java·kotlin
bubiyoushang8882 小时前
Kotlin中快速实现MVI架构
android·开发语言·kotlin
今阳4 小时前
鸿蒙开发笔记-17-ArkTS并发
android·前端·harmonyos
帅次4 小时前
Flutter动画全解析:从AnimatedContainer到AnimationController的完整指南
android·flutter·ios·小程序·kotlin·android studio·iphone
CYRUS_STUDIO5 小时前
逆向某物 App 登录接口:抓包分析 + Frida Hook 还原加密算法
android·app·逆向
casual_clover5 小时前
Android 中 解析 XML 字符串的几种方式
android·xml
CV资深专家5 小时前
Android 构建配置中的变量(通常在设备制造商或定制 ROM 的 AndroidProducts.mk 或产品配置文件中定义)
android
今阳7 小时前
鸿蒙开发笔记-16-应用间跳转
android·前端·harmonyos