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 协程的世界里,阻塞总是例外。
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui