做资讯类或者电商类的应用,WebView 是个绕不开的组件。商品详情是 H5 的,活动页是 H5 的,连用户协议都是 H5 的。我之前做活动模块,测试跑了一下午,手机烫得能煎鸡蛋,最后直接闪退,抓日志一看,又是经典的 OutOfMemoryError。

检查了一圈代码,Web 组件在页面销毁的时候确实跟着生命周期销毁了,看起来毫无破绽。但打开 DevEco Profiler 看内存快照,你会发现即使 Web 组件已经不在组件树上了,底层的渲染进程(Render Process)依然稳稳地占着几十甚至上百兆的内存。

这就是 WebView 最容易让人栽跟头的地方:组件销毁,不等于渲染引擎内核被释放。如果你频繁打开新的 Web 页面,系统会不断拉起新的渲染进程,旧的如果不主动清理,内存很快就会见底。

预创建与渲染进程的池化策略

鸿蒙的 Web 组件在底层采用的是多进程架构。主应用是一个进程,负责解析 HTML 和渲染的 Web 内核运行在独立的渲染进程里。这样做的好处是 Web 页面崩溃不会带崩主应用,但坏处是进程的创建和销毁开销很大。

为了优化首屏加载速度,系统其实提供了一套池化机制。你可以通过 webview.WebviewController.initializeWebEngine() 提前初始化内核。很多开发者为了快,在每个页面的 aboutToAppear 里都去调一次,结果就是疯狂创建渲染进程。

正确的做法是在 EntryAbility 的 onCreate 阶段,或者应用冷启动的某个时机,全局初始化一次,然后让所有 Web 页面共享这个引擎环境。

typescript

import { UIAbility } from '@kit.AbilityKit';
import { webview } from '@kit.ArkWeb';

export default class EntryAbility extends UIAbility {
  onCreate(): void {
    // 全局只初始化一次,预热 Web 内核
    webview.WebviewController.initializeWebEngine();
  }
}

关键语句是 initializeWebEngine()。它不会返回一个具体的 WebView 实例,而是告诉系统“我要用 Web 能力了,请把底层的环境准备好”。当你后续打开 Web 组件时,可以复用这个环境,降低启动耗时。但要注意,即使做了全局预热,每个拥有独立 WebviewController 的 Web 组件在销毁时,仍然需要妥善处理。

释放控制器与彻底销毁

为什么 Web 组件随着页面销毁了,内存还是不降?因为持有渲染进程句柄的对象可能还被某个地方引用着。大多数情况下,是 WebviewController 没有被彻底释放。

系统的设计是,一个 Web 组件对应一个 WebviewController。当页面退出,组件触发 aboutToDisappear 时,你必须手动调用控制器的 destroy() 方法,并断开引用。

typescript

@Entry
@Component
struct ArticlePage {
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({ src: 'https://news.example.com', controller: this.controller })
        .onPageEnd(() => {
          // 页面加载完成后的逻辑
        })
    }
  }

  aboutToDisappear() {
    // 页面销毁时必须释放内核资源
    if (this.controller) {
      this.controller.destroy();
    }
  }
}

关键语句是 this.controller.destroy()。这个方法会通知系统回收对应的渲染进程资源。常见错误是只写了 this.controller = null,这仅仅是解除了主进程的引用,如果没有调 destroy(),底层的渲染进程依然处于活跃状态,变成僵尸进程。

另外,如果你的项目里有多个 H5 页面来回跳转(比如使用路由跳转),千万不要把 WebviewController 定义在全局单例里复用。每个 Web 实例必须绑定自己的 Controller,跳转新页面就创建新的,销毁旧页面就清理旧的。

拦截无用请求与减少内存驻留

除了内核进程的释放,Web 页面本身加载的资源也是内存杀手。如果 H5 页面里有巨大的 Base64 图片,或者大量的长视频自动播放,主进程的内存也会被撑爆。

在实际开发中,一定要利用 onInterceptRequest 去拦截那些不必要的资源加载。比如用户网络不好,或者流量受限时,直接拦截掉几兆的高清大图,返回一张本地的占位图。

typescript

Web({ src: 'https://shop.example.com', controller: this.controller })
  .onInterceptRequest((event) => {
    if (!event) return null;
    
    // 拦截特定的大图请求,替换为本地图片
    if (event.request.getRequestUrl().includes('high_res_banner.jpg')) {
      const localRes: WebResourceResponse = {
        data: getContext(this).resourceManager.getRawFdSync('banner_placeholder.jpg').fd,
        mimeType: 'image/jpeg',
        encoding: 'utf-8',
        responseCode: 200,
        reason: 'OK',
        responseData: ''
      };
      return localRes;
    }
    return null;
  })

这段代码的核心在于 onInterceptRequest 回调。当你返回一个自定义的 WebResourceResponse 时,Web 内核就不会去网络上下载原始资源了,而是直接使用你提供的数据。执行结果就是,既节省了流量,又减少了 Web 内核解析大文件时的内存峰值。

处理 WebView 的内存问题,不能只盯着 ArkTS 层的内存快照。你的每一个路由跳转、每一次 Web 组件的创建,都可能连带拉起一个重量级的渲染进程。把初始化提前、把拦截做到位、把 destroy 刻在脑子里,这样才能避免 Web 模块成为拖垮整个 App 的罪魁祸首。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