HarmonyOS的页面生命周期比Android简单——没有onStart/onStop/onResume那堆回调,但简单不等于没有坑。UIAbility的生命周期、NavDestination的显示/隐藏、组件的aboutToAppear/aboutToDisappear,这三层生命周期的执行顺序和交互逻辑如果不搞清楚,状态管理就会一团糟。加上应用切后台被系统回收后恢复状态的需求,生命周期的问题远比想象中复杂。

UIAbility生命周期

在这里插入图片描述

UIAbility是应用的入口组件,生命周期回调:

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

class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 应用创建,初始化全局数据
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 窗口创建,加载主页面
    windowStage.loadContent('pages/Index');
  }

  onForeground(): void {
    // 应用回到前台
  }

  onBackground(): void {
    // 应用切到后台
  }

  onWindowStageDestroy(): void {
    // 窗口销毁
  }

  onDestroy(): void {
    // 应用销毁
  }
}

执行顺序:onCreate → onWindowStageCreate → onForeground → (运行中) → onBackground → (后台) → onForeground → … → onBackground → onWindowStageDestroy → onDestroy

关键:onBackground不是onDestroy。 应用切后台后可能长时间存活,也可能随时被系统杀掉。不要在onBackground中做"最终清理"——那是onDestroy的事。

NavDestination生命周期

NavDestination有自己的显示/隐藏回调:

NavDestination() {
  // 页面内容
}
.onShown(() => {
  // 页面变为可见(push到栈顶或返回到此页)
})
.onHidden(() => {
  // 页面被覆盖(新页面push或本页pop)
})
.onWillAppear(() => {
  // 页面即将显示,动画开始前
})
.onWillDisappear(() => {
  // 页面即将消失,动画开始前
})
.onDisappeared(() => {
  // 页面已从导航栈移除
})

执行顺序:push新页面时,新页面的onWillAppear → onShown,旧页面的onHidden。pop返回时,当前页的onWillDisappear,下级页面的onShown。

onShown/onHidden可以反复触发——push覆盖触发onHidden,pop回来触发onShown。aboutToAppear只在组件首次创建时触发一次。

组件生命周期

@Component
struct MyComponent {
  aboutToAppear(): void {
    // 组件即将构建,在build之前
    // 适合做数据初始化
  }

  aboutToDisappear(): void {
    // 组件即将销毁
    // 适合做资源清理
  }

  build() {
    // 构建UI
  }
}

aboutToAppear中不要做耗时操作。 它在UI线程执行,如果这里做网络请求或大量计算,会阻塞首帧渲染。正确做法是aboutToAppear中发起异步请求,UI先显示加载态,数据回来后更新。

三层生命周期协作

UIAbility、NavDestination、Component三层生命周期的执行时序:

应用启动:
  UIAbility.onCreate → UIAbility.onWindowStageCreate → UIAbility.onForeground
  → Index页面 aboutToAppear → Index页面 build → Index页面 onShown

push到详情页:
  Detail页面 aboutToAppear → Detail页面 build
  → Index页面 onHidden → Detail页面 onShown

pop返回:
  Detail页面 onWillDisappear → Detail页面 aboutToDisappear
  → Index页面 onShown

应用切后台:
  UIAbility.onBackground → 所有页面保持不变(组件不销毁)

应用回前台:
  UIAbility.onForeground

应用被杀:
  无回调,下次启动走onCreate

最大的坑:应用切后台再回来,组件不重新创建。 aboutToAppear不会再次触发。如果页面数据可能过期(比如切后台超过5分钟),需要在onForeground或onShown中刷新。

后台恢复策略

应用在后台可能被系统回收。回来时如果进程还在,数据还在内存中;如果进程被杀,需要从冷启动恢复。

判断是否从冷启动恢复:

class EntryAbility extends UIAbility {
  private isColdStart: boolean = true;

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.STARTUP_CONTINUATION) {
      // 从异常恢复启动
      this.isColdStart = false;
    } else {
      this.isColdStart = true;
    }
  }
}

STARTUP_CONTINUATION表示应用从异常终止后恢复。这个场景需要从持久化存储中恢复数据。

关键状态持久化

切后台前保存关键数据,回来后恢复:

import { preferences } from '@kit.ArkData';

// 保存关键状态
async saveAppState(): Promise<void> {
  let pref = await preferences.getPreferences(this.context, 'app_state');
  await pref.putSync('last_active_time', Date.now());
  await pref.putSync('current_page', this.currentPageName);
  await pref.putSync('scroll_position', this.scrollOffset);
  await pref.putSync('form_draft', this.draftContent);
  await pref.flush();
}

// 恢复关键状态
async restoreAppState(): Promise<void> {
  let pref = await preferences.getPreferences(this.context, 'app_state');
  let lastTime = pref.getSync('last_active_time', 0) as number;
  let elapsed = Date.now() - lastTime;

  if (elapsed > 5 * 60 * 1000) {
    // 超过5分钟,数据可能过期,重新加载
    this.refreshAllData();
  } else {
    // 5分钟内,恢复保存的状态
    this.scrollOffset = pref.getSync('scroll_position', 0) as number;
    this.draftContent = pref.getSync('form_draft', '') as string;
  }
}

在onBackground中调用saveAppState,在onForeground中调用restoreAppState。超过5分钟的数据视为过期,强制刷新。

