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 协程的世界里,阻塞总是例外。
相关推荐
阿巴斯甜4 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker5 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95276 小时前
Andorid Google 登录接入文档
android
黄林晴7 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab19 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android