在这里插入图片描述

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌 关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀

前言

在 Stage 模型里写 UIAbility 时,有两个生命周期很容易被当成同一件事:onWindowStageDestroy() 和 onDestroy()。

它们都出现在退出阶段,官方示例里也都能看到“释放资源”的语义,但两者管理的生命周期层级并不一样。Stage 模型本身就把应用组件和窗口管理做了解耦:UIAbility 负责组件生命周期,而显示相关状态由 WindowStage 承担。

所以,这篇只讨论一个问题:资源到底应该在哪释放。

我们用一个最小 Demo 放三类资源进去:一个系统环境监听器、一个定时器、一个主窗口对象引用。然后主动结束当前 UIAbility,观察 onWindowStageDestroy() 和 onDestroy(),把资源的“所有者”与清理位置对应起来。


一、先给出资源释放的判断方法

不要先问“哪个回调更晚”,而应该先问:

这个资源到底属于 WindowStage,还是属于 UIAbility?

HarmonyOS 的 Stage 模型明确把应用组件和窗口管理解耦,UIAbility 生命周期主要负责创建、销毁、前后台等状态,显示相关状态则通过 WindowStage 暴露。

结合官方当前示例,可以把本文涉及的资源先分成下面三类:

资源创建位置建议释放位置原因
window.Window 引用及窗口关联资源onWindowStageCreate()onWindowStageDestroy()生命周期依赖当前 WindowStage
setInterval() 定时任务onCreate()onDestroy()本例让它跟随 UIAbility 实例
ApplicationContext 环境监听onCreate()onDestroy()官方 EnvironmentCallback 示例就是这样注册和注销

华为官方多个当前示例对 onWindowStageDestroy() 的注释都是“主窗口销毁,释放 UI 相关资源”;而 EnvironmentCallback 官方 API 示例则直接在 UIAbility.onCreate() 中调用 ApplicationContext.on('environment', ...),在 onDestroy() 中调用 off('environment', ...)。

这已经给出了一个很实用的边界:

窗口生命周期结束时清窗口资源;UIAbility 生命周期结束时清 Ability 级资源。


二、HarmonyOS 7 下先把版本和接口确认清楚

本文以 HarmonyOS 7.0 / API 26.0.0 作为开发背景。华为最新升级适配文档明确说明,HarmonyOS 7.0 对应 API 版本为 26.0.0;从 26.0.0 开始,HarmonyOS 开发套件的 API 版本号也改用 X.Y.Z 语义化格式。

不过本文使用的核心能力并不是 HarmonyOS 7 才新增的能力。

UIAbilityContext 所属模块首批接口从 API version 9 开始支持,并且仅用于 Stage 模型;terminateSelf() 可以主动销毁当前 UIAbility。EnvironmentCallback 同样从 API version 9 开始支持,也是 Stage 模型接口。定时器模块则从 API version 3 开始提供,setInterval() 创建的重复定时任务需要通过 clearInterval() 主动删除。

本文涉及的 Kit 很少:

  • UIAbility、EnvironmentCallback、common.UIAbilityContext:Ability Kit;
  • window.WindowStage、window.Window:ArkUI;
  • 日志直接使用 console.info(),避免为了 Demo 再引入额外业务代码。

整个示例不需要申请额外权限,也不需要为资源释放增加 module.json5 权限配置。

这里还要区分一个容易混淆的配置:官方说明 terminateSelf() 默认不会清理任务中心中的任务;如果业务确实需要停止 UIAbility 后同时移除最近任务,可以给 Ability 配置 removeMissionAfterTerminate: true。这属于任务管理行为,不是本文讨论的“释放监听器、定时器和窗口引用”,因此 Demo 不配置它。

【建议插图1:HarmonyOS 7 / API 26.0.0 工程中 EntryAbility.ets 的生命周期代码位置】


三、搭一个只观察资源释放的最小 Demo

Demo 的逻辑非常简单。

UIAbility 创建时做两件事:

  1. 注册一个 ApplicationContext 系统环境变化监听;
  2. 创建一个每 2 秒打印一次日志的定时器。

等 WindowStage 创建完成并加载页面后,再取得主窗口 window.Window 对象并保存引用。

退出时则反过来:

  • onWindowStageDestroy() 只处理窗口相关引用;
  • onDestroy() 注销监听并清理定时器。

页面上只留一个按钮,调用 UIAbilityContext.terminateSelf(),这样可以主动进入正常的 UIAbility 销毁流程。官方文档确认 terminateSelf() 用于销毁 UIAbility 自身,且只能在主线程调用。

EntryAbility.ets

这段代码解决的核心问题不是“怎么写生命周期”,而是让三类资源具有清晰的归属关系。

