Kotlin Android非挂起函数中如何调用协程await替代runBlocking
你当前实现的是OkHttp的Authenticator接口,该接口的authenticate方法是同步回调约定:方法返回时必须给出重试需要的Request?结果,接口本身没有提供挂起函数版本的实现入口,你无法修改第三方库的接口定义,因此不可能在这个方法里直接用非阻塞的async实现“异步等待结果再返回”——非阻塞协程会在执行到挂起点时立刻返回,无法满足接口同步返回值的要求。
async是协程作用域的扩展函数,调用后只会返回Deferred<T>句柄,要拿到实际返回值必须调用await(),而await()本身是挂起函数,只能在协程上下文或其他挂起函数中调用,无法在你当前的非suspend覆写方法中直接使用拿到结果。- 你当前使用的
runBlocking本来就是官方提供的、用于“同步代码块桥接调用挂起函数”的标准实现,且OkHttp本身会在后台工作线程触发authenticate回调,这里的阻塞不会影响主线程,本身是符合接口设计语义的写法。
如果你希望替换默认的runBlocking实现,统一协程上下文调度、避免协程嵌套问题,可以用以下最小改动的方案:
- 首先在你的
Authenticator实现类中,定义和类生命周期绑定的独立协程作用域,在类销毁时调用cancel()避免内存泄漏:
private val authScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- 修改
authenticateByRefreshToken中的刷新逻辑,给runBlocking指定你定义的协程上下文,让挂起的刷新逻辑运行在你指定的IO后台线程,替换原有写法:
@Synchronized private fun authenticateByRefreshToken(response: Response): Request? { // 原有前置逻辑保持不变 val newRefreshTokenResponse: Resource<RefreshTokenResponse?> = runBlocking(authScope.coroutineContext) { refreshTokenImpl.refreshToken() } // 原有后续构造Request的逻辑保持不变 }
注意:你给方法加的
@Synchronized注解是必要的,可以避免多个并发请求同时触发401时,重复调用token刷新接口导致的token覆盖、刷新失效问题。
如果想彻底避免在authenticate回调中做阻塞等待,可以实现token预刷新逻辑:在token即将过期前,主动在业务层的协程中提前调用刷新接口,将新token存入tokenProvider,这样当请求触发authenticate回调时,可以直接读取已经刷新完成的token构造重试请求,不需要在回调中执行挂起逻辑。这种方案性能最优,但需要额外维护token过期时间的预判逻辑。
不要尝试用GlobalScope.async启动协程后,在方法中调用Future.get()或者其他阻塞方式等待Deferred结果,这种写法和runBlocking的阻塞本质没有区别,还会带来协程泄漏、线程调度不可控的问题,稳定性比直接使用runBlocking更差。
内容的提问来源于stack exchange,提问作者Shyneil Singh

