Android 中 `runBlocking` 其实只有一种使用场景

Kotlin 协程提供了一种简洁而强大的方式来处理异步编程。runBlocking 是协程库中的一个重要构造器,它允许我们在协程中运行代码并阻塞当前线程,直到协程完成。尽管协程的主要目的是非阻塞地处理异步任务,但 runBlocking 的设计初衷是为了在已经存在的异步环境中衔接协程的使用。本文将介绍 runBlocking 的本质和使用场景。

什么是 runBlocking

runBlocking 是一个协程构造器,它启动一个新的协程并阻塞当前线程,直到协程执行完成。它通常用于在非协程环境中调用挂起函数,确保代码的执行顺序。

kotlin 复制代码
fun main() = runBlocking {
    // 在协程中运行代码
    println("Hello, World!")
}

runBlocking 的本质

runBlocking 的本质是将协程代码块转换为阻塞代码,以便在不支持协程的环境中使用。这在需要从同步代码调用挂起函数的场景中非常有用。例如,在 Java 代码中调用 Kotlin 协程,或者在 Retrofit 拦截器中同步获取token。

使用场景

runBlocking 适用于需要启动协程并阻塞当前线程直到其完成的场景,例如在 Retrofit 拦截器中获取token、以及与遗留的基于阻塞调用的代码进行集成。 runBlocking 应该被视为一个"桥梁"工具,用于连接协程世界和非协程世界,而不是在纯协程代码中的常规使用模式。Android 中 runBlocking 真正合理的使用场景本质上只有一种。 这个唯一的场景就是:在专用的、完全可控的后台线程中,作为协程世界与非协程世界的桥梁

不应该使用 runBlocking 的地方

有些情况下不应该使用 runBlocking。首先,runBlocking 绝不应该直接在挂起函数中使用。runBlocking 会阻塞当前线程,而你不应该在挂起函数中进行阻塞调用。

kotlin 复制代码
// 在挂起函数中使用绝对禁止使用 runBlocking
suspend fun getToken() = runBlocking {
    // ...
}

我们还应该小心不要在不应该阻塞的线程上使用 runBlocking。这在 Android 上尤其是个问题,因为阻塞主线程会导致应用程序ANR。

kotlin 复制代码
// 错误:在主线程上使用 runBlocking
fun onClick() = runBlocking {
    userNameView.test = getUserName()
}

// 正确:使用 launch 启动一个异步协程
fun onClick() {
    lifecycleScope.launch {
        userNameView.test = getUserName()
    }
}

场景 1:在 Retrofit 拦截器中使用 runBlocking

以下是一个具体的示例,展示如何在 Retrofit 拦截器中使用 runBlocking 来同步获取Token。

1. 创建 TokenProvider 类

首先,我们需要一个 TokenProvider 类来管理令牌的获取和存储。这个类将包含一个挂起函数 getToken,用于从本地数据库或网络中获取Token。

kotlin 复制代码
class TokenProvider(private val apiService: ApiService, private val tokenDao: TokenDao) {

    // 挂起函数,用于获取Token
    suspend fun getToken(): String {
        // 尝试从本地数据库获取Token
        var token = tokenDao.getToken()

        // 如果本地没有令牌或令牌已过期,则从网络获取Token
        if (token == null || token.isExpired()) {
            token = fetchNewToken()
            // 将新令牌保存到本地数据库
            tokenDao.saveToken(token)
        }

        return token.tokenValue
    }

    // 挂起函数,用于从网络获取新Token
    private suspend fun fetchNewToken(): Token {
        val response = apiService.getNewToken()
        if (response.isSuccessful) {
            return response.body() ?: throw Exception("Token response body is null")
        } else {
            throw Exception("Failed to fetch new token: ${response.errorBody()?.string()}")
        }
    }
}

2. 定义 ApiService 接口

接下来,我们定义一个 Retrofit 接口 ApiService,用于获取新Token。

kotlin 复制代码
interface ApiService {
    @POST("auth/token")
    suspend fun getNewToken(): Response<Token>
}

3. 定义 TokenDao 接口

我们还需要一个 DAO 接口 TokenDao,用于访问本地数据库中的Token。

