ArkWeb 混合开发的内存清理与桥接陷阱

最近在排查主应用的 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 后,键盘弹出只会压缩窗口高度,底部的输入框会自适应滚动到可视区域,页面顶部的元素则岿然不动。综上所述,混合开发绝不是简单的拼接,管好桥接的生命周期,做好跨域兜底,才是稳定运行的前提。