NearPlay EntryAbility 生命周期与应用启动流程

一、Ability 框架概述

HarmonyOS 的 Ability 框架是应用组件化开发的核心基础设施,它定义了应用与系统之间交互的基本单元。在 HarmonyOS 的设计哲学中,Ability 既是应用的功能载体,也是系统进行资源调度与生命周期管理的基本单位。理解 Ability 框架的完整架构,是掌握 NearPlay 应用启动与运行机制的前提。

1.1 UIAbility 与 ExtensionAbility

HarmonyOS 将 Ability 分为两大类:UIAbility 和 ExtensionAbility。两者在设计目标、生命周期模型、运行模式和系统调度策略上存在根本性差异。

UIAbility 是承载用户界面交互的核心组件。每个 UIAbility 拥有独立的 WindowStage(窗口舞台),能够加载 ArkUI 声明式页面、管理页面路由栈、响应用户输入事件。UIAbility 的生命周期由系统严格管控,包含 onCreate、onWindowStageCreate、onForeground、onBackground、onWindowStageDestroy、onDestroy 六个核心回调。UIAbility 的典型使用场景包括:应用的入口页面、独立的业务功能模块、需要独立窗口的交互界面等。在 NearPlay 应用中,EntryAbility 就是唯一一个 UIAbility,它承载了应用的全部页面路由与用户交互。

ExtensionAbility 是无界面扩展组件的基类,用于提供特定场景下的系统能力扩展。与 UIAbility 不同,ExtensionAbility 不拥有独立的 WindowStage,不能直接加载 UI 页面,其生命周期也更为精简。HarmonyOS 为不同的扩展场景定义了多种 ExtensionAbility 子类:BackupExtensionAbility 用于数据备份恢复、ServiceExtensionAbility 用于后台长时任务、FormExtensionAbility 用于卡片服务、ShareExtensionAbility 用于跨应用分享等。在 NearPlay 中,EntryBackupAbility 就是 BackupExtensionAbility 的具体实现,用于支持应用的云端备份与恢复能力。

两者的核心区别总结如下:

维度 UIAbility ExtensionAbility
UI 能力 拥有 WindowStage,可加载页面 无 WindowStage,无 UI
生命周期回调 六个核心回调 子类定义的简化回调
运行模式 独立进程或共享进程 由宿主应用进程管理
用户可见性 可与用户直接交互 透明运行,用户无感知
系统调度 主动调度,支持前后台切换 被动触发,任务完成后销毁

1.2 AbilityStage

AbilityStage 是 HarmonyOS Ability 框架中的一个重要概念,它是 HAP 模块级别的生命周期组件。每个 HAP 模块可以配置一个 AbilityStage,当该 HAP 模块首次被加载到内存时,系统会创建 AbilityStage 实例并调用其 onCreate 回调。AbilityStage 的核心价值在于它提供了一个模块级别的初始化时机,早于该模块内任何 UIAbility 的 onCreate。

AbilityStage 提供两个核心回调:

  • onCreate():HAP 模块首次加载时调用,适合执行模块级别的全局初始化操作,如加载模块级配置、初始化共享数据、预加载资源等。
  • onAcceptWant(want):当以隐式 Want 启动 Ability 时,系统通过此回调确定目标 Ability 实例。这在多实例场景下尤为重要。

在 NearPlay 应用的当前版本中,并未显式配置 AbilityStage。这意味着系统使用默认的行为:模块加载后直接创建 EntryAbility 实例,跳过 AbilityStage 的 onCreate 阶段。这在单 Ability 场景下是完全合理的简化。当未来扩展多 Ability 时,AbilityStage 将承担 Want 分发和模块级初始化的职责。

1.3 生命周期回调体系

UIAbility 的六个生命周期回调构成了一个完整的状态机模型,精确描述了 Ability 从创建到销毁的全过程:

┌────────────────────────────────────────────────────┐
│              UIAbility 生命周期状态机                  │
├────────────────────────────────────────────────────┤
│                                                     │
│  [INITIAL] ──onCreate()──► [CREATED]                │
│                               │                     │
│                    onWindowStageCreate()             │
│                               │                     │
│                               ▼                     │
│                        [WINDOW_CREATED]             │
│                               │                     │
│                       onForeground()                │
│                               │                     │
│                               ▼                     │
│     ┌───────────────── [FOREGROUND] ◄────┐          │
│     │                          │         │          │
│     │                  onBackground()  onForeground()│
│     │                          │         │          │
│     │                          ▼         │          │
│     └──────────────► [BACKGROUND] ───────┘          │
│                                                     │
│  从 [BACKGROUND]:                                   │
│     onWindowStageDestroy() → [WINDOW_DESTROYED]     │
│     onDestroy() → [DESTROYED]                       │
│                                                     │
└───────────────────────────────────────────────────┘

每个回调的调用时机和适用操作:

  • onCreate:Ability 实例创建时,执行一次性的初始化逻辑。
  • onWindowStageCreate:主窗口创建完成,此时可设置窗口属性并加载首个页面。
  • onForeground:Ability 从后台回到前台,恢复与用户交互相关的状态和资源。
  • onBackground:Ability 退到后台,释放无需在前台持有的资源。
  • onWindowStageDestroy:主窗口销毁,释放与 UI 相关的资源。
  • onDestroy:Ability 即将销毁,执行最终的清理工作。

理解这个状态机对于正确管理 NearPlay 游戏页面的定时器、网络连接、音频播放等资源至关重要。


二、EntryAbility.ets 完整代码解读

下面逐行解读 NearPlay 应用的 EntryAbility.ets 源码,深入分析每个回调的实现细节与设计考量。

2.1 模块导入与常量定义

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

const DOMAIN = 0x0000;

代码从三个 Kit 导入所需能力:

  • @kit.AbilityKit:提供 UIAbility 基类、AbilityConstant(启动参数枚举)、ConfigurationConstant(配置常量)、Want(意图数据载体)。这是 Ability 框架的核心 Kit。
  • @kit.PerformanceAnalysisKit:提供 hilog 日志工具,用于输出带域名标签的结构化日志。hilog 是 HarmonyOS 推荐的日志方案,支持按域名、标签、级别过滤。
  • @kit.ArkUI:提供 window 模块,用于窗口管理操作,包括 WindowStage 的类型定义和窗口属性设置。

DOMAIN 常量值为 0x0000,这是 hilog 的域名标识。在正式应用中,建议使用项目独有的域名值以避免日志冲突,当前使用 0x0000 是开发阶段的简化处理。

2.2 onCreate 回调

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  try {
    this.context.getApplicationContext().setColorMode(
      ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
    );
  } catch (err) {
    hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
  }
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}

onCreate 是 Ability 生命周期中最先被调用的回调,在 Ability 实例创建时触发。它接收两个参数:

  • want: Want:包含启动当前 Ability 的意图信息,包括 action、entity、uri、parameters 等。当用户点击桌面图标启动应用时,want 中携带的是系统合成的隐式意图;当通过代码显式启动时,want 中携带的是开发者构造的显式意图。
  • launchParam: AbilityConstant.LaunchParam:启动参数,包含 launchReason(启动原因,如冷启动、热启动、连续调用等)和 lastExitReason(上次退出原因,如正常退出、被系统回收等)。

