之前接手了一个鸿蒙工程,里面有一个很重的 H5 页面。用户在 App 里来回切换,只要切上几十次,App 绝对闪退。
我拿着性能工具抓了半天内存,发现是 Web 组件和原生的 JavaScriptProxy 内存泄漏。
这篇复盘,讲讲 Web 混合开发里那些极易导致内存爆炸的雷区。
Bridge 里藏了上下文
我们在原生侧注册给 H5 的桥接类(Bridge),往往会用到 Context。
typescript
// 极其容易泄漏的写法
class MyBridge {
context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
// 把上下文存到了成员变量里
this.context = context;
}
showToast(msg: string) {
promptAction.showToast({ message: msg });
}
}
// 注册时
const bridge = new MyBridge(getContext(this) as common.UIAbilityContext);
this.controller.registerJavaScriptProxy(bridge, 'nativeBridge', ['showToast']);
致命点: UIAbilityContext 的生命周期是跟整个 Ability 绑定的。Web 组件被销毁了,但这个 Bridge 对象被底层(C++ 侧)引用着,导致整个 Context 无法释放,最终导致窗口泄漏。
解法: Bridge 里的方法,尽量不要持有长生命周期的上下文。如果非要用,用弱引用,或者通过方法参数动态获取。
忘记注销 Proxy
当用户退出含有 Web 组件的页面时,如果不注销注册的 Proxy,底层依然保留着对原生对象的引用。
必须在页面的 aboutToDisappear 里,把注册的桥接对象全部干掉。
typescript
aboutToDisappear() {
if (this.controller) {
// 注销所有注册到 JS 的 Proxy 对象
this.controller.deleteJavaScriptProxy('nativeBridge');
// 如果 H5 内部还在跑定时器,最好强制停止
this.controller.stop();
}
}
H5 侧的连带责任
很多时候,原生代码没问题,是 H5 那边把原生坑了。
有一次,H5 端为了防止按键连点,在 onClick 里写了个 setInterval 一直调原生的方法。结果 H5 页面关闭时,定时器没清,原生一直在响应,直接卡死。
约定大于配置: 在制定 Bridge 协议时,必须给 H5 前端立规矩。
-
原生向 H5 推送数据,必须由原生主动调用
runJavaScript。 -
H5 调用原生方法,必须保证"短平快",禁止在定时器里高频调用。
混合开发,就像是在两个不同的世界之间修桥。桥修得好,交通顺畅;桥基不牢,全盘崩溃。
-
千万别在 Bridge 类里随意持有 UI 上下文。
-
页面销毁前,务必
deleteJavaScriptProxy。 -
多用
runJavaScript替代 H5 的轮询主动拉取。