kotlin 复制代码
interface TokenDao {
    @Query("SELECT * FROM token LIMIT 1")
    suspend fun getToken(): Token?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun saveToken(token: Token)
}

4. 定义 Token 数据类

定义一个数据类 Token,表示Token,包括Token值和过期时间。

kotlin 复制代码
data class Token(
    @PrimaryKey val id: Int,
    val tokenValue: String,
    val expiryDate: Long
) {
    // 检查Token是否已过期
    fun isExpired(): Boolean {
        return System.currentTimeMillis() > expiryDate
    }
}

5. 在拦截器中使用 runBlocking

最后,我们在 Retrofit 拦截器中使用 runBlocking 来同步获取Token,并将Token添加到请求头中。

kotlin 复制代码
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()

        // 使用 runBlocking 来同步获取令牌
        val token = runBlocking {
            tokenProvider.getToken()
        }

        // 添加令牌到请求头
        val newRequest = originalRequest.newBuilder()
            .header("Authorization", token)
            .build()

        return chain.proceed(newRequest)
    }
}

场景 2 :在专用后台线程中使用 runBlocking

在某些情况下,当我们在专用的后台线程(如 HandlerThread)中工作时,使用 runBlocking 可能是可以接受的。这种情况下:

  1. 不会阻塞重要线程:操作在专门的后台线程执行
  2. 架构限制:在基于回调的系统中需要调用挂起函数
  3. 顺序执行需求:需要确保任务按顺序完成
kotlin 复制代码
class MessageWorkerThread @Inject constructor() {
    private val handlerThread = HandlerThread(threadName).apply { start() }
    
    private val handler: Handler = object : Handler(handlerThread.looper) {
        override fun handleMessage(msg: Message) {
            runBlocking {
                checkSomething()
                sendEmptyMessageDelayed(TASK_MESSAGE, 1000)
            }
        }
    }
    
    fun startChecking() {
        Log.e("MessageWorkerThread", "start")
        handler.sendEmptyMessage(TASK_MESSAGE)
    }
    
    suspend fun checkSomething() {
        Log.e("MessageWorkerThread", "in:${Thread.currentThread().name}::checkSomething")
        delay(1000)
    }
    
    companion object {
        private const val TASK_MESSAGE: Int = 0x1001
        private const val threadName: String = "MessageWorkerThread"
    }
}

注意 :即使在这种场景下,也应该优先考虑纯协程的解决方案,只有在架构限制无法避免时才使用 runBlocking

总结:为什么说只有一种场景

经过深入分析,我们可以得出结论:在 Android 生产代码中,runBlocking 真正合理的使用场景确实只有一种------在专用后台线程中作为架构衔接的桥梁

runBlocking 应该被视为最后的手段,而不是首选的工具。当你考虑使用 runBlocking 时,先问自己三个问题:

  1. 我是否确切知道当前线程可以被安全阻塞?
  2. 是否存在不阻塞的替代方案?
  3. 这个阻塞操作是否在完全可控的专用线程中? 记住,在 Android 协程的世界里,阻塞总是例外。
相关推荐
应用市场5 小时前
PHP microtime()函数精度问题深度解析与解决方案
android·开发语言·php
沐怡旸6 小时前
【Android】Dalvik 对比 ART
android·面试
消失的旧时光-19437 小时前
Android NDK 完全学习指南:从入门到精通
android
消失的旧时光-19437 小时前
Kotlin 协程实践:深入理解 SupervisorJob、CoroutineScope、Dispatcher 与取消机制
android·开发语言·kotlin
2501_915921437 小时前
iOS 26 描述文件管理与开发环境配置 多工具协作的实战指南
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915909067 小时前
iOS 抓包实战 从原理到复现、定位与真机取证全流程
android·ios·小程序·https·uni-app·iphone·webview
2501_915106328 小时前
HBuilder 上架 iOS 应用全流程指南:从云打包到开心上架(Appuploader)上传的跨平台发布实践
android·ios·小程序·https·uni-app·iphone·webview
Meteors.8 小时前
安卓进阶——Material Design库
android·安卓
佳哥的技术分享8 小时前
kotlin基于MVVM架构构建项目
android·开发语言·kotlin