1. 为什么要在 Cocos Creator 里嵌入 WebView
做过小游戏或者互动应用的朋友大概率都遇到过这种需求:游戏主逻辑跑在 Cocos Creator 里,但某个模块——比如活动页、排行榜、用户协议、支付说明、甚至是内嵌的 H5 小游戏——用原生引擎写成本太高,用 Web 技术栈反而半天就能搞定。这时候最自然的想法就是:能不能在 Cocos 的场景里直接嵌一个浏览器窗口,把 H5 页面塞进去?
答案是可以的,而且这是 Cocos Creator 跨平台开发里一个相当实用的能力。所谓WebView 嵌入,本质上就是在原生层(Android 的WebView、iOS 的WKWebView)创建一个真正的系统浏览器组件,然后把它作为一个原生视图叠加在游戏渲染层之上,再通过桥接机制让 JS 层和原生层互相通信。它和引擎自带的cc.WebView组件不是一回事——后者在原生平台上其实也是走系统 WebView,但在 Web 平台上就是个 iframe,行为差异很大。
这个能力解决的核心问题是混合开发的边界问题:把适合 Web 做的交给 Web,把适合引擎做的交给引擎,两者通过消息通道协作。适合谁来参考?我建议是有一定 Cocos Creator 使用经验、同时懂一点 Android 或 iOS 原生开发的同学。如果你完全没碰过原生层,这篇文章也能让你看懂整体链路,但真正落地时最好找个原生同学配合,或者自己补一下 Java/Kotlin 和 Objective-C/Swift 的基础。
我前后在三个项目里用过这套方案,踩过的坑不算少,从最早的"WebView 盖不住、点不动"到后来的"键盘弹起把游戏顶飞",每一个都是血泪。下面我把整套思路、实现细节和避坑经验完整拆一遍,你可以直接抄作业。
2. 整体方案设计与技术选型思路
2.1 三种可选路径的取舍
在 Cocos Creator 里实现 WebView 嵌入,市面上大致有三条路,我先把它们摆出来对比,你再决定走哪条。
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 引擎自带 cc.WebView | 用引擎封装好的组件 | 跨平台统一 API,写起来快 | 原生层定制能力弱,通信受限,样式不可控 | 简单展示型页面 |
| 原生自定义 WebView + 桥接 | 自己写 Android/iOS 原生代码,暴露接口给 JS | 完全可控,通信灵活,性能好 | 需要原生开发能力,双端各写一套 | 复杂交互、需要深度定制 |
| 纯 H5 页面跳转 | 直接打开系统浏览器或新页面 | 零成本 | 脱离游戏上下文,体验割裂 | 外链、协议类页面 |
我个人的结论很明确:只要涉及 JS 与游戏逻辑的双向通信、或者需要控制 WebView 的位置大小和生命周期,就别用引擎自带的组件,直接上原生自定义方案。引擎自带的cc.WebView在 Web 平台上是 iframe,在原生平台上是系统 WebView,但它的 API 太薄,你没法精细控制,比如你想让 WebView 只占屏幕下半部分、想监听页面加载进度、想在页面里调用游戏的方法,它都做得很别扭。
2.2 桥接通信的设计原则
选定原生自定义方案后,核心就是设计 JS 和原生之间的通信桥。这里有个关键原则:通信要单向解耦,不要互相直接调用。
什么意思?就是 JS 层不应该直接去调原生的方法,原生也不应该直接去调 JS 的函数,而是通过一个统一的消息通道,双方都往通道里发消息、从通道里收消息。这样做的好处是:双端实现可以独立演进,加一个新消息类型不用改对方的代码结构,调试的时候也容易打日志。
具体到实现上,Android 侧通常用addJavascriptInterface注入一个 Java 对象给页面,页面通过window.xxx.postMessage()调用;iOS 侧用WKScriptMessageHandler接收页面消息,用evaluateJavaScript往页面里发消息。Cocos 的 JS 层则通过引擎提供的原生反射机制(native.reflection或jsb)来调用原生暴露的方法。
注意:
addJavascriptInterface在 Android 4.2 以下有安全漏洞,虽然现在最低版本早就过了,但注入的对象方法一定要加@JavascriptInterface注解,否则高版本上调用无效,这个坑我见过不止一个同学踩。
2.3 渲染层级与触摸事件的处理
这是整个方案里最容易翻车的地方。WebView 是原生视图,它和游戏画面不在同一个渲染管线里。在 Android 上,Cocos 的 GLSurfaceView 和 WebView 是两个独立的 View,谁盖在谁上面取决于 View 树的添加顺序和 Z 轴顺序。在 iOS 上,Cocos 的视图和 WKWebView 也是兄弟关系,需要手动管理层级。
核心矛盾在于:WebView 显示的时候,游戏画面还在下面跑,但触摸事件会被 WebView 拦截。如果你希望 WebView 显示时游戏暂停响应触摸,那没问题;但如果你希望 WebView 只占一部分区域、其他区域还能操作游戏,那就需要精确控制 WebView 的 frame 和触摸区域。
我的做法是:WebView 显示时,在 JS 层用一个全屏遮罩拦截所有触摸,只把需要穿透的区域通过原生接口设置成 WebView 的实际大小。这样逻辑清晰,不会出现"点到了游戏按钮但 WebView 没反应"的诡异情况。
3. 核心细节解析与实操要点
3.1 Android 侧的关键实现
Android 侧的核心工作是创建一个 WebView,把它加到 Activity 的根布局上,并暴露通信接口。我先说几个关键点。
第一,WebView 必须加在主线程,这个不用多说,Android 的 View 操作都必须在 UI 线程。Cocos 的原生调用默认就在主线程,所以一般没问题,但如果你在子线程里触发显示,记得用runOnUiThread包一层。
第二,WebView 的布局参数要动态计算。Cocos 的游戏画面是全屏的,WebView 如果要占屏幕的一部分,你需要把游戏坐标系转换成 Android 的像素坐标系。这里涉及一个坑:Cocos 的设计分辨率、屏幕实际分辨率、Android 的 dp/px 三者不是一回事。我的经验是,在 JS 层算好比例,把 0 到 1 的相对坐标传给原生,原生再乘以实际屏幕宽高,这样双端都不用关心具体分辨率。
第三,WebView 的销毁要及时。很多同学显示完 WebView 就忘了销毁,结果内存一路涨。正确的做法是:隐藏时调用removeView并从父布局移除,必要时调用destroy()释放资源。如果只是隐藏,至少也要onPause暂停页面里的定时器和视频。
// 简化的 Android 侧核心逻辑示意 public class WebViewBridge { private WebView webView; private Activity activity; public void showWebView(final String url, final float x, final float y, final float width, final float height) { activity.runOnUiThread(new Runnable() { @Override public void run() { if (webView == null) { webView = new WebView(activity); WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); webView.addJavascriptInterface(new JsBridge(), "cocosBridge"); } // 根据相对坐标计算实际像素 int screenW = activity.getWindowManager() .getDefaultDisplay().getWidth(); int screenH = activity.getWindowManager() .getDefaultDisplay().getHeight(); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( (int)(width * screenW), (int)(height * screenH)); params.leftMargin = (int)(x * screenW); params.topMargin = (int)(y * screenH); webView.setLayoutParams(params); if (webView.getParent() == null) { activity.addContentView(webView, params); } webView.loadUrl(url); } }); } }这段代码是示意,实际项目里我会把 WebView 的创建和显示拆开,创建时只初始化一次,显示时只调整布局和可见性,避免重复创建带来的闪烁。
3.2 iOS 侧的关键实现
iOS 侧用WKWebView,比 Android 的 WebView 性能好很多,但配置也更繁琐。几个关键点:
WKWebView 的创建必须在主线程,这个和 Android 一样。配置WKWebViewConfiguration时要开启 JS 执行,并且注册WKScriptMessageHandler来接收页面消息。注意,WKScriptMessageHandler会造成循环引用,记得在合适的时候移除,否则 WebView 不会释放。
iOS 的坐标系统是左上角原点,和 Android 一致,但要注意 Retina 屏幕的 scale 问题。Cocos 的视图和 WKWebView 都在同一个坐标系下,直接用UIScreen.main.bounds计算即可。
// 简化的 iOS 侧核心逻辑示意 - (void)showWebView:(NSString *)url x:(CGFloat)x y:(CGFloat)y width:(CGFloat)width height:(CGFloat)height { if (!self.webView) { WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; [config.userContentController addScriptMessageHandler:self name:@"cocosBridge"]; self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config]; } CGRect screen = [UIScreen mainScreen].bounds; self.webView.frame = CGRectMake(x * screen.size.width, y * screen.size.height, width * screen.size.width, height * screen.size.height); if (!self.webView.superview) { [[[UIApplication sharedApplication] keyWindow] addSubview:self.webView]; } [self.webView loadRequest:[NSURLRequest requestWithURL: [NSURL URLWithString:url]]]; }提示:iOS 上 WKWebView 的键盘处理是个大坑。当页面里有输入框、键盘弹起时,系统会自动调整视图,可能把整个游戏画面顶上去。解决办法是监听键盘通知,手动把游戏视图的 frame 恢复回去,或者干脆在 WebView 显示时把游戏视图隐藏。
3.3 JS 层与原生层的通信封装
JS 层要做的事情有两件:调用原生方法显示/隐藏 WebView,以及接收原生转发过来的页面消息。
Cocos Creator 的原生反射调用,在 3.x 版本里用native.reflection.callStaticMethod(Android)和native.reflection.callStaticMethod(iOS 的类方法)。但更推荐的做法是在原生层封装一个统一的入口类,JS 只调这一个类的方法,这样双端接口一致,JS 层不用写平台判断。
// JS 层封装示意 const WebViewBridge = { show(url, x, y, width, height) { if (sys.isNative) { if (sys.platform === sys.Platform.ANDROID) { jsb.reflection.callStaticMethod( "com/example/WebViewBridge", "showWebView", "(Ljava/lang/String;FFFF)V", url, x, y, width, height ); } else if (sys.platform === sys.Platform.IOS) { jsb.reflection.callStaticMethod( "WebViewBridge", "showWebView:x:y:width:height:", url, x, y, width, height ); } } }, hide() { // 类似地调用原生的 hide 方法 } };这里有个细节:Android 的反射调用签名必须严格匹配,(Ljava/lang/String;FFFF)V表示参数是 String 加四个 float,返回值 void。写错了会直接崩溃或者静默失败,调试起来很痛苦。我的建议是先在原生层写一个最简单的无参方法测试通路,通了再逐步加参数。
3.4 消息通道的双向设计
通信是双向的:游戏要告诉页面一些状态(比如用户 ID、当前关卡),页面也要告诉游戏一些事件(比如用户点了某个按钮、页面加载完成)。
我的设计是统一用 JSON 字符串传递消息,消息体里带一个type字段区分类型。这样双端解析逻辑一致,扩展新消息类型只需要加一个 type,不用改通道本身。
// 游戏发给页面的消息 const msg = JSON.stringify({ type: "userInfo", data: { uid: "12345" } }); // 通过原生接口注入到页面 nativeBridge.evaluateJs(`window.onGameMessage(${msg})`); // 页面发给游戏的消息(页面里调用) window.cocosBridge.postMessage(JSON.stringify({ type: "pageReady" }));原生层收到页面消息后,再通过 Cocos 的事件机制转发给 JS 层。Cocos 3.x 里可以用native.bridge或者自定义的事件派发。我一般会在原生层维护一个消息队列,JS 层定时拉取或者原生主动推送,看具体场景。
注意:
evaluateJavaScript在 iOS 上必须在主线程调用,而且如果页面还没加载完,调用会失败。稳妥的做法是等页面didFinishNavigation回调后再发消息,或者在页面里主动发一个pageReady消息,游戏收到后再发数据。
4. 完整实操流程与核心环节实现
4.1 环境准备与项目结构
先说环境。Cocos Creator 我用的是 3.8 版本,Android 用 Android Studio,iOS 用 Xcode。构建原生项目时,Cocos 会生成一个build/android和build/ios目录,我们的原生代码就加在这两个目录里。
但这里有个大坑:每次重新构建,Cocos 会覆盖 build 目录,你手写的原生代码会丢。解决办法有两个:一是把原生代码放在 Cocos 的native/engine模板目录里,构建时会自动拷贝;二是用 git 管理,构建后手动合并。我推荐第一种,虽然配置麻烦点,但一劳永逸。
具体做法是在项目根目录建一个native/engine/android和native/engine/ios目录,把自定义的 Java/OC 文件放进去,然后在构建配置里指定模板路径。Cocos 3.x 支持自定义构建模板,这个在官方文档里有说明,我这里不展开。
4.2 Android 侧完整实现步骤
第一步,在native/engine/android下创建WebViewBridge.java,放在合适的包名下。这个类要提供showWebView、hideWebView、evaluateJs三个静态方法。
第二步,showWebView里创建 WebView 并添加到 Activity 的根布局。这里要注意,Cocos 的 Activity 根布局可能不是 FrameLayout,你需要先找到合适的父容器。我的做法是用activity.findViewById(android.R.id.content)拿到内容根布局,它一定是 FrameLayout,直接往里加就行。
第三步,配置 WebView 的WebSettings。必须开启的项:setJavaScriptEnabled(true)、setDomStorageEnabled(true)(页面用 localStorage 时需要)、setAllowFileAccess(true)(加载本地文件时需要)。如果页面要播放视频,还要设置setMediaPlaybackRequiresUserGesture(false)。
第四步,注入 JS 接口。addJavascriptInterface(new JsBridge(), "cocosBridge"),JsBridge类里定义@JavascriptInterface标注的postMessage方法,收到消息后通过 Cocos 的机制转发。
第五步,处理返回键。WebView 显示时,用户按返回键应该先关闭 WebView 而不是退出游戏。这需要在 Activity 的onBackPressed里判断,如果 WebView 可见就先隐藏它。
@Override public void onBackPressed() { if (WebViewBridge.isVisible()) { WebViewBridge.hideWebView(); return; } super.onBackPressed(); }4.3 iOS 侧完整实现步骤
iOS 侧稍微复杂一点,因为要处理WKScriptMessageHandler的循环引用和键盘问题。
第一步,创建一个WebViewBridge类,实现WKScriptMessageHandler协议。注意用弱引用持有自己,避免循环引用。
第二步,showWebView里创建 WKWebView 并添加到 keyWindow 上。这里要注意,Cocos 的视图控制器可能不是 keyWindow 的 rootViewController,直接加到 keyWindow 上是最稳妥的。
第三步,注册消息处理器。[config.userContentController addScriptMessageHandler:self name:@"cocosBridge"],然后在userContentController:didReceiveScriptMessage:里处理消息。
第四步,处理键盘。监听UIKeyboardWillShowNotification和UIKeyboardWillHideNotification,在键盘弹起时把游戏视图的 frame 恢复,或者把 WebView 的 frame 上移。
- (void)keyboardWillShow:(NSNotification *)notification { CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; // 把 WebView 上移,避免被键盘遮挡 CGRect frame = self.webView.frame; frame.origin.y -= keyboardFrame.size.height; self.webView.frame = frame; }第五步,销毁时移除消息处理器。[config.userContentController removeScriptMessageHandlerForName:@"cocosBridge"],否则 WebView 不会释放,内存泄漏。
4.4 JS 层的调用与状态管理
JS 层我建议封装一个单例的WebViewManager,管理 WebView 的显示状态、消息队列和回调。
class WebViewManager { constructor() { this.isShowing = false; this.messageHandlers = {}; } show(url, rect) { if (this.isShowing) return; this.isShowing = true; // 显示遮罩,拦截游戏触摸 this.showMask(); WebViewBridge.show(url, rect.x, rect.y, rect.width, rect.height); } hide() { if (!this.isShowing) return; this.isShowing = false; this.hideMask(); WebViewBridge.hide(); } onMessage(type, handler) { this.messageHandlers[type] = handler; } dispatchMessage(jsonStr) { const msg = JSON.parse(jsonStr); const handler = this.messageHandlers[msg.type]; if (handler) handler(msg.data); } }遮罩的作用是拦截游戏触摸,防止用户在 WebView 上操作时误触游戏。遮罩本身是一个全屏的cc.Node,挂一个BlockInputEvents组件即可。
4.5 参数计算与坐标转换
坐标转换是实操中最容易出错的地方。Cocos 的坐标系是左下角原点,Android 和 iOS 都是左上角原点,所以 Y 轴要翻转。
假设游戏设计分辨率是 960x640,实际屏幕是 1920x1080,你想让 WebView 占屏幕中间 80% 宽、60% 高,居中显示。那么:
- 相对宽度 = 0.8,相对高度 = 0.6
- 相对 X = (1 - 0.8) / 2 = 0.1
- 相对 Y = (1 - 0.6) / 2 = 0.2
传给原生的就是(0.1, 0.2, 0.8, 0.6),原生再乘以实际屏幕宽高。这样双端都不用关心具体分辨率,适配性最好。
提示:如果 WebView 要贴底显示(比如底部弹窗),相对 Y 就是
1 - 高度。如果贴顶,相对 Y 就是 0。这些计算在 JS 层做,原生只负责乘屏幕尺寸。
5. 常见问题与排查技巧实录
5.1 WebView 显示白屏或加载失败
这是最常见的问题,原因通常有三个:网络权限没加、URL 不对、页面本身报错。
Android 上,AndroidManifest.xml里必须加<uses-permission android:name="android.permission.INTERNET" />,这个漏了会直接白屏。iOS 上,如果加载的是 HTTP 页面,需要在Info.plist里配置NSAppTransportSecurity允许任意加载,否则会被系统拦截。
排查方法:先在原生层加日志,看onPageStarted和onPageFinished有没有回调。如果onPageStarted都没触发,说明 URL 或网络有问题;如果触发了但页面空白,说明页面本身有 JS 错误,用 Chrome 的chrome://inspect或者 Safari 的开发者工具连上去看控制台。
5.2 触摸事件穿透或无法点击
WebView 显示后,游戏按钮还能点到,或者 WebView 里的按钮点不动,都是触摸事件处理的问题。
游戏按钮还能点到,说明遮罩没生效或者遮罩层级不对。检查遮罩节点的zIndex是否高于其他 UI,以及BlockInputEvents组件是否挂上。
WebView 里的按钮点不动,通常是 WebView 的 frame 设置有问题,或者被其他原生视图遮挡。检查 WebView 的LayoutParams是否正确,以及是否被添加到了正确的父容器。
5.3 键盘弹起导致画面错乱
iOS 上这个问题特别明显。键盘弹起时,系统会自动调整 keyWindow 的 frame,导致整个游戏画面被顶上去。
解决办法是在键盘通知里手动恢复游戏视图的 frame。具体做法是:在keyboardWillShow里记录当前游戏视图的 frame,然后把 WebView 上移;在keyboardWillHide里把 WebView 移回原位。如果游戏视图也被顶了,同样需要恢复。
Android 上默认不会顶起整个画面,但如果 Activity 的windowSoftInputMode设置成了adjustResize,也会出现类似问题。改成adjustPan或者adjustNothing可以避免。
5.4 内存泄漏与 WebView 销毁
WebView 不销毁会导致内存持续增长,尤其是反复打开关闭的场景。
Android 上,隐藏 WebView 时要removeView,必要时destroy()。但注意,destroy()后 WebView 就不能再用了,下次显示要重新创建。我的做法是:如果只是临时隐藏,用setVisibility(GONE);如果确定不再用了,才destroy()。
iOS 上,WKWebView 的销毁要移除WKScriptMessageHandler,否则会循环引用。另外,如果页面里有视频或定时器,要在隐藏时暂停。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 白屏 | 网络权限缺失 | 检查 Manifest/Info.plist | 添加 INTERNET 权限 |
| 白屏 | URL 错误 | 看 onPageStarted 回调 | 修正 URL,加日志 |
| 白屏 | 页面 JS 报错 | 连开发者工具看控制台 | 修复页面代码 |
| 触摸穿透 | 遮罩未生效 | 检查遮罩层级和组件 | 挂 BlockInputEvents |
| 按钮点不动 | frame 错误 | 检查 LayoutParams | 修正坐标计算 |
| 键盘顶飞画面 | 系统自动调整 | 监听键盘通知 | 手动恢复 frame |
| 内存泄漏 | WebView 未销毁 | 监控内存曲线 | removeView + destroy |
| 消息收不到 | 桥接未注册 | 检查 addJavascriptInterface | 确认注解和名称 |
5.6 几个我踩过的独家坑
第一个坑:Android 上addJavascriptInterface注入的对象,方法名如果和系统方法重名,会被覆盖。我有个项目里注入的方法叫close,结果和 WebView 自带的close冲突,页面调用时行为诡异。后来改成postMessage就正常了。所以注入的方法名最好加前缀,比如cocos_。
第二个坑:iOS 上evaluateJavaScript在页面重定向时会丢失。如果页面加载后发生了 302 跳转,之前注入的 JS 上下文会失效。解决办法是监听didFinishNavigation,每次导航完成后重新注入。
第三个坑:WebView 的缓存会导致页面更新不生效。开发阶段经常遇到改了 H5 页面但 WebView 里还是旧版,这是因为缓存。可以在 URL 后面加时间戳,或者设置setCacheMode(LOAD_NO_CACHE)。但生产环境不要禁用缓存,否则每次加载都慢。
第四个坑:Cocos 的构建会覆盖原生代码。前面提过,一定要把原生代码放在模板目录里,或者用 git 管理。我有一次构建后忘了合并,调试了半天才发现代码被覆盖了。
6. 性能优化与进阶玩法
6.1 加载速度优化
WebView 的首次加载通常比较慢,尤其是页面资源多的时候。几个优化方向:
预创建 WebView。在游戏启动时就创建好 WebView 实例,只是不显示。这样真正需要显示时,只需要 loadUrl,省去了创建的开销。但注意,预创建会占用内存,低端机上要权衡。
预加载页面。如果知道用户大概率会打开某个页面,可以在后台先 loadUrl,显示时直接展示。这个适合活动页这种确定性的场景。
资源本地化。把 H5 页面的 JS、CSS、图片打包到本地,用file://协议加载,速度比网络加载快很多。但要注意跨域问题,以及本地文件的更新机制。
6.2 通信性能优化
频繁的 JS 与原生通信是有开销的,尤其是大量数据传输时。几个优化点:
批量发送。不要每条消息都单独发,攒一批一起发。比如游戏状态同步,可以每 100ms 发一次,而不是每次变化都发。
减少数据量。传 JSON 时只传必要字段,不要整个对象序列化。大数组考虑用二进制或者分片传输。
避免同步调用。evaluateJavaScript在 iOS 上是异步的,但 Android 的loadUrl("javascript:...")也是异步的。不要指望调用后立即拿到返回值,用回调或事件机制。
6.3 进阶:WebView 与游戏的深度协作
基础的双向通信做好后,可以玩一些更高级的:
游戏画面截图传给页面。用 Cocos 的camera渲染到 RenderTexture,转成 base64 传给页面显示。适合做分享图、成绩单。
页面控制游戏角色。页面里的按钮点击后,通过消息通道调用游戏的方法,控制角色移动或释放技能。适合做虚拟手柄、活动交互。
页面内嵌游戏小游戏。页面里再嵌一个 Cocos 的 Web 版小游戏,通过 postMessage 和主游戏通信。这个比较重,但技术上可行。
提示:深度协作时,消息协议要设计好版本号,方便后续扩展。我一般会在消息体里加一个
v字段,页面和游戏都根据版本号做兼容处理。
7. 我个人在实际操作中的几点体会
这套方案我从最早用引擎自带组件,到后来自己写原生桥接,前后迭代了三四版。最大的体会是:不要试图用一套代码解决所有平台的问题。Android 和 iOS 的 WebView 行为差异很大,强行统一 API 只会让代码越来越乱。我的做法是 JS 层只定义接口,双端各自实现,接口一致但内部逻辑完全独立。
另一个体会是日志一定要打全。WebView 的问题往往出在原生层,JS 层看不到。我在原生层的每个关键节点都加了日志,显示、隐藏、加载开始、加载完成、收到消息、发送消息,全部打出来。出问题时看日志基本能定位到是哪一环断了。
还有一点,测试一定要在真机上做。模拟器和真机的 WebView 行为差异很大,尤其是键盘、触摸、性能这些。我早期在模拟器上测得好好的,一到真机就各种问题。现在我的习惯是,只要涉及原生功能,一律真机测试,模拟器只用来快速验证逻辑。
最后分享一个小技巧:如果 WebView 里的页面需要和游戏共享登录态,不要通过 URL 传 token,那样不安全也不优雅。正确的做法是页面加载完成后,游戏通过消息通道把 token 发给页面,页面存到内存里,后续请求带上。这样 token 不会出现在 URL 里,也不会被浏览器历史记录泄露。
这套方案后续还可以扩展的方向很多,比如支持多个 WebView 实例、支持 WebView 动画过渡、支持页面与游戏共享本地存储等。我目前正在做的是把 WebView 的显示隐藏做成一个可配置的转场动画,让体验更顺滑。如果你也在做类似的东西,欢迎交流踩坑经验。