当前 onCreate 的实现做了两件事:第一,通过 this.context.getApplicationContext().setColorMode() 设置应用级颜色模式为 COLOR_MODE_NOT_SET,这意味着应用将跟随系统深色/浅色模式自动切换,而不是强制指定某种模式。setColorMode 调用被 try-catch 包裹,说明这是一个可能失败的操作——在特定系统版本或配置下可能抛出异常。第二,输出 onCreate 日志,便于开发调试时追踪生命周期时序。

值得注意的是,onCreate 中并未进行页面状态初始化或数据加载。这是因为 NearPlay 的设计遵循了"延迟初始化"原则:数据加载在页面的 aboutToAppear 中触发,而非在 Ability 的 onCreate 中提前执行。这种设计使得页面之间的数据加载相互独立,避免了在 onCreate 中加载所有页面数据导致的内存浪费。

2.3 onDestroy 回调

onDestroy(): void {
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}

onDestroy 在 Ability 实例即将被系统销毁时调用。这是生命周期中最后一次执行代码的机会,适合释放全局资源、断开网络连接、保存持久化数据等。当前实现仅输出日志,未执行实质性的清理操作。这在单 Ability 且页面自行管理资源释放的场景下是可接受的——各游戏页面在 aboutToDisappear 中清理各自的定时器和资源。

2.4 onWindowStageCreate 回调

onWindowStageCreate(windowStage: window.WindowStage): void {
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
      return;
    }
    hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
  });
}

onWindowStageCreate 是 UIAbility 独有的回调,在主窗口创建完成后触发。参数 windowStage 是窗口舞台对象,它是 UI 内容的容器,提供了 loadContent(加载页面内容)、setUIContent(设置 UI 内容)等核心方法。

当前实现的核心操作是 windowStage.loadContent('pages/Index'),将 pages/Index 页面作为应用的入口页面加载到主窗口中。loadContent 是异步操作,回调函数接收 Error 对象,通过检查 err.code 判断加载是否成功。失败时输出错误日志,成功时输出确认日志。

这里的 'pages/Index' 是页面的逻辑路径,对应 main_pages.json 中注册的页面路由。系统会根据 module.json5 中的 "pages": "$profile:main_pages" 配置找到 main_pages.json,再根据其中注册的页面列表解析路径。

2.5 onForeground 与 onBackground 回调

onForeground(): void {
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}

onBackground(): void {
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}

onForeground 在 Ability 从后台切换到前台时调用,onBackground 在 Ability 从前台切换到后台时调用。当前两个回调仅输出日志,未执行资源管理操作。这在游戏应用中是一个需要关注的点——如果游戏页面中有正在运行的定时器(如狼人杀的倒计时、你画我猜的轮次计时),当应用退到后台时,这些定时器仍会持续运行,消耗系统资源。理想情况下,onBackground 中应通知当前活跃页面暂停游戏逻辑。

2.6 onWindowStageDestroy 回调

onWindowStageDestroy(): void {
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}

onWindowStageDestroy 在主窗口销毁时调用,通常发生在 Ability 即将销毁之前或窗口配置变更时。此回调适合释放与窗口绑定的 UI 资源,如自定义的窗口事件监听器、沉浸式状态栏配置等。当前实现仅输出日志。

2.7 完整生命周期时序图

  用户点击桌面图标
        │
        ▼
  ┌──────────────┐
  │   onCreate    │ ← 设置颜色模式、日志记录
  └──────┬───────┘
         │
         ▼
  ┌─────────────────────┐
  │ onWindowStageCreate  │ ← 创建主窗口、加载 pages/Index
  └──────┬───────────────┘
         │
         ▼
  ┌──────────────┐
  │  onForeground │ ← Ability 进入前台,页面可见
  └──────┬───────┘
         │
    ┌────┴────┐
    │  用户交互  │ ← 页面路由跳转、游戏操作
    └────┬────┘
         │
         ▼
  ┌──────────────┐
  │  onBackground │ ← 用户按Home/切到其他应用
  └──────┬───────┘
         │
         ▼         onForeground(再次回到前台)
  ┌────────────────────────────────────┐
  │      [后台等待中]  ◄─────── 再次切回  │
  └──────┬─────────────────────────────┘
         │ (用户关闭应用 / 系统回收)
         ▼
  ┌──────────────────────┐
  │ onWindowStageDestroy │ ← 窗口销毁
  └──────┬───────────────┘
         │
         ▼
  ┌──────────────┐
  │   onDestroy   │ ← Ability 销毁
  └──────────────┘

三、应用启动流程

应用启动是用户体验的第一步,也是系统资源调度的起点。HarmonyOS 应用的启动流程涉及系统服务、应用框架、Ability 生命周期、窗口管理、页面渲染等多个层次的协作。本节将详细追踪 NearPlay 应用从用户点击桌面图标到首页完成渲染的完整过程。

3.1 冷启动全流程

冷启动是指应用进程不存在时的启动方式,需要从头创建进程、初始化运行时、加载 HAP 包、创建 Ability 实例。这是最完整的启动路径,也是耗时最长的启动方式。

用户点击桌面图标
      │
      ▼
┌─────────────────────────────┐
│ 1. 桌面 Launcher 发出 Want  │
│    action: ohos.want.action.home │
│    entity:  entity.system.home   │
│    包名: com.nearplay              │
└─────────────┬───────────────┘
              │
              ▼
┌────────────────────────────┐
│ 2. 系统包管理服务 (BMS)     │
│    解析 Want → 匹配 skills  │
│    定位 EntryAbility        │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│ 3. 应用管理服务 (AMS)       │
│    检查进程是否已存在        │
│    不存在 → 创建新进程       │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│ 4. 进程创建与运行时初始化    │
│    fork/zygote 派生进程      │
│    初始化 ArkTS 运行时       │
│    加载 HAP 包资源           │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│ 5. AbilityStage.onCreate()  │
│    (当前未配置,跳过)       │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│ 6. EntryAbility.onCreate()  │
│    设置颜色模式              │
│    输出生命周期日志           │
└─────────────┬───────────────┘
              │
              ▼
┌───────────────────────────────┐
│ 7. EntryAbility                │
│    onWindowStageCreate()       │
│    windowStage.loadContent()   │
│    → 加载 'pages/Index'       │
└─────────────┬──────────────────┘
              │
              ▼
┌─────────────────────────────┐
│ 8. EntryAbility.onForeground()│
│    Ability 进入前台状态       │
└─────────────┬───────────────┘
              │
              ▼
┌────────────────────────────┐
│ 9. Index 页面构建与渲染      │
│    build() 执行              │
│    aboutToAppear() 触发      │
│    页面数据初始化             │
└─────────────┬───────────────┘
              │
              ▼
      [用户可见首页]

3.2 各阶段详细分析

阶段1:桌面 Launcher 发出 Want

当用户在桌面上点击 NearPlay 的应用图标时,Launcher 组件构造一个隐式 Want 并发送给系统。这个 Want 的 action 为 ohos.want.action.home,entity 为 entity.system.home,包名为 NearPlay 的 bundleName。这两个标识的组合构成了"从桌面启动应用"的语义,系统通过 skills 匹配机制将此 Want 路由到正确的 Ability。

阶段2:BMS 解析与匹配

Bundle Management Service(包管理服务)接收到 Want 后,执行 skills 匹配算法。它在所有已安装应用的 module.json5 中搜索 abilities 节点下的 skills 配置,寻找与 Want 中 action 和 entity 匹配的 Ability。NearPlay 的 EntryAbility 在 module.json5 中声明了:

