我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

直说吧:很多 App 卡在“启动这一脚油门”上——冷启动像老爷车、暖启动像睡过头,前后台来回切还经常丢状态。偏偏在鸿蒙(HarmonyOS/OpenHarmony)Stage 模型里,UIAbility 的生命周期WindowStage 的事件又是一对“连体婴”,一个负责流程,一个负责窗口“形态学”(可见性、焦点、尺寸、模式)。这篇就是带你把两者串成一条“能跑、能测、能自救”的生命线:创建 / 前台 / 后台 / 销毁窗口可见性/焦点/尺寸变化回调冷/暖启动判断异常恢复策略,以及测试钩子 & 埋点,一次打包拿走。上车系好安全带,咱开整!🚗💨

一、先上总览:生命周期 & 事件流怎么“咬合”

1)UIAbility 生命周期(Stage 模型常见顺序)

onCreate
  └─ onWindowStageCreate(windowStage)
        └─ 绑定窗口事件(尺寸/可见/焦点/模式…)
onForeground
onBackground
onWindowStageDestroy
onDestroy

2)WindowStage / Window 侧常见事件(示意)

  • 尺寸变化window.on('windowSizeChange', (size)=>{})
  • 可见性window.on('windowVisibilityChange', (isVisible)=>{})(不同 API 版本事件名可能略有差异)
  • 焦点变化window.on('windowFocusChange', (hasFocus)=>{})
  • 模式变化(横竖屏/分屏/悬浮):window.on('windowModeChange', (mode)=>{})
  • 避让区域/系统栏window.on('avoidAreaChange' | 'systemBarTintChange', cb)

友情提示:不同 API Level(如 API 9/10/11+)事件名与参数可能有微调,以当前 SDK 文档为准。本文代码用通用写法演示思路。

二、把生命周期写“稳”的最小范式(ArkTS 实战)

目标:冷/暖启动识别窗口事件统一订阅前后台状态可观测异常自愈可测可埋点

// /entry/src/main/ets/ability/MainAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import window from '@ohos.window';
import { LifecycleBus } from '../infra/lifecycle-bus';
import { Pref } from '../infra/pref';
import { Tracker } from '../infra/tracker';

export default class MainAbility extends UIAbility {
  private mainWindow?: window.Window;
  private boundEvents: Array<() => void> = [];
  private coldBoot = true; // 本进程第一次启动的“冷暖”标记(进程级)

  async onCreate(want, launchParam) {
    // 1) 进程级初始化(只做轻量工作)
    Tracker.init({ channel: 'prod', appVersion: '1.2.3' });
    Tracker.log('ability:create', { want, launchParam });

    // 2) 冷/暖启动判断(进程内第一次=冷启动)
    const hasBooted = Pref.getBoolean('hasBooted', false);
    this.coldBoot = !hasBooted;
    Pref.setBoolean('hasBooted', true);

    LifecycleBus.emit('ability:onCreate', { cold: this.coldBoot });
  }

  async onWindowStageCreate(windowStage: window.WindowStage) {
    Tracker.log('ability:windowStageCreate');

    // 1) 获取主窗口
    this.mainWindow = await windowStage.getMainWindow();

    // 2) 统一绑定窗口事件(尺寸、可见、焦点、模式)
    this.boundEvents.push(this.bindWindowEvents(this.mainWindow));

    // 3) 冷启动补偿:首帧前做最小数据预取
    if (this.coldBoot) {
      // 尽量小且可缓存的初始化,避免卡首屏
      void this.prewarmTinyData();
    }

    LifecycleBus.emit('ability:onWindowStageCreate');
  }

  onForeground() {
    Tracker.log('ability:foreground', { cold: this.coldBoot });
    LifecycleBus.emit('ability:onForeground');
    // 恢复资源/重连:例如 WebSocket、BLE、定时器
    this.resumeResourcesSafely();
  }

  onBackground() {
    Tracker.log('ability:background');
    LifecycleBus.emit('ability:onBackground');
    // 释放资源:暂停渲染/动画、降采样,挂起网络长连
    this.suspendResourcesSafely();
  }

  onWindowStageDestroy() {
    Tracker.log('ability:windowStageDestroy');
    LifecycleBus.emit('ability:onWindowStageDestroy');
    // 解绑窗口事件,防泄漏
    this.boundEvents.forEach(off => off());
    this.boundEvents = [];
    this.mainWindow = undefined;
  }

