如何用Kotlin Coroutines实现WebView的evaluateJavascript顺序执行
Kotlin 协程实现 WebView JS 脚本顺序执行队列方案
核心思路
我们可以通过两个核心组件实现需求:
- 用
suspendCancellableCoroutine将回调形式的evaluateJavascript转换为挂起函数,利用协程的顺序执行特性天然保证前一个任务执行完成再启动下一个 - 用协程标准库的
Channel作为并发安全的任务队列,无需手动处理多线程并发加锁逻辑
完整实现代码
1. 基础版本(仅顺序执行,无需返回值)
import android.content.Context import android.graphics.Bitmap import android.util.AttributeSet import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class SequentialScriptWebView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : WebView(context, attrs, defStyle) { // 和WebView生命周期绑定的协程作用域,销毁时自动取消避免内存泄漏 private val webScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) // 无容量限制的脚本通道,支持多线程并发发送脚本 private val scriptChannel = Channel<String>(Channel.UNLIMITED) // 页面加载状态标记,在WebViewClient回调中更新 private val isPageLoading = MutableStateFlow(true) init { // 启动脚本消费协程,常驻直到WebView销毁 webScope.launch { // 遍历通道内的所有脚本,按发送顺序执行 for (script in scriptChannel) { // 等待页面加载完成再执行脚本 isPageLoading.first { !it } // 执行脚本并等待回调返回 evaluateJavascriptSuspend(script) } } // 配置WebViewClient,更新加载状态 webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) isPageLoading.value = true } override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) isPageLoading.value = false } override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { super.onReceivedError(view, request, error) isPageLoading.value = false } } } /** * 对外暴露的入队方法,任意线程都可以安全调用 */ fun queueEvaluateJavascript(script: String) { scriptChannel.trySend(script) } /** * 将回调形式的evaluateJavascript转换为挂起函数 */ private suspend fun evaluateJavascriptSuspend(script: String): String? = suspendCancellableCoroutine { cont -> evaluateJavascript(script) { result -> cont.resume(result) } // 协程取消时的可选处理逻辑 cont.invokeOnCancellation { // 如有需要可以在这里添加中断JS执行的逻辑 } } override fun onDetachedFromWindow() { super.onDetachedFromWindow() // WebView销毁时取消协程作用域,关闭通道 webScope.cancel() scriptChannel.close() } }
2. 可选优化(支持获取脚本执行返回值)
如果需要在调用侧获取JS执行的返回结果,可以调整任务结构,将返回值通过协程返回:
// 定义脚本任务结构体 private data class ScriptTask( val script: String, val continuation: Continuation<String?> ) // 替换原有Channel类型 private val scriptChannel = Channel<ScriptTask>(Channel.UNLIMITED) // 对外暴露的挂起方法,调用时可以拿到执行结果 suspend fun evaluateJavascriptSync(script: String): String? = suspendCoroutine { cont -> scriptChannel.trySend(ScriptTask(script, cont)) } // 调整消费逻辑 init { webScope.launch { for (task in scriptChannel) { isPageLoading.first { !it } val result = evaluateJavascriptSuspend(task.script) // 将执行结果返回给调用方 task.continuation.resume(result) } } }
实现说明
- 所有脚本严格按照调用
queueEvaluateJavascript的顺序执行,前一个脚本的evaluateJavascript回调触发后才会执行下一个 Channel本身是线程安全的,支持从任意线程、任意协程并发提交脚本,无需手动添加同步锁- 协程作用域和WebView生命周期绑定,WebView销毁时自动取消所有待执行任务,不会产生内存泄漏
- 内置加载状态等待逻辑,页面加载过程中提交的脚本会自动等待加载完成后再按顺序执行
内容的提问来源于stack exchange,提问作者4gus71n
相关产品推荐
相关产品推荐