"skills": [{
  "entities": ["entity.system.home"],
  "actions": ["ohos.want.action.home"]
}]

BMS 的匹配规则是:Want 中的 action 必须在 skills.actions 中存在,Want 中的所有 entity 必须在 skills.entities 中存在。匹配成功后,BMS 将 EntryAbility 确定为目标 Ability。

阶段3-4:进程创建与运行时初始化

由于是冷启动,应用进程尚不存在。AMS(Ability Manager Service)通知系统创建新的应用进程。在 HarmonyOS 中,新进程通过 zygote 派生机制创建,子进程继承预初始化的 ArkTS 运行时环境。进程创建后,系统加载 NearPlay 的 HAP 包,将代码和资源映射到进程的内存空间。

阶段5:AbilityStage.onCreate()

如果模块配置了 AbilityStage,此时会触发其 onCreate 回调。NearPlay 当前未配置 AbilityStage,因此跳过此阶段。在多 Ability 架构下,AbilityStage 可用于执行模块级的全局初始化,如建立数据库连接、注册全局事件监听等。

阶段6:EntryAbility.onCreate()

系统通过反射机制创建 EntryAbility 实例并调用 onCreate。此时 Ability 的 context 已经可用,可以访问应用级上下文。当前实现中,onCreate 调用 this.context.getApplicationContext().setColorMode() 设置颜色模式,并输出日志。这个阶段不应该执行耗时操作,因为系统对 onCreate 的执行时间有限制,超时可能导致 ANR(Application Not Responding)。

阶段7:onWindowStageCreate() 与页面加载

这是启动流程中最关键的阶段。系统为主窗口创建 WindowStage 实例,然后调用 onWindowStageCreate。在回调中,windowStage.loadContent('pages/Index') 触发页面加载管线:

  1. 系统根据 'pages/Index' 查找 main_pages.json 中注册的页面路径
  2. 加载 Index 页面的 ArkTS 组件定义
  3. 创建 Index 组件实例,执行构造函数
  4. 调用 Index.build() 生成组件树
  5. ArkUI 框架执行布局与渲染
  6. 将渲染结果提交给 WindowStage 显示

阶段8:onForeground()

页面加载完成后,系统调用 onForeground 将 Ability 切换到前台状态。此时应用窗口获得焦点,用户可以开始交互。onForeground 的调用标志着启动流程的完成,应用进入活跃状态。

阶段9:Index.aboutToAppear()

在 build() 执行之后、组件挂载到组件树之前,aboutToAppear 回调被触发。这是页面级数据初始化的标准时机。在 NearPlay 中,Index 页面的 aboutToAppear 可能执行活动列表加载、用户状态检查、权限验证等操作。

3.3 热启动与温启动

除了冷启动,应用还可能以热启动或温启动方式运行:

  • 热启动:应用进程存在,Ability 实例存在且在后台,用户切回时仅触发 onForeground,无 onCreate 和 onWindowStageCreate。启动耗时极短。
  • 温启动:应用进程存在,但 Ability 实例已被系统销毁(如内存不足时回收),需要重新执行 onCreate → onWindowStageCreate → onForeground。由于进程已存在,省去了进程创建和运行时初始化的耗时。
启动类型对比:

冷启动:  [进程创建] → [运行时初始化] → [HAP加载] → onCreate → onWindowStageCreate → onForeground
温启动:  (进程已存在)                    → onCreate → onWindowStageCreate → onForeground
热启动:  (进程+Ability已存在)                                → onForeground

对于 NearPlay 这样的游戏应用,热启动场景最常见——用户在游戏进行中切到微信回消息,再切回来只触发 onForeground,游戏页面状态完整保留。

3.4 启动耗时优化策略

虽然 NearPlay 当前的启动流程相对简单,但随着功能增长,启动耗时可能成为问题。以下是关键优化方向:

  1. onCreate 瘦身:将非必要的初始化操作延迟到 aboutToAppear 或更晚时机。当前 onCreate 中仅设置颜色模式,这是合理的。
  2. 页面预加载:对于可预判的后续页面(如热门游戏页面),可在 Index 加载完成后异步预加载组件定义。
  3. 资源异步加载:图片、字体等资源采用异步加载策略,避免阻塞首帧渲染。
  4. 启动窗口优化:module.json5 中的 startWindowIcon 和 startWindowBackground 配置了启动窗口的外观,确保用户体验的连续性。

四、WindowStage 配置

WindowStage 是 UIAbility 的窗口舞台,是连接 Ability 生命周期与 ArkUI 页面渲染的桥梁。本节深入探讨 WindowStage 的配置策略与窗口属性管理。

4.1 loadContent 与 setUIContent

WindowStage 提供两种加载页面内容的方法:

loadContent(path, callback):通过页面路径加载内容。这是 NearPlay 当前使用的方式,windowStage.loadContent('pages/Index') 将 Index 页面加载到主窗口。path 参数对应 main_pages.json 中注册的页面名称。loadContent 会在内部创建页面组件实例、执行 build()、完成布局渲染,整个过程是异步的。

setUIContent(path, callback):功能与 loadContent 类似,但在特定场景下有细微差异。setUIContent 更侧重于"设置"已有窗口的内容,而 loadContent 更侧重于"加载"页面到窗口。在实际开发中,两者可互换使用,但建议在同一项目中保持一致性。

NearPlay 选择 loadContent 是 HarmonyOS 默认模板的推荐做法,语义上更清晰——在窗口创建阶段"加载"入口页面。

4.2 窗口属性配置

在 onWindowStageCreate 中,除了加载页面,还可以配置窗口属性:

onWindowStageCreate(windowStage: window.WindowStage): void {
  // 1. 获取主窗口实例
  let mainWindow = windowStage.getMainWindowSync();
  
  // 2. 设置全屏模式
  mainWindow.setWindowLayoutFullScreen(true);
  
  // 3. 配置状态栏
  let sysBarProps: window.SystemBarProperties = {
    statusBarColor: '#00000000',
    statusBarContentColor: '#000000'
  };
  mainWindow.setWindowSystemBarProperties(sysBarProps);
  
  // 4. 加载页面
  windowStage.loadContent('pages/Index');
}

对于 NearPlay 游戏应用,全屏沉浸式体验尤为重要。在狼人杀、你画我猜等游戏中,状态栏和导航栏会分散注意力,影响游戏沉浸感。虽然当前代码未显式配置全屏模式,但这是后续优化的重要方向。

4.3 全屏与状态栏策略

HarmonyOS 提供灵活的状态栏控制能力:

  • 沉浸式全屏setWindowLayoutFullScreen(true) 使页面布局延伸到状态栏区域,但状态栏图标仍然显示。页面内容需要考虑安全区避让。
  • 隐藏状态栏setWindowSystemBarEnable('status', false) 完全隐藏状态栏,适合全屏游戏场景。
  • 自定义状态栏样式:通过 setWindowSystemBarProperties 设置状态栏背景色和内容色,使其与页面风格统一。

NearPlay 的页面如 WerewolfGame、DrawGuessGame 等游戏页面,在游戏进行阶段应考虑切换为沉浸式全屏模式,在暂停或结算时恢复状态栏显示。这需要在页面级代码中通过 window.getLastWindow(getContext(this)) 获取窗口实例并动态调整。