  onDestroy() {
    Tracker.log('ability:destroy');
    LifecycleBus.emit('ability:onDestroy');
  }

  // —— helpers ——

  private bindWindowEvents(win: window.Window): () => void {
    const offs: Array<() => void> = [];

    // 尺寸变化(如分屏/横竖屏)
    const onSize = (size: window.Size) => {
      Tracker.log('window:size', size);
      LifecycleBus.emit('window:size', size);
    };
    win.on('windowSizeChange', onSize);
    offs.push(() => win.off('windowSizeChange', onSize));

    // 可见性变化(遮挡/最小化)
    const onVisible = (visible: boolean) => {
      Tracker.log('window:visible', { visible });
      LifecycleBus.emit('window:visible', { visible });
    };
    // 某些 API 版本事件名可能不同,按需替换
    // 例如:win.on('windowVisibilityChange', onVisible);
    win.on('windowVisibilityChange', onVisible);
    offs.push(() => win.off('windowVisibilityChange', onVisible));

    // 焦点变化(输入焦点)
    const onFocus = (focused: boolean) => {
      Tracker.log('window:focus', { focused });
      LifecycleBus.emit('window:focus', { focused });
    };
    win.on('windowFocusChange', onFocus);
    offs.push(() => win.off('windowFocusChange', onFocus));

    // 模式变化(全屏/分屏/悬浮…)
    const onMode = (mode: number) => {
      Tracker.log('window:mode', { mode });
      LifecycleBus.emit('window:mode', { mode });
    };
    win.on('windowModeChange', onMode);
    offs.push(() => win.off('windowModeChange', onMode));

    // 避让区域(刘海/系统栏/小窗)
    const onAvoid = (area) => {
      LifecycleBus.emit('window:avoid', area);
    };
    win.on('avoidAreaChange', onAvoid);
    offs.push(() => win.off('avoidAreaChange', onAvoid));

    // 统一 off
    return () => offs.forEach(off => off());
  }

  private async prewarmTinyData() {
    try {
      await Promise.race([
        // 预拉取用户最小态配置(如主题/最近视图缓存 KEY)
        Pref.preloadCacheKeys(['theme', 'lastTab']),
        this.sleep(120) // 超时兜底,不阻塞首屏
      ]);
    } catch (e) {
      Tracker.error('prewarm:fail', e);
    }
  }

  private resumeResourcesSafely() {
    // 例:重连消息通道、恢复定时器
    LifecycleBus.emit('app:resume');
  }

  private suspendResourcesSafely() {
    // 例:暂停动画、降低定时器频率、挂起高功耗任务
    LifecycleBus.emit('app:suspend');
  }

  private sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); }
}

“味儿正”的三点:
1)初始化轻onCreate可推迟的都别做;把沉重初始化拆到 onForeground 后的后台任务
2)事件统一:所有窗口事件集中绑定/解绑,一键 off 防泄漏。
3)可观测LifecycleBus 把生命周期事件化,UI/数据/埋点“订阅即用”。

三、冷启动 vs 暖启动:别靠“体感”,要靠“证据”

定义(实务口径)

  • 冷启动:进程首次创建(onCreate 之前没有该进程),需要做布局膨胀 + 首次数据装载
  • 暖启动:进程仍在、Ability 被复用,更多是视图恢复/轻数据刷新

可靠判断法

  • 进程内 flaghasBooted 首次 false → true。
  • 前后台切换onForegroundhasBooted=true 且页面栈存在,即判定暖启动路径。
  • 持久首开判定(可选):首开 24 小时内另做埋点区分“首开冷启动”。

冷/暖策略差异表

事项 冷启动 暖启动
首屏渲染 最少视图 + 骨架屏 直接用缓存数据回填
数据拉取 关键数据并发拉 + 延迟二级数据 增量拉取/校验缓存
动画/动效 延后到首帧后 1~2 个 tick 可立即执行
追踪埋点 冷启动 cost 分段上报(create → firstFrame) 暖启动耗时(background→foreground→可交互)

四、窗口事件:可见性、焦点、尺寸变化“怎么正确用”

