做鸿蒙混合应用开发,很多人第一次上手 ArkWeb,都会犯同一个错误:直接丢一个 Web 组件,填上 H5 链接,跑起来看到网页正常展示,就觉得大功告成。看起来一切正常,但只要多测试几轮,各种诡异问题就会陆续冒出来。

来回进出这个页面五六次之后,APP 内存持续上涨,越来越卡,最后直接闪退;页面已经返回销毁了,H5 页面里的定时器还在后台疯狂执行;偶尔跳转 H5 页面出现随机白屏;甚至页面销毁之后,JSBridge 的异步回调还在往原生层回传数据,直接触发空指针崩溃。

我自己在 HarmonyOS7 项目做混合页面的时候,就踩过这套坑。当时项目赶迭代,写完基础 Web 页面就直接提交测试。测试反馈反复打开页面 APP 会卡顿闪退,我排查整整一下午,最后定位根源:Web 实例没有和鸿蒙页面生命周期绑定,没有做资源释放。

绝大多数新手学习 ArkWeb,关注点只停留在 “怎么把 H5 页面显示出来”,完全忽略 ArkWeb 不是一个简单的 UI 组件,它背后是一套独立的 Web 渲染内核。页面的创建、前台显示、切后台、销毁,每一个阶段都要同步控制 Web 内核。一旦生命周期管理缺失,内存泄漏、白屏、残留回调这类 bug 会源源不断。

本篇手记,我们从 0 开始搭建 HarmonyOS7 环境下 ArkWeb 页面,实现远程 H5 页面加载、加载状态监听,把 Web 实例和页面生命周期完整绑定,同时处理加载失败兜底页面。

前置环境准备:DevEco Studio,编辑器切换白色皮肤,项目 SDK 选择 HarmonyOS7,模拟器或者真机都可以调试。 阅读建议:不要直接复制代码跑,跟着文章逐段敲,边修改边观察运行效果,更容易理解问题。

一、先搞懂:HarmonyOS7 里 ArkWeb 到底是什么

ArkWeb 是鸿蒙内置的 Web 渲染内核组件,核心作用就是在原生 ArkTS 页面里面嵌入网页。既可以加载公网远程 H5 链接,也能加载打包进应用 rawfile 目录里的本地静态 HTML 页面。

很多开发者把 ArkWeb 当成一个黑盒子:只要填入 src 网页地址,组件自动渲染页面。这个理解只对了一半。 ArkWeb 内部会单独启动渲染进程、JS 执行环境。只要 Web 实例没有主动销毁,H5 页面内部的 js 定时器、事件监听、网络请求会持续运行。哪怕原生页面已经关闭,从页面栈退出了,Web 内核的资源依旧驻留在内存里面。

我习惯用一个生活化的类比来理解这个关系:原生页面是一间房间,ArkWeb 就是房间里面的电视机。

  1. aboutToAppear:房间准备就绪,电视机准备上电;
  2. onPageShow:进入房间,电视机开机播放画面;
  3. onPageHide:暂时离开房间,电视机继续播放,消耗电量;
  4. aboutToDisappear:彻底离开房间,必须断电拆掉电视机。如果你不做销毁,电视机持续耗电播放,也就是我们常说的内存泄漏。

所谓 ArkWeb 生命周期管理,本质就是页面状态发生变化的时候,同步控制这套 Web 内核的启动、暂停、恢复、销毁。

这里有一个非常关键的提醒:我们现在使用的是HarmonyOS7,不是 7.0。HarmonyOS7 的 ArkWeb API 接口有小幅调整,和旧版本存在差异,直接照搬老版本鸿蒙的 Web 代码,大概率编译报错。写代码的时候一定要区分版本,不要混用 API。

二、基础页面搭建:创建 Web 组件,加载远程 H5

我们新建一个 ArkTS 页面,页面核心就是Web组件。先写最简可运行示例,实现网页加载,后面再逐步增加生命周期处理逻辑。

2.1 基础可运行代码

​
import { Web, WebController, WebLoadStatus } from '@kit.ArkWeb';
import { router } from '@ohos/router';

@Entry
@Component
struct ArkWebDemoPage {
  // 声明Web控制器,用来操控Web实例
  private webController: WebController = new WebController();
  // 记录页面加载状态,UI展示
  @State loadStatus: string = "待加载";

  aboutToAppear() {
    console.info("页面:aboutToAppear,页面即将渲染");
  }

  aboutToDisappear() {
    console.info("页面:aboutToDisappear,页面准备销毁");
  }