4.4 窗口尺寸与方向

WindowStage 还支持配置窗口尺寸和显示方向。对于手机端 NearPlay,默认竖屏方向即可满足大多数页面需求。但某些游戏(如你画我猜的绘画界面)可能更适合横屏模式,可通过 mainWindow.setPreferredOrientation(window.Orientation.LANDSCAPE) 动态切换。这种页面级的方向切换应在页面的 aboutToAppear 中设置,aboutToDisappear 中恢复,避免影响其他页面。


五、onForeground/onBackground

前后台切换是移动应用最频繁的状态转换之一。用户在使用 NearPlay 进行游戏时,可能因为接电话、回消息、查看通知等操作频繁在前后台之间切换。正确处理 onForeground 和 onBackground 回调,对于保障用户体验和系统资源效率至关重要。

5.1 前后台切换触发时机

┌────────────────────────────────────────────────┐
│            前后台切换触发场景                       │
├────────────────────────────────────────────────┤
│                                                  │
│  onForeground 触发:                              │
│  ├─ 用户从最近任务列表切回应用                     │
│  ├─ 用户点击桌面图标重新打开应用(热启动)          │
│  ├─ 从通知栏点击进入应用                          │
│  └─ 其他应用通过 startAbility 跳转到本应用         │
│                                                  │
│  onBackground 触发:                              │
│  ├─ 用户按 Home 键回到桌面                        │
│  ├─ 用户从底部上滑回到桌面                        │
│  ├─ 用户切换到其他应用                            │
│  ├─ 系统弹窗覆盖应用(如来电、权限弹窗)           │
│  └─ 锁屏                                         │
│                                                  │
└────────────────────────────────────────────────┘

需要特别注意的是:系统弹窗(如来电界面、权限请求对话框)覆盖应用时,也会触发 onBackground。当弹窗消失后,触发 onForeground。这意味着 onBackground/onForeground 的触发频率可能比预期更高。

5.2 资源释放时机

onBackground 是释放非必要资源的最佳时机。对于 NearPlay 游戏应用,以下资源应在退到后台时考虑释放或暂停:

必须暂停的资源

  • 游戏定时器(setInterval/setTimeout):狼人杀的夜晚倒计时、快速反应游戏的答题计时器等,退到后台时应暂停,回到前台时恢复或重新开始。
  • 音频播放:背景音乐和音效应暂停,避免在后台持续播放干扰用户。
  • 动画:Lottie 动画、属性动画等应暂停,减少 GPU 负载。
  • 实时网络连接:WebSocket 长连接可能需要断开或进入低功耗模式。

可以保留的资源

  • 页面路由栈:Router 管理的页面栈应完整保留,确保回到前台时用户看到的是离开前的页面。
  • @State 状态数据:组件的状态变量在 Ability 未销毁前一直有效。
  • 已加载的图片缓存:保留在内存中以便快速恢复显示。

当前 NearPlay 的不足
当前 onBackground 和 onForeground 仅输出日志,未执行任何资源管理操作。这意味着当用户在游戏进行中切到后台,游戏定时器仍在运行、音频仍在播放。这在实际发布时可能导致:

  1. 游戏逻辑错误(后台期间倒计时归零、轮次自动跳过)
  2. 系统资源浪费(后台应用占用过多资源可能被系统强制杀掉)
  3. 用户体验问题(后台突然播放音效吓到用户)

5.3 改进方案

推荐的改进方案是在 Ability 级别维护一个前台状态标志,并通过事件机制通知当前活跃页面:

// 在 EntryAbility 中
private isForeground: boolean = false;

onForeground(): void {
  this.isForeground = true;
  // 通过 emitter 或 AppStorage 通知页面
  AppStorage.setOrCreate('ability_foreground', true);
}

onBackground(): void {
  this.isForeground = false;
  AppStorage.setOrCreate('ability_foreground', false);
}

游戏页面监听此状态变化:

@StorageLink('ability_foreground') isForeground: boolean = true;

aboutToAppear(): void {
  if (!this.isForeground) {
    this.pauseGame();
  }
}

onIsForegroundChange(): void {
  if (this.isForeground) {
    this.resumeGame();
  } else {
    this.pauseGame();
  }
}

这种方案将 Ability 级别的前后台状态传递到页面级别,使页面能够做出恰当的响应。除了 AppStorage 方案,还可以使用 emitter 事件机制实现 Ability 到页面的通知,emitter 的优势是支持携带复杂数据参数,适合传递暂停原因、恢复策略等附加信息。

5.4 后台保活与系统回收风险

HarmonyOS 对后台应用有严格的生命周期管理策略。当系统内存不足时,会按照优先级回收后台应用进程。NearPlay 如果在 onBackground 中未释放足够资源,可能被系统判定为高内存占用而优先回收,导致用户切回时需要重新冷启动。

系统回收策略的优先级从低到高为:后台空进程 → 后台无服务进程 → 后台有服务进程 → 前台应用。NearPlay 作为无后台服务的游戏应用,在后台状态下处于较低优先级,被回收的风险较高。因此,onBackground 中主动释放非必要资源不仅是优化,更是降低被回收风险的必要措施。具体来说,应在 onBackground 中清除大型图片缓存、停止所有动画和定时器、断开非必要的网络连接,将内存占用降到最低水平。


六、生命周期与页面交互

UIAbility 生命周期与 ArkUI 页面生命周期是两个不同层级的概念,它们之间存在精妙的交互关系。理解这种关系对于正确管理 NearPlay 的页面资源至关重要,特别是游戏页面中定时器的清理问题。

6.1 页面生命周期回调

ArkUI 页面(@Page 装饰器标注的组件)拥有自己的生命周期回调:

  • aboutToAppear:页面即将可见时调用,在 build() 之前执行。这是页面数据初始化的标准时机。
  • aboutToDisappear:页面即将销毁时调用。这是清理页面资源的最后机会,必须在此清理 setInterval、setTimeout、事件监听器等。
  • onPageShow:页面每次显示时调用,包括首次显示和从其他页面返回时。
  • onPageHide:页面每次隐藏时调用,包括跳转到其他页面和切到后台时。
  • onBackPress:用户按返回键时调用,返回 true 可拦截默认返回行为。

6.2 Ability 生命周期与页面生命周期的对应关系

Ability 生命周期                页面生命周期
─────────────────              ─────────────

onCreate()                     
     │                         
onWindowStageCreate()          
     │                         
  loadContent('Index')         
     │                         aboutToAppear()  ← Index 页面创建
     │                         build()           ← Index 页面构建
     │                         onPageShow()      ← Index 页面显示
     │                         
onForeground()                 
     │                         
  [用户操作]                    
  Router.push('GameRoom')      
     │                         aboutToAppear()  ← GameRoom 页面创建
     │                         onPageShow()      ← GameRoom 页面显示
     │                         onPageHide()      ← Index 页面隐藏
     │                         
  Router.back()                
     │                         aboutToDisappear() ← GameRoom 页面销毁★
     │                         onPageShow()       ← Index 页面重新显示
     │                         
onBackground()                 
     │                         onPageHide()      ← 当前页面隐藏
     │                         
onForeground()                 
     │                         onPageShow()      ← 当前页面重新显示
     │                         
onWindowStageDestroy()         
     │                         
onDestroy()                    
                               aboutToDisappear() ← 当前页面销毁

6.3 关键理解:Router.back() 不触发 onDestroy