1)尺寸变化(含分屏、旋转、小窗)

  • UI 响应:断点布局(栅格/自适应),避免重建大对象。
  • 图表/Canvas:仅在尺寸变化结束后节流重绘(如 100ms)。
  • 避免“抖动”:用 requestAnimationFrame 合批。
// /entry/src/main/ets/infra/ui-size-adapter.ts
import { LifecycleBus } from './lifecycle-bus';

let pending = false;
LifecycleBus.on('window:size', (size) => {
  if (pending) return;
  pending = true;
  requestAnimationFrame(() => {
    // 触发布局策略、刷新图表
    globalThis.store.viewport.update(size);
    pending = false;
  });
});

2)可见性与焦点

  • 可见性:失可见时暂停高耗时渲染/动画;重新可见再恢复。
  • 焦点:输入组件状态同步、光标/虚拟键盘管理。
LifecycleBus.on('window:visible', ({ visible }) => {
  globalThis.animator.toggle(visible);
});

LifecycleBus.on('window:focus', ({ focused }) => {
  if (!focused) globalThis.keyboard.hide();
});

五、异常恢复策略:别等崩了再后悔

1)初始化失败(网络/配置加载)

  • 做兜底 UI:错误页 + “重试”按钮,别整空白屏。
  • 指数退避:首次 1s → 2s → 4s… 上限 30s。
  • 降级路径:离线缓存 + 次要功能延后。
// /entry/src/main/ets/infra/retry.ts
export async function retry<T>(fn: ()=>Promise<T>, max=4) {
  let t = 1000, i = 0;
  while (true) {
    try { return await fn(); } 
    catch (e) { if (i++ >= max) throw e; await sleep(t); t = Math.min(t*2, 30000); }
  }
}

2)前后台切换丢状态

  • 轻量状态(tab、过滤条件)AppStorage/Preferences 即时落盘;
  • 中等状态(页面草稿/编辑态)节流保存
  • 重状态(长列表)存游标+查询条件,回前台增量拉+滚动定位

3)窗口重建/多窗口

  • 绑定事件时保存 off 句柄,在 onWindowStageDestroy 必须成对解绑
  • 多窗口时以 window.getWindowClass() 或自定义标识区分,不要误用全局单例

六、测试钩子 & 埋点:让“体验”变成“数据”

1)LifecycleBus:白盒可测

// /entry/src/main/ets/infra/lifecycle-bus.ts
type Handler = (payload?: any) => void;
class Bus {
  private map = new Map<string, Set<Handler>>();
  on(event: string, cb: Handler) { if (!this.map.has(event)) this.map.set(event, new Set()); this.map.get(event)!.add(cb); }
  off(event: string, cb: Handler) { this.map.get(event)?.delete(cb); }
  emit(event: string, payload?: any) { this.map.get(event)?.forEach(cb => cb(payload)); }
}
export const LifecycleBus = new Bus();

用法

  • UI/数据模块订阅生命周期,不直接依赖 Ability;
  • 单测里直接 LifecycleBus.emit('ability:onForeground') 驱动逻辑。

2)埋点 Tracker:延迟、合批、降采样

// /entry/src/main/ets/infra/tracker.ts
export class Tracker {
  private static q: any[] = [];
  private static ready = false;

  static init(ctx: any) {
    this.ready = true;
    setInterval(() => this.flush(), 2000);
  }

  static log(name: string, data: any = {}) {
    this.q.push({ t: Date.now(), name, data });
    if (this.q.length > 20) this.flush();
  }
  static error(name: string, e: any) { this.log(name, { error: `${e}` }); }

  private static flush() {
    if (!this.ready || this.q.length === 0) return;
    const batch = this.q.splice(0, this.q.length);
    // TODO: 发送到后端;弱网下持久化本地
  }
}

关键指标建议

  • 冷启动 TTI/首帧、暖启动恢复耗时;
  • 前后台切换次数、平均驻留时长;
  • 尺寸变化频次、模式切换分布;
  • 异常恢复成功率、重试次数分布。

3)Hypium 单元/集成测试(思路)

  • 单元:对 retryLifecycleBus、状态持久化逻辑做纯函数级测试。
  • 集成:Mock Window 事件流(尺寸、可见、焦点)→ 断言 UI/状态变化。
  • 端到端:冷/暖启动路径脚本化,统计首屏指标与恢复时序。

