前言

生命周期回调不是随便写的,每个回调代表应用处于不同状态,搞混了代码逻辑就全乱。
我见过最多的低级 bug 之一:在 onCreate 里初始化数据库,结果切到后台再回来,数据库又初始化了一遍,数据全乱了。同学,你压根没搞懂 UIAbility 的生命周期啊。

A hand-drawn doodle illustration on pure white pap

UIAbility 是 HarmonyOS7 应用的基本单元——你可以理解为 Android 的 Activity,但粒度更大。一个应用可以有多个 UIAbility,每个对应最近任务列表里的一条记录。

A hand-drawn doodle illustration on pure white pap

系统在调度 UIAbility 的时候,会依次调用几个生命周期回调。你的代码写在哪个回调里,直接决定了它的执行时机。 写错位置,轻则重复执行,重则数据丢失。今天把这 4 个关键回调掰开了讲。

UIAbility 是什么

一句话:UIAbility 是系统调度的基本单元,提供窗口让 UI 渲染。

所有页面和组件要显示,必须依托一个窗口。UIAbility 就是创建窗口的那个"壳"——你写的 @Entry 页面,都是通过 UIAbility 的 windowStage.loadContent() 加载进去的。

一个 UIAbility 可以管理多个页面(通过路由跳转),也可以只有单个页面。具体怎么拆分看业务需要。

4 个关键回调详解

UIAbility 的生命周期回调有 6 个,但最核心的是这 4 个:

onCreate → onWindowStageCreate → onForeground → onBackground
                                              ↕
                                         (可反复切换)

加上销毁阶段的 onWindowStageDestroyonDestroy,一共 6 个。重点看前 4 个。

onCreate:应用启动,初始化就放这

系统创建 UIAbility 实例时触发,整个生命周期只调一次。

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

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    console.info('onCreate: 应用启动')
    
    // 读取启动参数
    let param = want.parameters?.['userId'] as string
    console.info(`启动参数: ${param}`)
    
    // 初始化全局资源(数据库连接、网络配置等)
    AppStorage.setOrCreate('globalConfig', { theme: 'dark' })
  }
}

A hand-drawn doodle illustration on pure white pap

关键代码讲解:

  • want 参数 —— 启动时传进来的数据,别的 Ability 或系统拉起你时可以通过 want.parameters 传参
  • launchParam —— 包含启动模式信息,可以判断是冷启动还是热启动
  • AppStorage.setOrCreate() —— 在这里设置全局数据,所有页面都能访问

血泪教训:只放一次性初始化逻辑,别把每次切回前台都要执行的代码放这里。onCreate 只跑一次,切后台再回来不会重新触发。

onWindowStageCreate:窗口创建,加载页面

onCreate 之后,系统创建窗口,触发这个回调。加载入口页面就在这里。

onWindowStageCreate(windowStage: window.WindowStage): void {
  console.info('onWindowStageCreate: 窗口已创建')

  // 设置窗口属性
  windowStage.getMainWindow().then((win: window.Window) => {
    win.setWindowLayoutFullScreen(true)
    let sysBarProps: window.SystemBarProperties = {
      statusBarColor: '#00000000',
      statusBarContentColor: '#000000'
    }
    win.setWindowSystemBarProperties(sysBarProps)
  })

  // 加载入口页面
  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      console.error(`页面加载失败: ${err.code}`)
      return
    }
    console.info('页面加载成功')
  })
}

关键代码讲解:

  • windowStage.getMainWindow() —— 获取主窗口对象,可以设置全屏、状态栏颜色等
  • setWindowLayoutFullScreen(true) —— 设置沉浸式状态栏,页面内容延伸到状态栏区域
  • windowStage.loadContent('pages/Index') —— 这行最关键,指定要加载的页面路径,路径对应 pages/ 目录下的文件
  • 页面加载是异步的,回调里可以判断是否成功

很多开发者喜欢在这里初始化数据库,也可以,但注意这只执行一次,别重复初始化。

onForeground:切到前台,恢复状态

应用从后台切回前台时触发。用户看到的界面要刷新、暂停的任务要恢复,都在这里处理。

onForeground(): void {
  console.info('onForeground: 切到前台')

  // 恢复暂停的任务
  this.resumeBackgroundTask()
  
  // 刷新可能过期的数据
  this.refreshDataIfNeeded()
  
  // 通知所有页面恢复
  AppStorage.setOrCreate('isForeground', true)
}

private resumeBackgroundTask(): void {
  let taskInfo = AppStorage.get<string>('pausedTask')
  if (taskInfo) {
    console.info(`恢复任务: ${taskInfo}`)
  }
}

private refreshDataIfNeeded(): void {
  let lastUpdate = AppStorage.get<number>('lastUpdateTime') || 0
  let now = Date.now()
  if (now - lastUpdate > 30000) {
    console.info('数据已过期,重新拉取')
    AppStorage.setOrCreate('needRefresh', true)
  }
}

关键代码讲解:

  • 这个回调每次从后台回到前台都会触发,可以反复调用
  • AppStorage.setOrCreate('isForeground', true) —— 页面组件可以通过 @StorageLink 监听这个值,自动响应前后台切换
  • 数据过期检查 —— 应用在后台可能很久,回到前台时数据可能已经过时,需要判断是否刷新

