公司的主 App 里嵌套了一个庞大的电商 H5 模块,最近被测试团队打回。用户只要在首页和 H5 详情页之间来回切换十几次,应用的内存就会飙升到 2GB 以上,最后系统直接触发强杀。这次重构,让我彻底看清了 ArkWeb 组件里那些隐藏极深的内存陷阱。

被 JavaScriptProxy 锁死的生命周期
为了实现 H5 点击购买拉起原生支付,我注册了原生对象。起初代码跑得很顺利,但内存一直在持续泄漏。
这里藏着一个极深的警示:registerJavaScriptProxy 在底层 C++ 侧是全局注册的。如果你的页面被销毁了,但你没有显式去解绑,底层的渲染进程会一直持有你的 ArkTS 对象引用,导致整个页面的内存都无法被 GC 回收。
每次离开页面,必须把注册进去的桥梁(Bridge)拆掉。

typescript

@Entry
@Component
struct ShopH5Page {
  controller: webview.WebviewController = new webview.WebviewController();
  bridge: NativeBridge = new NativeBridge();

  aboutToAppear() {
    // 注册桥接对象
    this.controller.registerJavaScriptProxy(this.bridge, 'nativePay', ['startPay']);
  }

  aboutToDisappear() {
    // 警示:必须注销,否则页面内存绝不释放
    if (this.controller) {
      this.controller.deleteJavaScriptProxy('nativePay');
      // 如果H5内部还在播放视频或音频,必须强制停止
      this.controller.stop(); 
    }
  }
}

通信的高频风暴与序列化开销
H5 里的商品列表使用了无限滚动,每次滑动到底部,H5 就会调用原生网络请求去拉取下一页。当时 H5 端为了获取当前网络状态,写了一个定时器,每秒向原生发送一次 ping 请求。
这就导致主线程和 Web 渲染进程之间每秒都在发生跨进程通信(IPC)。跨语言传输 JSON 字符串的开销极大,直接把 UI 线程卡住了。
正确的做法是,把频繁的主动查询改为被动通知。原生侧监听网络状态,一旦状态改变,再通过 runJavaScript 注入给 H5。减少通信频率,是提升混合应用流畅度的第一步。

跨域限制下的本地资源加载
为了加速首屏,我们把商品详情页的静态资源全部打进了 rawfile 里。用 resource:// 协议加载页面。但一加载页面,控制台就报错说 H5 里的 Ajax 请求跨域了。
原来,resource:// 协议在 Web 组件里默认是不允许发起网络请求的。如果你不能修改服务端配置,唯一的解法是在原生层做请求拦截,或者通过 runJavaScript 在 H5 启动前注入一段脚本来重写 XMLHttpRequest。

typescript

// 在页面加载前注入,把请求代理给原生
const proxyScript = `
  const originalFetch = window.fetch;
  window.fetch = async function(url, options) {
    if (url.startsWith('http')) {
      // 转交给原生
      return window.nativeNetwork.request(url, JSON.stringify(options));
    }
    return originalFetch(url, options);
  };
`;
this.controller.runJavaScript(proxyScript);

渲染管线的降维打击
注意,Web 组件非常吃 GPU 资源。如果你在 H5 页面里用了复杂的 CSS3 动画,比如阴影、渐变和滤镜,能明显感觉到手机背部发热发烫。在鸿蒙的混合开发规范里,能用原生的 Text 和 Image 解决的,千万不要用 Web 组件去渲染。Web 组件只适合处理排版极其复杂的富文本和表单。
在你的混合应用里,写一个 H5 页面,里面放三个按钮,分别调用原生的弹窗、获取设备信息和关闭页面。给页面加上一个原生侧的 aboutToDisappear 打印。如果反复进出该页面十次,控制台的内存占用没有成倍增长,说明你的清理逻辑完全合格。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