七、常见坑 & 处方签

  1. 把重活放在 onCreate → 首屏“板砖脸”。

    • ✅ 只做必要初始化;其它延后到 onForeground 或首帧后 idle。
  2. 窗口事件散落绑定/遗漏解绑 → 内存泄漏 + 重复回调。

    • ✅ 统一 bindWindowEvents + 集中 off
  3. 尺寸变化全量重渲染 → 抖动/卡顿。

    • ✅ 节流 + 局部重排;Canvas 图表做范围重绘。
  4. 可见性与焦点混淆 → 键盘/动画状态错乱。

    • ✅ 分别处理;无焦点时隐藏输入法,有焦点才恢复光标动画。
  5. 冷暖启动埋点不分 → 优化无从下手。

    • ✅ 进程级 hasBooted + 阶段化埋点。
  6. 异常只弹 Toast → 用户原地等死。

    • ✅ 错误屏 + 重试 + 降级,用数据说话(恢复率纳 KPI)。

八、把“可见/焦点/尺寸”串成 UI 策略(小案例)

需求:分屏缩小时自动切换为紧凑布局;失焦时暂停动画;不可见时降采样定时器

// /entry/src/main/ets/app/policies.ts
import { LifecycleBus } from '../infra/lifecycle-bus';

let compact = false;
let timer: number | undefined;
let animating = true;

LifecycleBus.on('window:size', (size) => {
  const shouldCompact = size.width < 720; // 阈值按设计稿定
  if (shouldCompact !== compact) {
    compact = shouldCompact;
    globalThis.store.ui.setCompact(compact);
  }
});

LifecycleBus.on('window:focus', ({ focused }) => {
  animating = focused;
  globalThis.animator.toggle(animating);
});

LifecycleBus.on('window:visible', ({ visible }) => {
  if (!visible) {
    if (timer) clearInterval(timer);
    timer = setInterval(() => globalThis.store.clock.tick(), 1000); // 降采样
  } else {
    if (timer) clearInterval(timer);
    timer = setInterval(() => globalThis.store.clock.tick(), 250); // 恢复
  }
});

九、结语:生命周期不是“百科词条”,而是性能与体验的方向盘

当你把 UIAbility 的“生命”Window 的“形态” 这两根经脉疏通后,冷/暖启动有章法、前后台稳得住、窗口变化不惊魂,异常来了也不慌。接下来就愉快地做“加分项”吧:

  • 首帧渲染骨架屏优化、图表增量重绘
  • 前后台资源编排(网络/渲染/传感器);
  • 指标闭环(端到端埋点 → 看板 → 迭代)。

一句反问作收尾:既然能把“生命线”握在手里,凭什么还要把用户交给“偶然性”呢? 😉

附:可直接抄用的小工具(偏工程化)

1)Preferences 小封装

// /entry/src/main/ets/infra/pref.ts
import dataPreferences from '@ohos.data.preferences';

export class Pref {
  private static pref?: dataPreferences.Preferences;

  static async ensure() {
    if (!this.pref) {
      this.pref = await dataPreferences.getPreferences(globalThis.context, 'app.pref');
    }
    return this.pref!;
  }

  static async preloadCacheKeys(keys: string[]) {
    const p = await this.ensure();
    await Promise.all(keys.map(k => p.get(k)));
  }

  static getBoolean(key: string, def=false) {
    // 简化:真实应为异步,此处演示同步读取可以换为缓存
    const cached = globalThis.cache?.get(key);
    return cached ?? def;
  }

  static setBoolean(key: string, v: boolean) {
    globalThis.cache?.set(key, v);
  }
}

2)全局上下文(供 Ability/页面共享)

// /entry/src/main/ets/infra/context.ts
export class AppContext {
  constructor(public abilityContext: any) {}
}
// in MainAbility.onCreate: globalThis.appCtx = new AppContext(this.context);

快速 Checklist(上线前最后看一眼)✅

  • onCreate 轻量化,首帧前不做重 IO
  • 冷/暖启动判定与分流策略
  • Window 事件统一绑定/解绑,泄漏检测
  • 尺寸变化节流重绘,可见/焦点分治
  • 前后台资源编排(网络/动画/传感器)
  • 异常恢复(错误屏、重试、降级、日志)
  • 埋点闭环(分段耗时、恢复成功率)
  • 单测/集成测试可驱动(LifecycleBus 驱动)

(未完待续)

Logo

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

更多推荐