import { EnvironmentCallback, UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  private environmentCallbackId: number | undefined = undefined;
  private timerId: number | undefined = undefined;
  private mainWindow: window.Window | undefined = undefined;

  onCreate(): void {
    console.info('[LifecycleDemo] onCreate');

    const environmentCallback: EnvironmentCallback = {
      onConfigurationUpdated(config) {
        console.info(
          `[LifecycleDemo] configuration changed: ${JSON.stringify(config)}`
        );
      },

      onMemoryLevel(level) {
        console.info(
          `[LifecycleDemo] memory level changed: ${JSON.stringify(level)}`
        );
      }
    };

    const applicationContext = this.context.getApplicationContext();

    this.environmentCallbackId =
      applicationContext.on('environment', environmentCallback);

    console.info(
      `[LifecycleDemo] environment listener registered, id=${this.environmentCallbackId}`
    );

    this.timerId = setInterval(() => {
      console.info('[LifecycleDemo] timer tick');
    }, 2000);

    console.info(
      `[LifecycleDemo] timer created, id=${this.timerId}`
    );
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    console.info('[LifecycleDemo] onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error(
          `[LifecycleDemo] loadContent failed, code=${err.code}, message=${err.message}`
        );
        return;
      }

      this.mainWindow = windowStage.getMainWindowSync();

      console.info(
        '[LifecycleDemo] main window reference acquired'
      );
    });
  }

  onWindowStageDestroy(): void {
    console.info('[LifecycleDemo] onWindowStageDestroy');

    // WindowStage 生命周期已经进入销毁阶段,
    // 清除本 Ability 保存的窗口相关对象引用。
    this.mainWindow = undefined;

    console.info(
      '[LifecycleDemo] window related reference released'
    );
  }

  onDestroy(): void {
    console.info('[LifecycleDemo] onDestroy');

    const applicationContext = this.context.getApplicationContext();

    if (this.environmentCallbackId !== undefined) {
      applicationContext.off(
        'environment',
        this.environmentCallbackId
      );
      console.info(
        '[LifecycleDemo] environment listener removed'
      );
      this.environmentCallbackId = undefined;
    }

    if (this.timerId !== undefined) {
      clearInterval(this.timerId);
      console.info('[LifecycleDemo] timer cleared');
      this.timerId = undefined;
    }
  }
}

这里真正需要关注的是三个成员变量:

environmentCallbackId
timerId
mainWindow

它们不是为了方便写 Demo 才随便放在一起,而是故意代表三种典型资源:订阅关系、持续任务、窗口对象引用。

其中定时器还有一个官方约束:clearInterval() 删除的定时器应当与创建它的定时器位于同一线程。官方文档同时说明,应用切到后台以后,UI 场景中的定时器可能被冻结,因此“进入后台”也不能简单等价成“定时器已经结束”。


四、页面只负责触发正常退出

为了不把系统杀进程、任务管理等因素混进 Demo,可以直接调用 terminateSelf()。

官方示例已经给出了从 ArkUI 页面通过 getUIContext().getHostContext() 获取 UIAbilityContext 的方式。

Index.ets