这是一个极其重要且容易误解的设计要点。当用户在游戏页面执行 Router.back() 返回上一页时:

  1. 当前页面(如 WerewolfGame)的 aboutToDisappear() 被调用
  2. 上一页面(如 Index)的 onPageShow() 被调用
  3. EntryAbilityonDestroy() 不会被调用

这是因为 Router 操作仅影响页面路由栈,不影响 Ability 的生命周期。Ability 在整个页面导航过程中始终保持活跃状态,其 onCreate 到 onDestroy 的生命周期跨越了所有页面的创建和销毁。

这种设计的实际意义在于:

  • Ability 级别的资源(如 context、全局配置)在整个导航过程中持续可用
  • 页面级别的资源(如定时器、页面数据)随页面销毁而释放
  • 应用不会因为页面返回而重新启动

6.4 aboutToDisappear 与定时器清理

对于 NearPlay 的游戏页面,aboutToDisappear 的正确实现是防止内存泄漏和逻辑错误的关键。以下是定时器清理的标准模式:

@Page
@Component
struct WerewolfGame {
  private nightTimer: number = -1;
  private voteTimer: number = -1;
  private animationTimer: number = -1;

  aboutToAppear(): void {
    this.nightTimer = setInterval(() => {
      // 夜晚倒计时逻辑
    }, 1000);
  }

  aboutToDisappear(): void {
    // 清理所有定时器
    if (this.nightTimer !== -1) {
      clearInterval(this.nightTimer);
      this.nightTimer = -1;
    }
    if (this.voteTimer !== -1) {
      clearInterval(this.voteTimer);
      this.voteTimer = -1;
    }
    if (this.animationTimer !== -1) {
      clearTimeout(this.animationTimer);
      this.animationTimer = -1;
    }
  }
}

未清理定时器的后果

  1. 页面已销毁但定时器仍在执行,回调中引用的 @State 变量已失效
  2. 回调尝试更新已销毁组件的状态,可能导致运行时异常
  3. 定时器持有闭包引用,阻止页面组件被垃圾回收,造成内存泄漏
  4. 多次进出同一游戏页面会累积越来越多的"僵尸"定时器

6.5 页面级与 Ability 级资源管理职责划分

┌────────────────────────────────────────────────┐
│              资源管理职责划分                       │
├─────────────────────────────────────────────────┤
│                                                  │
│  页面级(aboutToDisappear 中清理):              │
│  ├─ setInterval / setTimeout 定时器               │
│  ├─ 页面独占的网络请求                             │
│  ├─ 事件监听器(emitter.on 注册的监听)            │
│  ├─ 页面动画控制器                                │
│  └─ 页面级音频播放器                               │
│                                                  │
│  Ability级(onDestroy 中清理):                   │
│  ├─ 应用级网络连接(WebSocket 长连接)             │
│  ├─ 应用级数据存储引用                             │
│  ├─ 应用级事件监听器                               │
│  └─ 应用级后台任务                                 │
│                                                  │
│  前后台切换(onBackground 中暂停):               │
│  ├─ 游戏逻辑定时器                                 │
│  ├─ 音频播放                                      │
│  └─ 动画播放                                      │
│                                                  │
└─────────────────────────────────────────────────┘

七、ExtensionAbility 备份

7.1 EntryBackupAbility 配置

NearPlay 在 module.json5 的 extensionAbilities 节点中配置了 EntryBackupAbility:

"extensionAbilities": [{
  "name": "EntryBackupAbility",
  "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
  "type": "backup",
  "exported": false,
  "metadata": [{
    "name": "ohos.extension.backup",
    "resource": "$profile:backup_config"
  }]
}]

各字段的含义:

  • name:扩展能力名称,在模块内唯一标识此 ExtensionAbility。
  • srcEntry:源码路径,指向实现文件 EntryBackupAbility.ets。
  • type:扩展类型为 backup,表示这是一个备份扩展能力。
  • exported:设为 false,表示此扩展能力不对外暴露,仅应用内部使用。
  • metadata:引用 backup_config 配置文件,该文件定义了备份规则——哪些数据需要备份、哪些排除在外。

7.2 备份能力的作用

HarmonyOS 的备份框架允许应用在用户切换设备或恢复出厂设置后恢复应用数据。EntryBackupAbility 提供了应用自定义备份逻辑的入口,开发者可以在其中指定需要备份的数据文件、偏好设置等。备份恢复框架是 HarmonyOS 数据安全体系的重要组成部分,它与系统的云备份服务协同工作,确保用户在更换设备或系统重置后能够无缝恢复应用数据。

备份恢复的基本工作流程如下:当用户触发系统级备份操作时,系统依次调用各应用的 BackupExtensionAbility 的 onBackup 回调,应用在此回调中将需要备份的数据写入指定的备份目录。恢复操作则相反,系统调用 onRestore 回调,应用从备份目录读取数据并恢复到应用沙箱中。整个过程中,应用只需要关注数据的序列化与反序列化逻辑,传输与存储由系统框架负责。

对于 NearPlay 游戏,可考虑备份的数据包括:

  • 用户个人资料(昵称、头像、个性签名)
  • 游戏历史记录与战绩统计
  • 用户偏好设置(音效开关、主题选择、通知偏好等)
  • 好友列表与社交关系缓存
  • 进行中的游戏存档(如长周期剧本杀的进度)

当前 EntryBackupAbility 使用的是默认实现,未来可根据业务需求扩展自定义备份恢复逻辑。特别是在用户更换新手机时,能够恢复游戏历史和社交关系对于用户留存至关重要,这是后续版本迭代中值得优先实现的功能点。


八、Want 与隐式跳转

Want 是 HarmonyOS 应用间通信与跳转的核心机制,它定义了操作意图的数据结构。理解 Want 的工作原理和 skills 匹配机制,对于掌握 NearPlay 应用的启动入口配置和未来扩展多 Ability 架构至关重要。

8.1 Want 数据结构

Want 包含以下核心字段:

Want {
  bundleName: string      // 目标应用包名
  abilityName: string     // 目标 Ability 名称(显式跳转用)
  action: string          // 要执行的操作
  entities: string[]      // 操作的附加类别
  uri: string             // 数据 URI
  type: string            // MIME 类型
  parameters: Record      // 附加参数键值对
}

Want 分为显式 Want 和隐式 Want:

  • 显式 Want:同时指定 bundleName 和 abilityName,精确跳转到目标 Ability。如 Router.pushUrl({ url: 'pages/GameRoom' }) 是页面级的显式跳转,context.startAbility({ bundleName: 'com.nearplay', abilityName: 'EntryAbility' }) 是 Ability 级的显式跳转。

  • 隐式 Want:不指定 abilityName,仅通过 action、entity、uri 等信息让系统匹配目标 Ability。桌面图标启动应用就是隐式 Want 的典型场景。

8.2 Skills 配置解析

NearPlay 的 EntryAbility 在 module.json5 中声明了 skills:

"skills": [{
  "entities": ["entity.system.home"],
  "actions": ["ohos.want.action.home"]
}]

skills 的语义:skills 声明了 Ability 能够响应的 Want 类型,相当于 Ability 的"服务广告"。系统在收到隐式 Want 时,通过 skills 匹配决定将 Want 路由到哪个 Ability。

