最近在排查主应用的 OOM 闪退问题,发现了一个极其隐蔽的源头。用户只要在原生首页和嵌套了 H5 的活动详情页之间来回切换十几次,应用的内存曲线就会像火箭一样飙升,最后被系统强行终止。起初我以为是 H5 内部的定时器没有清理,结果接了 DevEco Studio 的 Profiler 一看,全是 ArkTS 侧的原生对象没有释放。
在混合开发中,Web 组件并不是一个简单的"浏览器套壳",它的底层是一整套独立的渲染内核。如果不去主动干预,它会把你的原生内存吃得连骨头都不剩。
解除 JavascriptProxy 的内存锁定
为了实现 H5 唤起原生支付,我们用 registerJavaScriptProxy 把原生对象挂载到了 H5 的 window 对象上。起初为了图方便,只要页面加载完成就去注册一遍。
这其实是个极其危险的做法。底层的跨语言通信引擎(C++ 层)为了保证 H5 随时能调用,会长期持有你传入的 ArkTS 对象引用。只要你不主动解绑,这个对象连带它所引用的整个页面上下文,统统无法被垃圾回收器(GC)回收。
typescript
import webview from '@ohos.web.webview';
@Entry
@Component
struct ActivityPage {
private controller: webview.WebviewController = new webview.WebviewController();
private payBridge: PayBridge = new PayBridge();
build() {
Web({ src: 'https://m.example.com/activity', controller: this.controller })
.onControllerAttached(() => {
// 正确做法:在控制器挂载时仅注册一次
this.controller.registerJavaScriptProxy(this.payBridge, 'NativePay', ['startPay']);
})
}
aboutToDisappear() {
// 核心防线:页面销毁前必须解绑,切断底层引用
if (this.controller) {
this.controller.deleteJavaScriptProxy('NativePay');
}
}
}
这段代码的关键执行结果就是:当用户离开页面时,底层代理被销毁,内存占用瞬间回落。如果漏掉 deleteJavaScriptProxy,你进出一个页面 10 次,内存里就会堆积 10 个 PayBridge 实例。
停止渲染引擎的后台运行
你以为页面退出了,Web 组件就在休息了?并没有。底层的 Chromium 渲染引擎可能还在后台跑着动画,或者维持着网络连接。
很多开发者只用了 router.back() 就以为万事大吉,实际上必须显式通知 Web 内核停止工作。
typescript
aboutToDisappear() {
if (this.controller) {
this.controller.deleteJavaScriptProxy('NativePay');
// 强制停止网页加载和所有后台活动(动画、音频、视频)
this.controller.stop();
}
}
执行这行代码后,通过性能检测工具你会发现,后台的 CPU 占用率会立刻呈现断崖式下跌。这对于缓解手机发热和耗电有着立竿见影的效果。
跨域请求与端侧代理注入
为了提升首屏速度,我们把 H5 资源打包进了本地的 rawfile 目录。结果页面一打开,所有的接口请求全部报 CORS 跨域错误。因为在 resource:// 协议下,浏览器的同源策略极其严苛。
如果无法让后端配合修改响应头,最稳妥的方案是在 H5 加载前注入一段 JS 脚本,把网络请求"劫持"到原生侧来执行。
typescript
// 注入脚本,重写 fetch
const proxyScript = `
const originalFetch = window.fetch;
window.fetch = async function(url, options) {
if (url.startsWith('https://api.example.com')) {
// 转交给原生的网络库发送
const res = await window.NativeHttp.request(url, JSON.stringify(options));
return new Response(res);
}
return originalFetch.apply(this, arguments);
};
`;
this.controller.runJavaScript(proxyScript);
这段代码跑完后,H5 里的请求会被无缝移交给原生,彻底绕开了浏览器限制。不过要注意,桥接通信会有序列化开销,不建议用来传输超大体积的二进制数据。
软键盘平移与避让模式
在 H5 的输入框打字时,软键盘一弹出来,整个 Web 页面就被顶得飞起,连顶部的原生导航栏都不见了。
这是因为默认的键盘避让策略是平移窗口。必须将其改为压缩窗口高度。
typescript
// 在 EntryAbility 的 onWindowStageCreate 中配置
windowStage.getMainWindow().then((win) => {
win.setWindowLayoutFullScreen(true);
// 关键警示:必须设为 RESIZE,否则页面会被整体推飞
win.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
});
改用 RESIZE 后,键盘弹出只会压缩窗口高度,底部的输入框会自适应滚动到可视区域,页面顶部的元素则岿然不动。综上所述,混合开发绝不是简单的拼接,管好桥接的生命周期,做好跨域兜底,才是稳定运行的前提。