import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  build() {
    Column({ space: 20 }) {
      Text('UIAbility Resource Release Demo')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)

      Button('Terminate UIAbility')
        .onClick(async () => {
          const context =
            this.getUIContext().getHostContext()
              as common.UIAbilityContext;

          try {
            await context.terminateSelf();
          } catch (err) {
            const error = err as BusinessError;
            console.error(
              `terminateSelf failed, code=${error.code}, message=${error.message}`
            );
          }
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这份代码是按照当前官方接口定义组织的最小示例,没有在这里声称已经经过具体设备编译或真机运行。实际发布文章前,建议使用 HarmonyOS 7 / API 26.0.0 的目标设备再做一次日志验证。

【建议插图2:点击“Terminate UIAbility”前,控制台持续输出 timer tick 的日志】

【建议插图3:点击按钮后,控制台中 onWindowStageDestroy、窗口引用释放、onDestroy、监听注销和 timer cleared 的日志】


五、onWindowStageDestroy 到底应该释放什么

onWindowStageDestroy() 最重要的关键词不是 Destroy,而是 WindowStage。

官方当前大量窗口示例都采用这样的结构:

onWindowStageDestroy(): void {
  // Main window is destroyed, release UI related resources
}

也就是把它定位在主窗口销毁阶段,用于处理 UI、窗口相关资源。

因此,如果某个对象只有在当前 WindowStage 存在时才有意义,例如:

窗口对象引用、与窗口绑定的 UI 上下文、针对当前窗口创建的辅助对象,以及业务自行创建并需要主动管理的子窗口资源,那么清理逻辑应该优先围绕 onWindowStageDestroy() 设计。

本例中的:

private mainWindow: window.Window | undefined;

就是最简单的窗口生命周期对象。

这里还有一个很重要的细节:主窗口是系统通过 WindowStage 提供给 UIAbility 的,不应该为了“释放资源”而自己调用 destroyWindow() 把主窗口销毁一遍。

Demo 做的是解除业务代码保存的引用:

this.mainWindow = undefined;

如果实际项目自行创建了子窗口,则属于另一种情况。华为当前《子窗口开发指导》明确指出,不再需要子窗口时,应使用对应的窗口销毁接口释放子窗口。


六、onDestroy 更适合处理 UIAbility 级资源

监听器和定时任务则不一样。

我们的 EnvironmentCallback 是在:

onCreate()

中注册的,它跟随的是这个 UIAbility 实例,而不是某个具体窗口。

官方 EnvironmentCallback 文档甚至直接给出了完全相同的生命周期配对:

onCreate -> ApplicationContext.on('environment', ...)
onDestroy -> ApplicationContext.off('environment', ...)

所以这里放到 onDestroy() 清理非常自然。

定时器也是同样的设计。因为本例是在 UIAbility.onCreate() 中建立定时任务,希望它存在于整个 UIAbility 生命周期,所以在 onDestroy() 中执行:

clearInterval(this.timerId);

如果实际业务中的定时器只服务某个页面,那么它就不应该照搬本文放到 UIAbility.onDestroy()。页面级资源应跟随页面或组件自身的生命周期释放。

资源在哪里创建,不是唯一标准;资源属于谁,才是更可靠的判断依据。


七、观察两个 Destroy 时,不要只记调用顺序

在正常结束 UIAbility 的场景中,这个 Demo 最值得观察的是两个阶段:

WindowStage 销毁阶段
    ↓
onWindowStageDestroy()
    ↓
释放窗口相关资源

UIAbility 销毁阶段
    ↓
onDestroy()
    ↓
注销监听、停止 Ability 级持续任务

开发时很容易把它简化成:

“反正最终都会退出,那全部塞到 onDestroy 不就行了?”

问题就在这里。

如果资源本来依赖窗口,把释放动作一直拖到 UIAbility 的最终销毁阶段,会让窗口资源与 Ability 生命周期发生不必要的耦合;反过来,如果把所有监听器、任务都塞到 onWindowStageDestroy(),又等于默认这些资源一定依赖窗口,这同样不准确。

Stage 模型专门把 UIAbility 和 WindowStage 拆开,资源管理最好也保留这层边界。


八、几个容易理解错的地方

这里有三个地方特别值得检查。

第一,onBackground() 不是通用资源销毁点。 UIAbility 进入后台和 UIAbility 被销毁是两件事。尤其是本文的定时器,官方 Timer 文档明确说明应用切到后台后定时器可能被冻结,这并不表示定时器已经被删除。

第二,不要把 onDestroy() 理解成进程级 finally。 官方当前 restartApp() 文档明确指出,通过该接口重启进程时,不会触发进程中 Ability 的 onDestroy 生命周期回调。也就是说,重要数据不能仅依靠“应用退出时再保存”这种设计保证可靠性。

第三,窗口引用清空和销毁窗口不是同一件事。 系统管理的主窗口只需要停止继续持有、停止继续操作;业务主动创建且需要独立管理的窗口,则应按照窗口 API 的生命周期要求主动销毁。华为当前子窗口指导也明确要求,不再需要子窗口时进行销毁。


九、实际项目怎么排查资源没有释放

遇到 UIAbility 退出后仍怀疑有资源残留时,可以按“资源所有权”往回查,而不是先在两个 Destroy 之间猜。

先确认资源在哪里创建。如果在 onWindowStageCreate() 中取得,而且离开这个 WindowStage 后就没有继续存在的意义,优先检查 onWindowStageDestroy()。

如果资源从 onCreate() 开始存在,并且设计上服务整个 UIAbility,例如应用级监听、连接句柄或本例这样的持续任务,再检查 onDestroy() 是否成对执行了 off、clear、disconnect 等对应操作。

监听器尤其要确认注册返回的 ID 是否保存下来。EnvironmentCallback 官方示例就是保存 callbackId,销毁时再把同一个 ID 交给 off()。

然后检查是否错误地把 onBackground() 当成退出。后台只是状态变化,并不意味着 UIAbility 生命周期已经结束。

最后再检查退出路径。不要假设任何情况下都一定执行 onDestroy();对于官方已经明确说明不会触发该回调的特殊路径,更不能把关键持久化操作只押在这里。


开发经验总结

onWindowStageDestroy() 和 onDestroy() 真正的区别,不是“两个销毁回调选哪个”,而是它们代表两个不同层级的生命周期结束。

窗口、UIContext、窗口关联对象这类资源,应该优先跟随 WindowStage 管理;监听器、连接、定时任务等 UIAbility 级资源,则跟随 UIAbility 生命周期管理。创建和释放尽量形成清晰的配对关系:

onWindowStageCreate
        ↕
onWindowStageDestroy

onCreate
        ↕
onDestroy

这样处理之后,即使以后一个 UIAbility 中加入更多窗口、监听和异步任务,也比较容易判断资源应该归到哪一层,而不是把所有清理代码堆到最后一个 onDestroy()。

如果正在整理现有 HarmonyOS 工程,可以直接搜一遍 on()、setInterval()、窗口对象和各种 connect 调用,然后逐个问一句:这个资源的真正所有者到底是谁? 很多资源释放问题,到这里就已经能定位出方向。

❤️ 如果本文帮到了你…

  • 请点个赞,让我知道你还在坚持阅读技术长文!
  • 请收藏本文,因为你以后一定还会用上!
  • 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
Logo

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

更多推荐