entity.system.home:表示"系统主屏幕"实体,用于标识从桌面启动的场景。当用户在桌面上点击应用图标时,系统发出的 Want 携带此 entity。

ohos.want.action.home:表示"回家"操作,即打开应用的主界面。这是桌面启动应用的标准 action。

这两个标识的组合形成了 HarmonyOS 应用入口的标准配置,所有需要在桌面显示图标的应用都必须声明此 skills。

8.3 隐式 Want 匹配算法

系统的隐式 Want 匹配遵循以下规则:

  1. action 匹配:Want 中指定的 action 必须在 skills.actions 列表中存在。如果 Want 包含多个 action,则每个 action 都必须被匹配。
  2. entity 匹配:Want 中指定的所有 entity 必须在 skills.entities 列表中存在。skills 可以声明比 Want 更多的 entity,但不允许少于 Want 要求的 entity。
  3. uri/type 匹配(可选):如果 Want 指定了 uri 或 type,skills 中相应的 uri/type 模式必须匹配。
  4. 优先级:如果多个 Ability 匹配成功,系统选择优先级最高的。显式 Want 优先于隐式 Want;同优先级时选择已运行的实例。
隐式 Want 匹配流程:

  Want { action: "ohos.want.action.home", entity: "entity.system.home" }
    │
    ▼
  遍历所有已安装应用的 skills 配置
    │
    ├─ EntryAbility skills: { actions: ["ohos.want.action.home"], entities: ["entity.system.home"] }
    │   ├─ action 匹配: ✓ ("ohos.want.action.home" in actions)
    │   ├─ entity 匹配: ✓ ("entity.system.home" in entities)
    │   └─ 匹配成功 → 选择此 Ability
    │
    └─ 其他 Ability skills: 无匹配

8.4 自定义 Skills 与跨应用跳转

除了系统标准的 skills,NearPlay 未来还可以定义自定义 skills 以支持跨应用跳转。例如,如果 NearPlay 提供游戏邀请功能,其他社交应用可以通过隐式 Want 直接跳转到 NearPlay 的特定游戏房间:

"skills": [{
  "entities": ["entity.system.home"],
  "actions": ["ohos.want.action.home"]
}, {
  "entities": ["entity.nearplay.game"],
  "actions": ["ohos.want.action.joingame"],
  "uris": [{
    "scheme": "nearplay",
    "host": "game",
    "path": "/join"
  }]
}]

其他应用通过构造如下 Want 即可跳转:

let want: Want = {
  action: 'ohos.want.action.joingame',
  entities: ['entity.nearplay.game'],
  uri: 'nearplay://game/join?roomId=12345'
};
this.context.startAbility(want);

这种基于隐式 Want 的跨应用通信机制是 HarmonyOS 分布式能力的基础,为 NearPlay 未来的社交邀请、跨设备游戏等场景提供了技术支撑。

8.5 exported 字段与安全模型

EntryAbility 的 exported: true 配置表示此 Ability 可以被其他应用启动。如果设为 false,则只能被同应用内的代码启动。对于应用入口 Ability,exported 必须为 true,否则桌面图标无法启动应用。

安全考量:exported 为 true 的 Ability 是应用的对外接口,必须对输入的 Want 参数进行严格的验证,防止恶意应用通过构造特殊 Want 进行攻击。在 NearPlay 中,当前 onCreate 仅读取 want 设置颜色模式,不解析 parameters 中的业务数据,因此不存在明显的安全风险。但当未来扩展自定义 skills 时,必须验证所有外部输入。


九、多 Ability 场景

9.1 为何当前只有 EntryAbility

NearPlay 当前版本仅配置了一个 UIAbility——EntryAbility。这是经过深思熟虑的架构决策,基于以下考量:

页面路由优先原则:HarmonyOS 的 Router 导航机制已经提供了完善的页面栈管理能力。NearPlay 的页面结构(Index → GameRoom → 具体游戏页面)是一个典型的线性导航流,完全可以通过 Router 在单一 Ability 内管理。引入多 Ability 会增加进程间通信的复杂性,但不会带来实质性的用户体验提升。

资源共享效率:单一 Ability 意味着所有页面共享同一个 WindowStage 和同一个 JS 运行时上下文。页面之间的数据传递可以通过 @State、AppStorage、Router 参数等多种方式实现,效率远高于跨 Ability 的数据传递(需要序列化/反序列化)。

开发复杂度控制:每个 Ability 需要独立的生命周期管理、窗口配置和状态保存。对于中小型应用,单一 Ability + 多页面的架构是最简单、最可靠的选择。

启动性能:单一 Ability 在冷启动时只需创建一个 Ability 实例和一次 WindowStage。多 Ability 架构可能导致系统需要决定启动哪个 Ability,增加匹配耗时。

9.2 未来扩展:SignAbility

当 NearPlay 引入独立的登录/注册模块时,可以考虑添加 SignAbility。登录模块与游戏主流程有天然的隔离边界:

┌────────────────────────────────────────────────┐
│           多 Ability 架构(未来规划)              │
├────────────────────────────────────────────────┤
│                                                 
│  ┌──────────────┐     ┌─────────────────────┐  │
│  │  SignAbility  │     │    EntryAbility       │  │
│  │  (登录/注册)  │     │    (主界面/游戏)      │  │
│  │              │     │                      │  │
│  │ pages/SignIn │     │ pages/Index          │  │
│  │ pages/SignUp │     │ pages/GameRoom       │  │
│  │ pages/Forgot │     │ pages/WerewolfGame   │  │
│  │              │     │ pages/DrawGuessGame  │  │
│  └──────┬───────┘     │ pages/ChatPage       │  │
│         │             │ ...                  │  │
│         │ startAbility│                      │  │
│         └────────────►│                      │  │
│                        └──────────────────────┘  │
│                                                 
│  ┌──────────────┐                               │
│  │ GameAbility   │     独立进程运行大型游戏       │
│  │ (独立游戏)    │     避免影响主应用稳定性       │
│  │              │                               │
│  │ pages/BigGame│                               │
│  └──────────────┘                               │
│                                                 │
└────────────────────────────────────────────────┘

SignAbility 的设计理由:

  • 登录流程有独立的页面栈,不应与游戏页面的 Router 栈混在一起
  • 登录完成后可以销毁 SignAbility 释放内存,而不是将登录页面保留在 Router 栈底部
  • 登录状态可以在 Ability 级别隔离,未登录用户只能看到 SignAbility

9.3 未来扩展:GameAbility

对于资源消耗较大的游戏(如实时联网的狼人杀),可以考虑独立的 GameAbility:

  • 进程隔离:GameAbility 可以配置为独立进程("launchType": "specified"),游戏崩溃不影响主应用
  • 独立窗口:游戏可以有自己的窗口配置(如横屏、全屏),不影响主应用的窗口设置
  • 生命周期独立:游戏退到后台时只触发 GameAbility 的 onBackground,不影响主应用的页面栈
  • 资源回收:游戏结束后销毁 GameAbility,立即释放所有游戏资源

module.json5 中的多 Ability 配置示例:

"abilities": [{
  "name": "EntryAbility",
  "srcEntry": "./ets/entryability/EntryAbility.ets",
  "exported": true,
  "skills": [{
    "entities": ["entity.system.home"],
    "actions": ["ohos.want.action.home"]
  }]
}, {
  "name": "SignAbility",
  "srcEntry": "./ets/signability/SignAbility.ets",
  "exported": true,
  "skills": [{
    "entities": ["entity.nearplay.sign"],
    "actions": ["ohos.want.action.signin"]
  }],
  "launchType": "singleton"
}, {
  "name": "GameAbility",
  "srcEntry": "./ets/gameability/GameAbility.ets",
  "exported": true,
  "skills": [{
    "entities": ["entity.nearplay.game"],
    "actions": ["ohos.want.action.playgame"]
  }],
  "launchType": "specified"
}]

launchType 的三种模式:

  • singleton:单实例模式,整个应用只存在一个此 Ability 实例(默认值)
  • standard:标准模式,每次 startAbility 都创建新实例
  • specified:指定实例模式,由 AbilityStage.onAcceptWant 决定是否复用已有实例

9.4 单 Ability 与多 Ability 的选择决策树

需要新增页面?
    │
    ├─ 页面与现有页面属于同一业务流程?
    │   ├─ 是 → 使用 Router 在现有 Ability 中导航(推荐)
    │   └─ 否 → 继续判断
    │
    ├─ 新页面需要独立的窗口配置(如横屏/全屏)?
    │   ├─ 否 → 使用 Router 在现有 Ability 中导航
    │   └─ 是 → 继续判断
    │
    ├─ 新页面的崩溃不应影响主应用?
    │   ├─ 否 → 使用 Router 在现有 Ability 中导航
    │   └─ 是 → 继续判断
    │
    ├─ 新页面需要完全独立的生命周期?
    │   ├─ 否 → 使用 Router 在现有 Ability 中导航
    │   └─ 是 → 创建新的 UIAbility
    │
    └─ 默认选择:Router + 单 Ability

十、应用状态保存

状态保存与恢复是移动应用的核心课题。用户在使用 NearPlay 时可能因为接电话、切换应用、甚至系统杀后台等操作中断游戏,应用需要在恢复时尽可能还原用户的状态。

10.1 当前状态管理方案

NearPlay 当前使用 @State 装饰器管理页面级状态:

@Page
@Component
struct WerewolfGame {
  @State currentPhase: string = 'night';     // 当前游戏阶段
  @State nightCount: number = 1;             // 夜晚轮次
  @State alivePlayers: Player[] = [];        // 存活玩家列表
  @State timerValue: number = 60;            // 倒计时
  // ...
}

@State 的特点是:状态变更触发 UI 重新渲染,状态生命周期与组件实例绑定。当页面因 Router.back() 或 Ability 销毁而消失时,@State 数据随之丢失。当应用切到后台再回来(Ability 未销毁),@State 数据保留。

@State 的局限性

  • 不支持跨页面共享:Index 页面的 @State 数据无法直接在 GameRoom 页面访问
  • 不支持持久化:Ability 销毁后 @State 数据全部丢失
  • 不支持进程恢复:应用被系统杀掉后重新启动,@State 无法恢复

10.2 AppStorage:应用级状态共享

AppStorage 是 HarmonyOS 提供的应用级状态存储,所有页面都可以读写同一份数据:

// 在 EntryAbility.onCreate 中初始化
AppStorage.setOrCreate('currentUser', defaultUser);
AppStorage.setOrCreate('isGameRunning', false);
AppStorage.setOrCreate('currentRoomId', '');

// 在任意页面中访问
@Component
struct SomeComponent {
  @StorageLink('currentUser') user: UserInfo = defaultUser;
  @StorageProp('isGameRunning') isGameRunning: boolean = false;
}

@StorageLink 和 @StorageProp 的区别:

  • @StorageLink:双向绑定,页面修改直接同步到 AppStorage
  • @StorageProp:单向绑定,页面只能读取 AppStorage 的值,修改不会同步回去

对于 NearPlay,AppStorage 适合存储:

  • 当前登录用户信息(所有页面都可能需要显示用户头像/昵称)
  • 全局游戏状态标志(是否有进行中的游戏)
  • 应用级配置(音效开关、通知开关)

10.3 PersistentStorage:持久化状态

PersistentStorage 在 AppStorage 的基础上增加了持久化能力,数据会写入设备存储,应用重启后自动恢复:

// 持久化关键配置
PersistentStorage.persistProp('soundEnabled', true);
PersistentStorage.persistProp('notificationEnabled', true);
PersistentStorage.persistProp('themeMode', 'light');

// 在页面中使用
@Component
struct SettingsPage {
  @StorageLink('soundEnabled') soundEnabled: boolean = true;
  @StorageLink('notificationEnabled') notificationEnabled: boolean = true;
}

PersistentStorage 适合存储:

  • 用户偏好设置(音效、通知、主题等)
  • 上次使用的游戏类型(方便首页推荐)
  • 用户引导完成标志

注意事项:PersistentStorage 不适合存储大量数据或敏感数据。大量数据应使用关系型数据库(@kit.ArkData 的 relationalStore),敏感数据应使用安全存储。此外,PersistentStorage 的写入操作是异步的,在 Ability 销毁前未完成的写入可能丢失,因此关键数据的持久化应在数据变更时立即触发,而非依赖 onDestroy 回调。

LocalStorage 也是值得提及的状态管理方案。它是 Ability 级别的状态共享机制,作用域限于同一个 UIAbility 内的多个页面。与 AppStorage 的区别在于:AppStorage 是应用级的,跨 Ability 共享;LocalStorage 是 Ability 级的,仅在当前 Ability 的页面树中有效。对于 NearPlay 当前的单 Ability 架构,LocalStorage 和 AppStorage 的效果等价,但 AppStorage 在未来多 Ability 扩展时更具前瞻性。

10.4 页面状态恢复策略

当应用从后台恢复或重新启动时,页面状态恢复是保障用户体验的关键。以下是 NearPlay 各场景的状态恢复策略:

┌─────────────────────────────────────────────────────┐
│                  状态恢复场景矩阵                       │
├──────────────┬────────────────┬────────────────────────┤
│    场景       │  @State 状态   │  恢复策略               │
├──────────────┼────────────────┼───────────────────────┤
│ 前后台切换    │  保留(Ability │  无需恢复,数据仍在     │
│ (热启动)     │  未销毁)      │                        │
├──────────────┼────────────────┼────────────────────────┤
│ 温启动       │  丢失(Ability │  aboutToAppear 中       │
│ (Ability重建) │  重建)        │  从服务端重新拉取       │
├──────────────┼────────────────┼───────────────────────┤
│ 冷启动       │  丢失(进程重建)│ aboutToAppear 中       │
│ (进程重建)    │               │  从服务端重新拉取       │
│              │               │  + PersistentStorage    │
│              │               │  恢复用户偏好            │
├──────────────┼────────────────┼────────────────────────┤
│ 页面返回      │  上层页面保留  │  onPageShow 中          │
│ (Router.back) │  下层页面销毁  │  刷新可能过期的数据     │
└──────────────┴────────────────┴────────────────────────┘

游戏进度恢复的特殊考量
对于进行中的游戏,状态恢复更为复杂。推荐方案:

  1. 实时保存:游戏关键状态(轮次、角色、投票结果)每次变更时同步到服务端
  2. 本地缓存:使用 Preferences 缓存最近的 GameState,支持离线恢复
  3. 重连机制:onForeground 时检测是否有未完成的游戏,提示用户是否继续
  4. 超时处理:后台超过一定时间后,服务端自动结束游戏,前端恢复时显示结算页面

