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 可能是可以接受的。这种情况下:
- 不会阻塞重要线程:操作在专门的后台线程执行
- 架构限制:在基于回调的系统中需要调用挂起函数
- 顺序执行需求:需要确保任务按顺序完成
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 时,先问自己三个问题:
- 我是否确切知道当前线程可以被安全阻塞?
- 是否存在不阻塞的替代方案?
- 这个阻塞操作是否在完全可控的专用线程中? 记住,在 Android 协程的世界里,阻塞总是例外。