  build() {
    Column() {
      Text(`H5加载状态:${this.loadStatus}`)
        .fontSize(16)
        .margin(10)

      // ArkWeb核心组件
      Web({
        src: "https://example.com",
        controller: this.webController
      })
        .width('100%')
        .height('80%')
        // 监听页面加载状态回调
        .onLoadStatusChange((status: WebLoadStatus) => {
          switch(status) {
            case WebLoadStatus.StartLoading:
              this.loadStatus = "开始加载H5页面";
              break;
            case WebLoadStatus.FinishLoading:
              this.loadStatus = "页面加载完成";
              break;
            case WebLoadStatus.ErrorLoading:
              this.loadStatus = "页面加载失败";
              break;
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

​

2.2 逐段代码解读

  1. WebController:Web 组件的控制器,相当于 Web 页面的遥控器。加载页面、前进后退、执行 H5 内部 JS 脚本、销毁实例,全部都要通过这个控制器操作。重点提醒:一个 Web 组件绑定一个 WebController,千万不要多个 Web 组件共用同一个控制器,很容易出现状态错乱、回调异常。
  2. src:网页地址。可以填写远程 https 链接;如果加载本地页面,可以写$rawfile/index.html读取 rawfile 目录下本地 H5。
  3. onLoadStatusChange:加载状态回调,包含三种状态:开始加载、加载完成、加载出错。我们可以在这里控制加载动画、切换错误兜底页面。

2.3 运行效果与新手第一个大坑

把代码粘贴到 DevEco,启动模拟器,打开页面,页面会渲染 example 网页,上方文字同步更新加载状态。

第一个高频踩坑点:访问远程网页,必须配置网络权限。 如果没有在 module.json5 中申请 INTERNET 权限,页面直接报加载失败。很多新手只写 ArkTS 业务代码,完全忘记权限配置,看到加载失败,排查半小时都想不到根源在这里。

​
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

​

小提示:真机和模拟器都需要配置权限,模拟器有时候会缓存权限状态,修改配置之后,建议卸载应用重新安装测试。

三、核心重点:ArkWeb 和页面生命周期绑定

上面这段代码虽然可以正常加载网页,但是存在致命缺陷:没有生命周期处理逻辑,会造成严重内存泄漏。

我们先理清鸿蒙页面四个关键生命周期回调:

  1. aboutToAppear:页面即将渲染,组件初始化阶段;
  2. onPageShow:页面切换到前台,页面可见;
  3. onPageHide:页面切后台,页面被遮挡,用户不可见;
  4. aboutToDisappear:页面即将销毁,页面从页面栈移除。

对应的 Web 实例控制逻辑:

  • onPageHide 页面切后台:暂停 Web 内核,H5 页面动画、视频、js 定时器暂停运行,降低 CPU 和功耗占用;
  • onPageShow 页面回到前台:恢复 Web 内核渲染,JS 脚本继续执行;
  • aboutToDisappear 页面销毁:销毁 Web 实例,释放内核资源,终止 H5 内部所有回调。

⚠️ 重要提醒:不要在 aboutToDisappear 以外的位置调用 destroy。过早销毁 WebController,页面渲染过程中会直接抛出异常。

3.1 增加生命周期处理后的完整代码

import { Web, WebController, WebLoadStatus } from '@kit.ArkWeb';
import { router } from '@ohos/router';

@Entry
@Component
struct ArkWebLifeCyclePage {
  private webController: WebController = new WebController();
  @State loadStatus: string = "待加载";

  aboutToAppear() {
    console.info("aboutToAppear:页面准备渲染");
  }

  // 页面切到前台
  onPageShow() {
    console.info("onPageShow 页面前台");
    // 恢复web渲染和JS执行
    this.webController.resume();
  }

  // 页面切后台
  onPageHide() {
    console.info("onPageHide 页面后台");
    // 暂停web内核执行,节省资源
    this.webController.pause();
  }

  aboutToDisappear() {
    console.info("aboutToDisappear 页面销毁");
    // 销毁Web实例,释放内核资源,这一步是防止内存泄漏的关键
    this.webController.destroy();
  }

  build() {
    Column() {
      Text(`H5加载状态:${this.loadStatus}`)
        .fontSize(16)
        .margin({top:10})

      Web({
        src: "https://example.com",
        controller: this.webController
      })
        .width('100%')
        .height('80%')
        .onLoadStatusChange((status: WebLoadStatus) => {
          switch(status) {
            case WebLoadStatus.StartLoading:
              this.loadStatus = "开始加载H5页面";
              break;
            case WebLoadStatus.FinishLoading:
              this.loadStatus = "页面加载完成";
              break;
            case WebLoadStatus.ErrorLoading:
              this.loadStatus = "页面加载失败,请检查网络";
              break;
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

3.2 核心接口讲解

  1. webController.pause():暂停 Web 内核。H5 页面里面视频、动画、JS 计时器全部暂停运行,减少后台功耗。页面被遮挡、切后台必须调用。
  2. webController.resume():恢复内核执行,页面回到前台时调用。
  3. webController.destroy():销毁 Web 渲染实例,释放内存资源。这是整篇文章最重要的一行代码。

如果不调用 destroy,Web 内核进程会常驻内存,H5 内部定时器、事件监听持续运行。多次打开关闭页面,Web 实例不断叠加,内存持续上涨,最终 APP 卡顿闪退。很多线上 ArkWeb 项目的内存问题,根源就是漏掉这一行销毁代码。

四、页面加载失败的兜底处理

网络波动、地址错误、证书异常的时候,H5 页面加载失败,默认只会展示空白页面,用户完全不知道发生了什么,体验很差。我们可以监听加载错误,展示原生兜底提示页面,增加重试按钮。

在 Web 组件上增加onErrorReceive回调,捕获页面加载异常。

4.1 增加错误捕获与重试完整代码片段

import { Web, WebController, WebLoadStatus } from '@kit.ArkWeb';
import { router } from '@ohos/router';

@Entry
@Component
struct ArkWebLifeCyclePage {
  private webController: WebController = new WebController();
  @State loadStatus: string = "待加载";
  @State hasError: boolean = false;

  aboutToAppear() {
    console.info("aboutToAppear:页面准备渲染");
  }

  onPageShow() {
    console.info("onPageShow 页面前台");
    this.webController.resume();
  }

  onPageHide() {
    console.info("onPageHide 页面后台");
    this.webController.pause();
  }

  aboutToDisappear() {
    console.info("aboutToDisappear 页面销毁");
    this.webController.destroy();
  }

  build() {
    Column() {
      Text(`H5加载状态:${this.loadStatus}`)
        .fontSize(16)
        .margin({top:10})

      // 加载失败兜底UI
      if(this.hasError){
        Column(){
          Text("页面加载失败")
            .fontSize(20)
            .margin(10)
          Button("重新加载")
            .onClick(()=>{
              this.hasError = false;
              this.webController.reload();
            })
        }
      }

      Web({
        src: "https://example.com",
        controller: this.webController
      })
        .width('100%')
        .height('80%')
        .onLoadStatusChange((status: WebLoadStatus) => {
          switch(status) {
            case WebLoadStatus.StartLoading:
              this.loadStatus = "开始加载H5页面";
              this.hasError = false;
              break;
            case WebLoadStatus.FinishLoading:
              this.loadStatus = "页面加载完成";
              this.hasError = false;
              break;
            case WebLoadStatus.ErrorLoading:
              this.loadStatus = "页面加载失败,请检查网络";
              this.hasError = true;
              break;
          }
        })
        .onErrorReceive((err)=>{
          console.error("H5加载异常", JSON.stringify(err));
          this.hasError = true;
          this.loadStatus = "页面加载出错";
        })
    }
    .width('100%')
    .height('100%')
  }
}
  • reload():Web 控制器提供的刷新页面接口,重新请求 H5 地址。
  • 踩坑提醒:如果页面已经执行 destroy 销毁,再调用 reload 会抛出异常,所以重试按钮业务上最好增加状态判断,页面销毁之后禁止用户操作。

这里还有一个很多人踩的误区:onErrorReceive只能捕获页面资源层面的加载错误。比如网络断开、域名无法访问。H5 页面内部的 JS 代码报错,不会触发这个回调。 很多新手以为这个回调可以捕获 H5 里所有 JS 异常,实际并不能,这点一定要分清。

五、实战测试:验证内存与生命周期效果

写完完整代码,我们做简单测试,验证生命周期逻辑是否生效。

  1. 打开页面,查看控制台日志:打印 aboutToAppear、onPageShow,Web 执行 resume 恢复内核。
  2. 把 APP 切后台,日志打印 onPageHide,Web 执行 pause 暂停内核,H5 页面动画停止。
  3. 返回上一页,触发 aboutToDisappear,执行 destroy 销毁 Web 实例。

反复打开关闭页面 5 次,观察 DevEco 内存监视器。正确调用 destroy,内存不会持续上涨;注释掉 destroy 代码,每打开一次页面,内存就增加几十 MB。

很多开发者自测的时候,只看页面能不能正常显示,完全忽略内存监控。等到上线测试阶段才发现内存泄漏问题,修改和回归测试的成本会高很多。做混合应用开发,内存监控是必做环节。

顺便吐槽一句:模拟器的内存数据和真机存在差异。模拟器只能作为初步调试参考,内存相关问题,最终一定要真机复测。模拟器有时候会有缓存干扰,数据不准。

六、本篇小结 

本篇手记,我们搭建了 HarmonyOS7 ArkWeb 基础页面,实现远程 H5 页面加载、加载状态监听。核心重点是把 Web 实例和鸿蒙页面生命周期绑定,掌握 pause、resume、destroy 三个核心接口,解决 ArkWeb 最常见的内存泄漏问题,同时增加加载失败兜底页面。

知识点回顾

  1. ArkWeb 自带独立渲染内核,不主动销毁 Web 实例,会造成内存泄漏;
  2. onPageHide 调用 pause 暂停内核,onPageShow 调用 resume 恢复内核;
  3. aboutToDisappear 里面,必须执行 webController.destroy () 释放资源;
  4. 加载远程 H5 页面,不要忘记 module.json5 配置 INTERNET 网络权限;
  5. 使用 onErrorReceive 捕获页面加载错误,增加重试兜底 UI,优化用户体验。

实操推荐:

  1. 将 src 地址修改为 rawfile 目录下 index.html,加载本地静态 H5 页面;
  2. 在本地 H5 页面写 js 定时器,观察页面退出之后,定时器是否停止运行;
  3. 注释掉 destroy 销毁代码,反复打开关闭页面,观察内存上涨情况,直观感受内存泄漏带来的影响。
Logo

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

更多推荐