不要保存所有状态——只保存用户自己输入的、不可重新获取的数据。 比如表单草稿、未发送的消息、编辑中的文本。列表数据、详情数据可以从网络重新加载,不需要持久化。

滚动位置恢复

列表页跳详情再返回,滚动位置丢失是最常见的体验问题:

private scroller: Scroller = new Scroller();
private savedScrollOffset: number = 0;

List() {
  // 内容
}
.scroller(this.scroller)
.onScroll(() => {
  this.savedScrollOffset = this.scroller.currentOffset().yOffset;
})

// 返回时恢复
onShown(): void {
  if (this.savedScrollOffset > 0) {
    setTimeout(() => {
      this.scroller.scrollTo({ xOffset: 0, yOffset: this.savedScrollOffset });
    }, 50);
  }
}

onScroll中持续记录偏移量。返回时scrollTo恢复。50ms延迟等列表渲染完成。

问题:如果返回时列表数据变化了(比如删除了一条),滚动位置可能对不上。 精确恢复需要记录第一个可见项的id而非偏移量:

private firstVisibleId: string = '';

.onScrollIndex((start: number) => {
  if (start < this.items.length) {
    this.firstVisibleId = this.items[start].id;
  }
})

// 恢复
onShown(): void {
  let targetIndex = -1;
  for (let i = 0; i < this.items.length; i++) {
    if (this.items[i].id === this.firstVisibleId) {
      targetIndex = i;
      break;
    }
  }
  if (targetIndex >= 0) {
    setTimeout(() => {
      this.scroller.scrollToIndex(targetIndex);
    }, 50);
  }
}

用id找index,即使列表项有增删也能恢复到正确位置。

表单数据保护

用户填了一半表单,跳到别的页面再回来,输入内容丢失:

@State formDraft: string = ''
private autoSaveTimer: number = -1

TextArea({ text: $$this.formDraft })
  .width('100%')
  .height(120)
  .onChange(() => {
    this.scheduleAutoSave();
  })

private scheduleAutoSave(): void {
  if (this.autoSaveTimer !== -1) {
    clearTimeout(this.autoSaveTimer);
  }
  this.autoSaveTimer = setTimeout(() => {
    this.saveDraft();
  }, 1000);
}

private async saveDraft(): Promise<void> {
  let pref = await preferences.getPreferences(this.context, 'drafts');
  await pref.putSync('draft_' + this.pageId, this.formDraft);
  await pref.flush();
}

输入变化后1秒自动保存草稿(防抖)。回到页面时从Preferences读取草稿:

aboutToAppear(): void {
  this.loadDraft();
}

private async loadDraft(): Promise<void> {
  let pref = await preferences.getPreferences(this.context, 'drafts');
  this.formDraft = pref.getSync('draft_' + this.pageId, '') as string;
}

草稿在提交成功后清除:

async submitForm(): Promise<void> {
  await this.api.submit(this.formDraft);
  let pref = await preferences.getPreferences(this.context, 'drafts');
  await pref.deleteSync('draft_' + this.pageId);
  await pref.flush();
  this.formDraft = '';
}

定时器与监听器清理

组件销毁时必须清理所有异步资源:

private timers: number[] = [];
private emitterCallbacks: emitter.Callback[] = [];

aboutToAppear(): void {
  let t1 = setInterval(() => { this.tick(); }, 1000);
  this.timers.push(t1);

  let callback = (data: emitter.EventData) => { this.onEvent(data); };
  this.emitterCallbacks.push(callback);
  emitter.on({ eventId: 1001 }, callback);
}

aboutToDisappear(): void {
  for (let i = 0; i < this.timers.length; i++) {
    clearInterval(this.timers[i]);
  }
  for (let i = 0; i < this.emitterCallbacks.length; i++) {
    emitter.off(1001, this.emitterCallbacks[i]);
  }
}

规则:在aboutToAppear中创建的定时器、监听器、订阅,必须在aboutToDisappear中清理。 每加一个setInterval/emitter.on/display.on,就加对应的清理代码。不多不少。

踩坑清单

问题 原因 解决
切后台回来数据没刷新 aboutToAppear不再次触发 在onForeground/onShown中刷新
滚动位置丢失 没记录偏移量 scroller.currentOffset + scrollTo
表单内容丢失 组件重建了 草稿持久化到Preferences
定时器泄漏 aboutToDisappear没清理 每个setInterval对应clearInterval
页面重复初始化 onShown和aboutToAppear都做了初始化 初始化放aboutToAppear,刷新放onShown
进程被杀后状态丢失 内存数据没持久化 关键数据写Preferences
onHidden中做清理 onHidden≠销毁,还可能onShown回来 清理放aboutToDisappear
冷启动没恢复数据 没判断launchReason 检查STARTUP_CONTINUATION
scrollTo时机太早 列表还没渲染完 setTimeout 50ms延迟
多次注册监听器 每次onShown都on on只注册一次,放aboutToAppear

生命周期管理的核心原则:创建和销毁成对,显示和刷新分离,关键数据持久化。 aboutToAppear做初始化,aboutToDisappear做清理,onShown做刷新,onBackground做保存。搞清楚每层回调的触发时机和生命周期,状态管理就不会乱。

Logo

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

更多推荐