尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android GeckoView与WebView深度对比:JS原生双向通信实战指南

Android GeckoView与WebView深度对比:JS原生双向通信实战指南 做Android开发这么多年WebView的坑我踩了无数轮。内存泄漏、渲染不一致、JS交互回调丢数据、版本碎片化这些都是家常便饭。直到我接手一个需要深度定制浏览器内核的项目才真正把GeckoView当成主角来用。它和WebView最大的区别在于GeckoView不是Android系统自带控件的封装而是一个可以独立升级、完整内置的浏览器引擎相当于在你的App里直接塞了一个Firefox的渲染核心。这篇文章我不会讲那些文档里翻得到的API列表而是从一个实际项目出发把GeckoView里最关键、也最容易困惑的JS与原生交互部分拆开揉碎。标题说5分钟搞定指的是你理解核心思路之后跑通一个双向通信Demo确实只需要几分钟。但理解了背后的消息传递机制和线程模型你才不会被各种奇奇怪怪的回调问题卡住半天。文章末尾有完整的Todo Demo代码原生端和Web端各一份直接拿去改就能用。1. 为什么选GeckoView不只是另一个WebView1.1 Android上嵌入浏览器的三条路做混合开发或者想在App里内置一个网页浏览环境大家第一反应是系统的WebView。但WebView有个老生常谈的问题它的内核版本跟着系统走Android 5.0到Android 14不同厂商还会魔改你根本无法保证用户手机上的渲染行为和你的测试机一致。而且系统WebView对CSS新特性的支持参差不齐调试起来极其痛苦。第二条路是Crosswalk这类方案。把Chromium整个打包进App解决了内核统一的问题但APK体积直接暴涨几十MB而且Crosswalk早就停止维护了新项目再用它属于给自己埋雷。第三条路就是GeckoView。它是Mozilla家的开源引擎和Firefox同源通过Maven仓库独立分发你可以像引入一个普通依赖一样把它打包进App。优点是内核版本由你决定渲染行为完全可控支持WebExtension扩展机制对标准Follow得很紧。缺点是包体确实比WebView大不少但相比Crosswalk那种动辄几十MB的侵入GeckoView的增量还算能接受。1.2 GeckoView和WebView的核心差异我用一个表格把两者的关键差异列出来方便你做技术选型时快速判断。对比项系统WebViewGeckoView内核来源Android系统自带Chrome内核厂商可能魔改Mozilla Gecko引擎随App分发版本控制跟随系统无法自行升级通过依赖版本完全锁定网页标准化支持碎片化严重依赖系统升级统一跟随你锁定的版本自定义能力受限JS注入安全模型较弱支持WebExtension、事件监听器扩展性强JS与原生交互addJavascriptInterface为主有安全风险WebMessageListener evaluateJS双向消息机制更安全包体积影响无增加约20-40MB按ABI拆分可优化GPU加速与渲染依赖系统WebView实现统一渲染管线行为可预期1.3 什么项目适合用GeckoView在决定引入之前你要想清楚一个问题你的App是“轻度嵌入网页”还是“把浏览能力做成核心功能”如果只是偶尔弹个广告页、加载个协议说明系统WebView完全够用没必要上GeckoView。但如果你做的是以下这些场景GeckoView就是很合适的选择需要对网页资源加载做精细控制比如拦截特定请求、自定义缓存策略需要统一的渲染效果不允许不同手机上页面排版出现差异需要嵌入选装广告过滤、脚本注入这类扩展能力你要基于浏览器引擎做二次开发比如Markdown编辑器预览、文档在线预览、数据可视化大屏嵌入App内需要加载大量本地HTML资源并且和这些页面有频繁的数据通信我手上这个项目是做一个数据大屏客户端页面上有大量Canvas图表和动画之前用WebView在低端机上经常出现GPU渲染撕裂和内存暴涨的问题后来切到GeckoView渲染稳定性和内存占用都改善了不少。这就是选型带来的实际回报。2. 开始之前环境搭建与GeckoView工程集成2.1 引入GeckoView依赖这里直接说结论项目里面加一行依赖就行。我用的版本是当前比较稳定的一个发布版本你可以在Maven仓库里查最新版。在项目根目录的build.gradle或者其他你配置仓库的地方里加上Mozilla的Maven仓库allprojects { repositories { google() mavenCentral() // 加上Mozilla仓库 maven { url https://maven.mozilla.org/maven2/ } } }在Module的build.gradle里添加依赖dependencies { implementation org.mozilla.geckoview:geckoview:116.0.20230704110215 }一个很重要的提示GeckoView的版本号并不完全遵循语义化版本它其实是发布日期的快照所以你会发现版本号长得很怪后面跟了一串日期数字。这不是事故是人家有意为之保证每次发布都是可追溯的快照。你不需要追求最新选一个稳定版本锁定就行。2.2 初始化GeckoRuntimeGeckoRuntime是全局单例一个App进程只需要创建一个。它负责整个Gecko引擎的生命周期、配置项、扩展管理、下载管理等。强烈建议在Application的onCreate里初始化避免后续使用Session时才去创建带来明显的首次启动延迟。class App : Application() { companion object { lateinit var geckoRuntime: GeckoRuntime } override fun onCreate() { super.onCreate() val settings GeckoRuntimeSettings.Builder() .javaScriptEnabled(true) .remoteDebuggingEnabled(true) // 开启远程调试强烈建议debug环境开启 .allowInsecureConnections(BuildConfig.DEBUG) // debug环境允许http明文 .build() geckoRuntime GeckoRuntime.create(this, settings) } }remoteDebuggingEnabled这个配置我特别说一下。它开启后你可以用Chrome的DevTools协议调试GeckoView里的页面。但注意Chrome DevTools连不上你需要用Firefox的远程调试工具或者直接访问about:debugging。这个功能只在调试环境开线上千万别开否则有安全风险。allowInsecureConnections只在debug包开启原因很直接GeckoView默认不允许加载http明文的资源如果你在开发阶段连的是本地局域网服务不开这个会一直白屏。2.3 创建GeckoSession并加载页面GeckoSession相当于一个浏览标签页。一个Runtime可以管理多个Session每个Session互相独立有自己的会话历史、Cookie、DOM状态。class MainActivity : AppCompatActivity() { private lateinit var geckoSession: GeckoSession override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val geckoView findViewByIdGeckoView(R.id.gecko_view) geckoSession GeckoSession() // 给Session配置内容监听器用于监听页面加载进度 geckoSession.contentDelegate object : GeckoSession.ContentDelegate { override fun onPageStart(session: GeckoSession, url: String) { // 页面开始加载 } override fun onPageStop(session: GeckoSession, success: Boolean) { // 页面加载完成 } } // 打开远程调试 geckoSession.open(GeckoSessionSettings.Builder().remoteDebuggingEnabled(true).build()) // 必须调用。调用后才能和Runtime绑定。 geckoSession.open(App.geckoRuntime) geckoView.setSession(geckoSession) // 加载本地asset里的HTML页面 geckoSession.load(GeckoSession.Loader().loadUri(resource://android/assets/index.html)) } }这里有个非常容易踩坑的地方session.open(runtime)和geckoView.setSession(session)的顺序不能反。先open再setSession否则Session没有准备好被View展示。还有个细节是如果你需要加载asset里的页面用的是resource://android/assets/这个自定义Scheme不是file://这一点和WebView完全不同。用file://直接加载本地HTMLGeckoView默认是拒绝的。2.4 生命周期管理不能偷懒GeckoView不是普通的View它和SurfaceView一样有自己的渲染线程和buffer管理所以Activity的每个生命周期回调都要转发给Session。少了任何一步都会出现黑屏、页面被销毁、内存泄漏这一类问题。override fun onResume() { super.onResume() geckoSession.textInput.onResume() } override fun onPause() { geckoSession.textInput.onPause() super.onPause() } override fun onDestroy() { geckoSession.close() super.onDestroy() }我见过很多初学者只写了onDestroy里close结果页面切到后台再回来就白屏了其实就是没有把onPause和textInput.onPause()对应起来。TextInput封装的是软键盘和输入法交互如果不暂停输入法会继续尝试和已不可见的Session通信表现就是页面闪烁或者键盘弹不出来。3. JS与原生交互的两条路原理和选型在GeckoView里JS和原生通信有两条主路径一条是原生主动调用JS一条是JS主动调用原生。两条路的技术选型和适用场景完全不同我分开说。3.1 原生调JSevaluateJS的机制与坑原生端主动执行JS代码用session.evaluateJS(script)就行。这个方法可以在任何时机调用比如页面加载完成后、按钮点击时、收到服务器推送时、甚至定时任务里。// 原生端调用JS修改页面上id为title的元素文本 geckoSession.evaluateJS(document.getElementById(title).textContent 来自原生的消息)如果你需要拿到JS执行后的返回值光调用evaluateJS是不够的。GeckoView的evaluateJS是异步的你需要传入一个JSEvaluationResultDelegate来接收结果geckoSession.evaluateJS( document.title, object : GeckoSession.JSEvaluationResultDelegate { override fun onResult(value: GeckoResultJSValue) { value.accept(object : GeckoSession.JSValue { override fun getString(): String? { return value.toString() } }) } } )不过这里要提醒你GeckoView对JS返回值的序列化是有限制的复杂对象比如嵌套JSON返回过来可能变成字符串形式的JSON文本你需要自己在原生端再解析。简单类型字符串、数字、布尔没问题函数类型无法跨边界传递。这个机制背后的核心是evaluateJS是往Web引擎的JS线程投递任务不是同步调用。所以你在原生端发起的调用只是把一个任务“丢进去了”真正的执行发生在页面主线程。这也意味着如果页面JS线程被一个死循环卡住你的evaluateJS调用也会随之阻塞表现就是回调迟迟不来。3.2 JS调原生两种姿势对比JS往原生发消息GeckoView提供了两种机制。第一种是WebMessageListener我强烈推荐这个。注册了之后JS端可以像调用window.postMessage一样直接向原生发送消息。它和Android WebView的JavascriptInterface完全不是一个思路后者是把Java对象直接暴露给JS优点是直接用缺点是一旦页面被注入恶意脚本整个原生对象都可能被操作。WebMessageListener只暴露一个消息通道JS端能做的只是发消息给你你可以在原生端决定怎么处理安全边界清晰得多。注册方式如下geckoSession.webMessageListener object : GeckoSession.WebMessageListener { override fun onMessage(session: GeckoSession, message: GeckoSession.WebMessage) { // message.text就是JS发送过来的文本 Log.d(GeckoDemo, 收到JS消息: ${message.type} ${message.text}) // 这里可以切换线程做耗时操作再回到主线程更新UI } }JS端发送消息只需要一行window.window.wrappedJSObject.window.onGeckoMessage(hello from js);等等不对。GeckoView的WebMessageListener不是通过window上的方法暴露的它是通过GeckoSession的registerWebMessageListener注册一个事件处理器。JS端发送的方式是调用window.wrappedJSObject里的特定方法我还是直接说人话吧。实际上GeckoView的WebMessageListener工作方式是这样的在原生端你要显式注册一个事件名和对应的监听回调geckoSession.registerWebMessageListener( onNativeMessage, object : GeckoSession.WebMessageDelegate { override fun onMessage(session: GeckoSession, message: GeckoSession.WebMessage) { Log.d(GeckoDemo, 收到JS消息: ${message.text}) runOnUiThread { // 更新UI } } }, // 允许在content script里发消息 set(GeckoSession.WebMessageDelegate.ALLOW_IN_CONTENT_SCRIPTS), // 允许在扩展脚本里发消息用不到就传空 set() )JS端的发送方式window.wrappedJSObject.onNativeMessage(这是JS发给原生的消息);需要注意这个onNativeMessage方法不是window的内置方法是GeckoView注册到页面上下文里的一个消息入口。它和DOM API是隔离开的所以页面内部的JS代码不能直接操作原生对象这是设计上的安全考量。第二种机制是WebExtension这是GeckoView的高级玩法。你可以写一个浏览器扩展利用扩展的消息API在原生和网页之间做桥接。这个适合对通信协议有严格要求的场景但学习成本和调试成本都高了不少。对于大多数应用内嵌页面的需求WebMessageListener就够了。3.3 应该怎么选场景决定方案通信方向使用方式适用场景原生 - JSevaluateJS主动刷新页面数据、调用页面内部函数JS - 原生WebMessageListenerJS需要通知原生、请求原生能力如调相机、弹Toast双向高频WebMessageListener evaluateJS页面和原生持续交换数据如实时数据大屏复杂协议WebExtension需要多页面共享状态、权限控制、脚本注入的复杂场景我个人经验是大部分业务场景用WebMessageListener evaluateJS这个组合就够了WebExtension更适合做产品化、插件化的浏览器内核应用普通App嵌入用不上。4. 完整Demo一个能跑的Todo应用原生和JS双向通信这个Demo我设计得很简单但五脏俱全。一个HTML页面页面上有一个输入框、一个按钮还有一个列表区域。按钮点击后JS把输入框内容发给原生原生把它保存在内存里然后再调用JS把最新的Todo列表渲染出来。这正好覆盖了JS调原生、原生调JS两条路径。4.1 需求拆解和工程结构先说思路。我们要做的是加载一个本地HTML文件作为主界面页面上JS通过消息通道把用户输入的内容传给原生原生收到消息后把Todo项存到一个数组里原生在合适的时机调用evaluateJS把最新的Todo列表JSON传回页面页面收到JSON后渲染成列表工程结构非常简单就三个文件一个asset里的HTML一个MainActivity一个Application。app/src/main/ ├── assets/ │ └── index.html ├── java/.../MainActivity.kt ├── java/.../App.kt └── res/layout/activity_main.xml4.2 前端页面index.html这个页面的核心就是两步注册一个onNativeMessage监听专门用来接收原生端传回来的Todo列表同时暴露一个sendMessage函数给按钮点击事件把用户输入发给原生。!DOCTYPE html html head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleGeckoView Todo Demo/title style body { margin: 20px; font-family: system-ui, sans-serif; } input { padding: 8px; font-size: 16px; width: 200px; } button { padding: 8px 16px; font-size: 16px; } ul { margin-top: 20px; padding-left: 20px; } li { margin-bottom: 8px; font-size: 16px; } /style /head body h1Todo List/h1 input idtodo-input typetext placeholder输入待办事项 / button idadd-btn添加/button h2待办列表/h2 ul idtodo-list/ul script // 暴露给原生的消息接收入口 // 原生端设置 todoList 这个全局方法页面注册好对应的事件监听 window.todoList null; // 当原生端调用 evaluateJS 设置 todoList 时我们重新渲染列表 function renderTodoList(todoListJson) { var todos JSON.parse(todoListJson); var list document.getElementById(todo-list); list.innerHTML ; todos.forEach(function(item) { var li document.createElement(li); li.textContent item; list.appendChild(li); }); } // 给添加按钮绑定事件 document.getElementById(add-btn).addEventListener(click, function() { var inputText document.getElementById(todo-input).value; if (!inputText.trim()) { // 内容为空则不处理 return; } // 调用GeckoView暴露的原生消息入口把新Todo发给原生 if (window.wrappedJSObject window.wrappedJSObject.onTodoAdd) { window.wrappedJSObject.onTodoAdd(inputText); } document.getElementById(todo-input).value ; }); /script /body /html这段HTML里的关键点window.wrappedJSObject.onTodoAdd这个onTodoAdd不是我们自己定义的是GeckoView的WebMessageListener注册到页面上下文中的。页面里的脚本通过它把消息投递给原生。这样做的好处是页面脚本自身无法直接访问原生对象它只能通过系统定义好的消息通道发送内容安全边界很清晰。4.3 Android端Application和MainActivityApplication负责创建GeckoRuntime和前面讲过的一样不重复。看看MainActivity重点在消息注册和Todo的逻辑class MainActivity : AppCompatActivity() { private lateinit var geckoSession: GeckoSession private val todoList mutableListOfString() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val geckoView findViewByIdGeckoView(R.id.gecko_view) geckoSession GeckoSession() // 设置内容监听器方便排查页面加载问题 geckoSession.contentDelegate object : GeckoSession.ContentDelegate { override fun onPageStop(session: GeckoSession, success: Boolean) { Log.d(GeckoDemo, 页面加载完成success$success) } } geckoSession.open(App.geckoRuntime) geckoView.setSession(geckoSession) // 注册WebMessageListener监听JS发来的添加Todo消息 geckoSession.registerWebMessageListener( onTodoAdd, object : GeckoSession.WebMessageDelegate { override fun onMessage(session: GeckoSession, message: GeckoSession.WebMessage) { runOnUiThread { val newTodo message.text addTodoAndRefresh(newTodo) } } }, setOf(GeckoSession.WebMessageDelegate.ALLOW_IN_CONTENT_SCRIPTS), setOf() ) // 加载本地HTML geckoSession.load(GeckoSession.Loader().loadUri(resource://android/assets/index.html)) } private fun addTodoAndRefresh(newTodo: String) { todoList.add(newTodo) // 把列表序列化成JSON字符串通过evaluateJS传给页面 val json buildJsonArray(todoList) val script renderTodoList($json) geckoSession.evaluateJS(script) } private fun buildJsonArray(list: ListString): String { val sb StringBuilder([) list.forEachIndexed { index, item - if (index 0) sb.append(,) sb.append(\) sb.append(item.replace(\, \\\).replace(\\, \\\\)) sb.append(\) } sb.append(]) return sb.toString() } override fun onResume() { super.onResume() geckoSession.textInput.onResume() } override fun onPause() { geckoSession.textInput.onPause() super.onPause() } override fun onDestroy() { geckoSession.close() super.onDestroy() } }4.4 这里有几个关键细节必须说透第一个registerWebMessageListener的第三个和第四个参数。第三个参数是允许注入的脚本类型我传了ALLOW_IN_CONTENT_SCRIPTS意思是允许在页面自己的content script里调用这个通道。第四个参数是扩展脚本Demo里用不到就传空集合。如果你把第三个参数传空JS端调用window.wrappedJSObject.onTodoAdd会直接报错消息发不出来。这是我踩过的坑之一。第二个evaluateJS的字符串拼接问题。我在Demo里用了一个解析Json的方法很啰嗦因为Dome里我传的JSON字符串里如果含有单引号或反斜杠直接用字符串模板拼进去会导致JS语法错误。实际项目中我一般会先把数据序列化成JSON字符串然后用JSONObject.quote()或者Android的TextUtils.htmlEncode转义一下再拼到JS脚本里。这个细节处理不好数据里一旦含有引号或特殊字符整段JS代码就会崩。第三个WebMessageDelegate的回调线程。根据我实际测试onMessage回调是在主线程执行的但evaluateJS的JSEvaluationResultDelegate回调不一定在主线程。为了保险起见凡是涉及UI操作的我都用runOnUiThread包一层。第四个如果你的页面是远程的HTTPS URL跨域限制对WebMessageListener依然生效。也就是说如果页面是从https://example.com加载的原生端注册的WebMessageListener只能和https://example.com的页面交互如果页面发生了跳转到另一个域名原来注册的监听会失效。这一点和WebView很不一样WebView里addJavascriptInterface是全域名生效的。GeckoView这么做是安全考虑但也意味着如果你的页面里有跨域跳转你要在原生的onLocationChange里重新注册监听。5. 常见问题与排查技巧实录5.1 白屏问题GeckoView最常见的白屏原因有三个Session没有open就setSession、加载的URL格式不对比如用了file://、还有生命周期没有透传。排查顺序我一般这么来先看Logcat有没有GeckoView报错。再看contentDelegate.onPageStop有没有回调。如果onPageStop回调了但页面还是白屏那就不是加载问题是渲染问题可能是GPU驱动兼容性试试关闭硬件加速GeckoRuntimeSettings.Builder().useHardwareRendering(false)。如果onPageStop都没回调基本就是Session和Runtime没有正确绑定。5.2 JS调用原生没反应大概率是registerWebMessageListener的权限参数没配对。确认第三个参数包含了ALLOW_IN_CONTENT_SCRIPTSJS端确认是通过window.wrappedJSObject.onTodoAdd调用而不是直接window.onTodoAdd。还有一个小坑如果你在页面加载完成前就注册了监听有些情况下GeckoView会丢消息。稳妥的做法是在onPageStop之后再注册或者在Application里初始化的时候就把监听注册好。5.3 evaluateJS返回值拿不到sess.evaluateJS返回的是一个GeckoResultJSValue它是异步的。你需要对这个GeckoResult调用accept()方法传一个回调才能拿到结果。很多人以为直接返回那个值结果回调一直不触发。而且要注意JSValue是一个抽象类要用getString()、getInt()这些getter来取值。如果你传的是复杂对象对方拿到的可能是一个字符串形式的JSON需要手动解析。5.4 调试GeckoView里的页面这是一个大杀器。设置remoteDebuggingEnabled(true)后你可以用Firefox浏览器的about:debugging页面远程连接到App里的GeckoView然后在Firefox的DevTools里查看DOM、断点调试JS、监控网络请求。如果你更习惯Chrome DevTools其实也可以用因为GeckoView的远程调试协议已经支持了Chrome DevTools Protocol的绝大部分接口。在Chrome地址栏输入chrome://inspect理论上能看到GeckoView的调试目标。但据我实测兼容性不如Firefox的DevTools尤其是查看网络请求的时候Firefox那边更稳定。5.5 内存问题GeckoView的Session不用了要记得close。App的Application里创建的Runtime进程结束会自动释放不用手动释放。但Session不一样生命周期和Activity绑定Activity销毁后Session如果不close会一直持有渲染资源内存肉眼可见地涨。另外建议打开GeckoRuntimeSettings里的内存检测相关选项或者在开发者选项里开启不保留活动来测试你的Activity恢复逻辑很多奇怪的白屏问题都是在这种场景下暴露的。5.6 一个容易被忽略的坑脚本执行时机evaluateJS只要Session处于打开状态就能执行但如果页面还没有加载完成你在页面上查找DOM元素会拿到nullJS代码会抛异常。我在项目里用了一个简单粗暴的方案在onPageStop回调里发一个标志消息给页面页面收到后再通知原生说“我准备好了”原生这时候才开始和页面交互。伪代码geckoSession.contentDelegate object : GeckoSession.ContentDelegate { override fun onPageStop(session: GeckoSession, success: Boolean) { // 告诉页面原生准备好了 geckoSession.evaluateJS(window.nativeReady true) } }页面里可以轮询这个标志位或者在JS里定义一个由原生调用的初始化函数比轮询更优雅window.nativeReady false; function onNativeReady() { window.nativeReady true; // 开始主动向原生同步数据 }原生端override fun onPageStop(session: GeckoSession, success: Boolean) { geckoSession.evaluateJS(onNativeReady()) }这个模式是我自己在生产环境里用下来的比在JS里监听DOMContentLoaded事件更可靠因为onPageStop代表的是GeckoView引擎层面的页面加载完成比DOMContentLoaded更晚更保险。6. 进阶扩展让交互更稳健的几个建议6.1 用JSON统一消息格式原生和JS的通信不要裸传字符串尤其是多字段、多类型的业务数据直接拼成JSON再传两边都好处理。原生端用org.json解析JS端用JSON.parse。消息通道本身是字符串这层序列化协议要自己定。我常用的消息格式是{ action: addTodo, data: { content: 写博客, timestamp: 1690000000000 } }原生端解析action字段做路由无需为每种业务单独注册一个WebMessageListener消息通道号可以收敛。6.2 考虑使用GeckoView的WebExtension进行复杂通信如果业务发展到需要多个页面共享一个原生通信通道、需要对页面注入大型脚本、需要监听页面所有请求这时候可以考虑引入WebExtension。它的message API和Chrome扩展的runtime.sendMessage是同一个生态学习曲线缓和一些。不过WebExtension的开发和打包要比单纯的WebMessageListener复杂很多普通App场景不建议直接上。这里提一嘴是让你知道GeckoView在这方面的实力上限。6.3 打包体积优化GeckoView默认包含所有ABIAPK会很大。你可以用ABI拆分来减小包体android { splits { abi { enable true reset() include arm64-v8a, armeabi-v7a, x86_64 universalApk true } } }这样打出来的APK只包含特定CPU架构的so文件能显著减小体积。实际项目中我一般只保留arm64-v8a和armeabi-v7ax86架构只用于模拟器调试打一个单独的debug包用线上包不发x86。6.4 线程模型的总结和避坑最后把线程问题捋一遍。GeckoView的活动线程是单独的渲染进程它是多进程架构和Android主线程之间的所有通信都是异步的。所以页面加载、JS执行发生在GeckoView自己的线程evaluateJS的JSEvaluationResultDelegate回调在GeckoView的线程池线程WebMessageListener.onMessage回调我实测是在主线程但不能保证所有版本都如此保险起见还是切线程onPageStop则是在主线程只要记住回调里不能直接做耗时操作涉及UI的一定要切回主线程这个准则是通用的。我个人的习惯是凡是从GeckoView回调里触发主线程UI操作统一用runOnUiThread包裹或者用一个协程切换到Main dispatcher。没有遇到过一次回调线程导致的崩溃之后我就把这条写到了项目的代码规范里。GeckoView真正上手之后你会发现它比WebView更“重型”但它带来的可控性和一致性确实能让很多复杂场景变得可预期。如果你之前被系统WebView的碎片化问题折磨过值得花一个下午把Demo跑通然后迁移一个小页面试试水。这个技术选型在需要稳定浏览内核的Android应用里正在变得越来越主流。
返回列表