这才是放"切回前台逻辑"的正确位置,不是 onCreate

onBackground:切到后台,保存状态

用户按 Home 键、切到别的 App,应用进入后台,触发 onBackground保存状态、暂停任务、释放资源,就在这里。

onBackground(): void {
  console.info('onBackground: 切到后台')

  // 保存当前状态
  this.saveAppState()
  
  // 暂停耗时操作
  this.pauseOngoingTasks()
  
  // 释放不必要的资源
  this.releaseIdleResources()
  
  AppStorage.setOrCreate('isForeground', false)
  AppStorage.setOrCreate('lastUpdateTime', Date.now())
}

private saveAppState(): void {
  let state = {
    currentPage: 'DetailPage',
    scrollPosition: 256,
    formData: { name: '测试', age: 20 }
  }
  AppStorage.setOrCreate('appState', JSON.stringify(state))
}

private pauseOngoingTasks(): void {
  console.info('暂停进行中的任务')
}

private releaseIdleResources(): void {
  console.info('释放空闲资源')
}

关键代码讲解:

  • saveAppState() —— 把关键状态序列化存到 AppStorage,下次回来可以恢复
  • 切后台后进程随时可能被杀,重要数据必须在这里持久化,不能指望下次 onForeground 还能恢复内存中的数据
  • 释放资源 —— 比如关闭不必要的文件句柄、取消网络请求等,减少内存占用,降低被系统杀掉的概率

WindowStage 生命周期

WindowStage 也有自己的生命周期,夹在 UIAbility 生命周期之间:

onWindowStageCreate → onWindowStageDestroy
  • onWindowStageCreate:窗口创建时触发,在这里加载页面
  • onWindowStageDestroy:窗口销毁时触发,在 UIAbility 销毁之前执行

如果你在 onWindowStageCreate 里做了资源分配(比如创建数据库连接),对应的清理逻辑应该放在 onWindowStageDestroy 里。

与 UI 生命周期关系

UIAbility 生命周期是宏观的,组件生命周期是微观的。它们的关系:

UIAbility.onCreate
    │
UIAbility.onWindowStageCreate
    │
    ▼
页面 aboutToAppear ←── 组件 aboutToAppear
    │
    ▼
页面 onPageShow     ←── 用户可见
    │
    ▼
UIAbility.onForeground
    │
  ─ ─ ─ 用户使用中 ─ ─ ─
    │
UIAbility.onBackground
    │
    ▼
页面 onPageHide     ←── 用户不可见
    │
    ▼
UIAbility.onWindowStageDestroy
    │
    ▼
UIAbility.onDestroy
    │
    ▼
页面 aboutToDisappear ←── 组件 aboutToDisappear

关键理解:UIAbility 生命周期管理的是进程级别的事,组件生命周期管理的是 UI 级别的事。 别搞混了。

实战:页面状态保存

最常见的场景:用户在详情页滑到一半,切到别的 App,回来后页面位置丢了。用生命周期回调来保存和恢复:

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

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    AppStorage.setOrCreate('scrollOffset', 0)
    AppStorage.setOrCreate('currentRoute', 'Index')
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index')
  }

  onForeground(): void {
    let savedOffset = AppStorage.get<number>('scrollOffset') || 0
    if (savedOffset > 0) {
      AppStorage.setOrCreate('restoreScroll', true)
    }
  }

  onBackground(): void {
    AppStorage.setOrCreate('isForeground', false)
    AppStorage.setOrCreate('lastBackgroundTime', Date.now())
  }
}

页面侧配合恢复:

@Entry
@Component
struct DetailPage {
  @StorageLink('scrollOffset') scrollOffset: number = 0
  @StorageLink('restoreScroll') needRestore: boolean = false
  private scroller: Scroller = new Scroller()

  onPageShow(): void {
    if (this.needRestore && this.scrollOffset > 0) {
      setTimeout(() => {
        this.scroller.scrollOffset(this.scrollOffset)
        this.needRestore = false
      }, 100)
    }
  }

  onPageHide(): void {
    this.scrollOffset = this.scroller.currentOffset().yOffset
  }

  build() {
    Scroll(this.scroller) {
      Column() {
        ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (item: number) => {
          Text(`${item} 项内容`).height(120)
        })
      }
    }
  }
}

关键代码讲解:

  • onPageHide 里保存滚动偏移量到 AppStorage —— 切后台时页面先隐藏,这是最后的保存机会
  • onForeground 里设置 restoreScroll 标记 —— 通知页面需要恢复滚动位置
  • onPageShow 里执行恢复 —— 页面重新可见时,根据标记和保存的偏移量滚动回去
  • setTimeout 延迟 100ms —— 等页面渲染完再滚动,否则可能还没布局好就跳了

写在最后

UIAbility 生命周期就一句话:onCreate 初始化一次,onForeground 恢复状态,onBackground 保存状态,别搞混。

最常犯的错误就是把本该放在 onForeground 的逻辑写到了 onCreate,结果冷启动正常、热启动出 bug。记住 onCreate 只调一次,前后台切换不会重新触发——这点搞明白了,生命周期的坑就踩得差不多了。

Logo

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

更多推荐