10.5 当前方案的不足与改进建议

NearPlay 当前未使用 AppStorage 和 PersistentStorage,所有状态都通过 @State 和 Router 参数传递。这导致:

  1. 数据冗余:同一份用户信息在多个页面的 @State 中分别存储
  2. 恢复能力弱:Ability 销毁后所有状态丢失,用户需要重新操作
  3. 配置丢失:用户的音效偏好等每次启动都需要重新设置

建议的改进路径:

  • 第一阶段:引入 PersistentStorage 存储用户偏好,确保配置跨会话保留
  • 第二阶段:引入 AppStorage 存储全局共享数据(用户信息、游戏状态),减少页面间参数传递
  • 第三阶段:实现服务端状态同步,支持跨设备游戏恢复

10.6 状态管理技术选型对照

┌─────────────────────────────────────────────────────┐
│              状态管理方案对照表                         │
├────────────┬──────┬──────┬───────┬──────┬──────────────┤
│   方案      │ 跨页面│ 持久化│ 跨Ability│ 复杂度│ 适用场景     │
├────────────┼──────┼──────┼───────┼──────┼──────────────┤
│ @State     │  ✗   │  ✗   │  ✗    │  低  │ 页面内状态   │
│ @Link      │  ✗   │  ✗   │  ✗    │  低  │ 父子组件同步 │
│ AppStorage │  ✓   │  ✗   │  ✓    │  中  │ 应用级共享   │
│ Persistent │  ✓   │  ✓   │  ✓    │  中  │ 用户偏好     │
│ LocalStorage│ 同Ability内│ ✗ │  ✗    │  中  │ Ability内共享│
│ Preferences│  ✓   │  ✓   │  ✓    │  中  │ 少量键值数据 │
│ RDB        │  ✓   │  ✓   │  ✓    │  高  │ 结构化数据   │
│ 服务端     │  ✓   │  ✓   │  ✓    │  高  │ 核心业务数据 │
└────────────┴──────┴──────┴───────┴──────┴──────────────┘

附录:NearPlay 完整生命周期流程图

╔══════════════════════════════════════════════════════════════════╗
║                    NearPlay 完整生命周期全景图                    ║
╚══════════════════════════════════════════════════════════════════╝

    [桌面图标点击]
         │
         │  Want: action=ohos.want.action.home, entity=entity.system.home
         │  BMS skills 匹配 → EntryAbility
         ▼
  ┌──────────────┐
  │   onCreate    │ ◄── 设置 ColorMode.COLOR_MODE_NOT_SET
  └──────┬───────┘
         │
         ▼
  ┌──────────────────────┐
  │ onWindowStageCreate  │ ◄── windowStage.loadContent('pages/Index')
  └──────┬───────────────┘     │
         │                     │ 页面管线: 解析路径 → 创建组件 → build() → 渲染
         │                     ▼
         │              ┌──────────────┐
         │              │ Index 页面    │
         │              │ aboutToAppear│ ◄── 加载活动列表、检查用户状态
         │              │ onPageShow   │
         │              └──────┬───────┘
         │                     │
         ▼                     │
  ┌──────────────┐             │
  │  onForeground │ ◄──────────┘  Ability 进入前台,用户可见
  └──────┬───────┘
         │
    ┌────┴───────────────────────────────────────────┐
    │                    用户交互阶段                    │
    │                                                  │
    │  Router.push('GameRoom')                         │
    │       │                                          │
    │       ├─ GameRoom.aboutToAppear() 加载房间数据    │
    │       ├─ GameRoom.onPageShow()                   │
    │       └─ Index.onPageHide()                      │
    │                                                  │
    │  Router.push('game/WerewolfGame')                │
    │       │                                          │
    │       ├─ WerewolfGame.aboutToAppear() 启动游戏   │
    │       │   └─ setInterval(倒计时)                 │
    │       ├─ WerewolfGame.onPageShow()               │
    │       └─ GameRoom.onPageHide()                   │
    │                                                  │
    │  Router.back() ◄── 返回游戏大厅                  │
    │       │                                          │
    │       ├─ WerewolfGame.aboutToDisappear() ★清理定时器│
    │       ├─ GameRoom.onPageShow()                   │
    │       └─ EntryAbility.onDestroy() ✗ 不触发!      │
    │                                                  │
    └────┬───────────────────────────────────────────┘
         │
    ┌────┴────┐
    │ Home 键  │ ◄── 用户退到后台
    └────┬────┘
         │
         ▼
  ┌──────────────┐
  │  onBackground │ ◄── 应释放/暂停:游戏定时器、音频、动画
  └──────┬───────┘     当前仅输出日志(需改进)
         │
    ┌────┴────┐
    │ 切回应用 │ ◄── 用户从最近任务返回
    └────┬────┘
         │
         ▼
  ┌──────────────┐
  │  onForeground │ ◄── 恢复:游戏定时器、音频、动画
  └──────┬───────┘     当前仅输出日志(需改进)
         │
    ┌────┴────────────┐
    │ 用户关闭/系统回收 │
    └────┬────────────┘
         │
         ▼
  ┌──────────────────────┐
  │ onWindowStageDestroy │ ◄── 窗口销毁,释放UI资源
  └──────┬───────────────┘
         │
         ▼
  ┌──────────────┐
  │   onDestroy   │ ◄── Ability销毁,最终清理
  └──────────────┘     页面 aboutToDisappear() 被调用

文档版本:v1.0
适用范围:NearPlay HarmonyOS 应用
关键结论:当前 EntryAbility 采用最小化实现策略,生命周期回调以日志输出为主。游戏页面的资源清理依赖 aboutToDisappear 而非 Ability 级回调。Router.back() 不触发 onDestroy 是理解页面与 Ability 生命周期关系的关键点。未来多 Ability 扩展和 AppStorage/PersistentStorage 状态管理是重要改进方向。

十一、onBackground与onForeground的资源管理

11.1 当前实现的不足

当前onBackground()和onForeground()只输出日志,没有实际的资源暂停和恢复逻辑。这意味着当用户按下Home键将应用切到后台时,游戏计时器仍然在运行、语音识别引擎仍然在监听、WebSocket连接仍然在维持。这些后台活动不仅浪费电量和网络资源,还可能导致意外行为——用户已经不看屏幕了,但语音识别仍在捕获环境声音并自动提交猜测。

11.2 理想的资源管理策略

onBackground()应执行以下暂停操作:暂停所有游戏计时器(setInterval/setTimeout)、停止语音识别引擎(VoiceInputHelper.stopListening())、降低WebSocket心跳频率、暂停Canvas动画。onForeground()应执行对应的恢复操作:恢复游戏计时器、重启语音识别(如果canSpeak=true)、恢复正常心跳频率、重绘Canvas。这种"切后台暂停、切前台恢复"的策略是移动应用的标准做法。

11.3 游戏中断恢复的用户体验考量

当用户在游戏进行中切到后台(如接电话、回微信),再切回来时,游戏状态应该无缝恢复。最理想的体验是:计时器暂停而非继续运行——用户接了三十秒电话回来,倒计时还是接电话前的数字,而不是少了三十秒。这需要在onBackground中记录暂停时刻的计时器剩余值,在onForeground中恢复。当前实现没有这种暂停恢复机制,切后台再回来可能导致倒计时归零、游戏阶段自动切换等意外行为。

Logo

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

更多推荐