1. 为什么先学 Stage 模型

如果说 ArkUI 决定了页面怎么写,那么 Stage 模型决定了一个鸿蒙应用怎么被系统识别、调度和拉起。很多初学者把 Ability 理解成“页面”,这个说法只对了一半:UIAbility 确实经常承载页面入口,但它更重要的身份是应用在前台与系统交互的生命周期单元。系统不是直接运行某个 ArkTS 文件,而是根据应用包、模块配置、Ability 声明和 Want 信息来找到合适的入口。

2. Stage 模型解决的核心问题

Stage 模型相比旧模型最大的变化,是把应用生命周期、窗口阶段、Ability 入口和 Extension 扩展能力拆得更清楚。开发者不再把所有事情都塞进一个前台页面里,而是根据场景选择 UIAbility、ServiceExtensionAbility、FormExtensionAbility 等不同能力。这样做的目标不是增加概念,而是让系统能更有序地管理进程、资源、后台行为和跨应用协作。

图 1  Stage 模型中的应用、模块与能力关系

3. 模块化组织:entry、feature 与 shared

在真实项目里,Stage 模型通常会和模块化一起出现。entry 模块提供主入口和基础页面,feature 模块承载可拆分业务,shared 模块沉淀公共代码和资源。模块之间不是简单地按文件夹区分,而是影响编译、分发、依赖和启动路径。对于一个新闻、商城或设备控制类应用来说,把首页、详情、账号、后台服务、卡片能力放在同一个大入口里,后期会非常难维护;而 Stage 模型鼓励你从一开始就把“用户看见的入口”和“系统调度的能力”分开设计。

4. UIAbility 生命周期怎么落地

UIAbility 生命周期里最容易混淆的是 onCreate 和 onWindowStageCreate。onCreate 适合做轻量初始化,例如读取启动参数、准备必要对象、注册少量全局监听;onWindowStageCreate 才是窗口相关逻辑真正开始的地方,通常在这里加载页面。把页面加载写错阶段,短期可能能跑,长期会导致窗口资源、页面栈和生命周期边界混乱。进入前台后,应用可以恢复动画、传感器、定位或订阅;退到后台时,则应暂停高频刷新、保存临时状态、减少无意义计算。

图 2  UIAbility 生命周期关键节点

5. ExtensionAbility 的职责边界

ExtensionAbility 的价值在于把特殊场景变成受控扩展。比如服务扩展适合封装后台能力和跨进程接口;卡片扩展适合给桌面提供轻量信息;输入法、分享、备份、无障碍等能力也都有自己的扩展入口。系统通过这些扩展类型知道你的应用想做什么,再按权限、生命周期和调度规则决定能不能做、何时做、做到什么程度。这个设计能减少应用随意拉起后台进程,也让不同应用之间的协作更可控。

6. 页面流转:Ability 与 Navigation 的分工

页面跳转也要分层理解。跨业务入口、跨任务栈的跳转通常和 Ability 相关;同一个 Ability 内的页面流转,更适合使用 Navigation、NavDestination 等页面导航机制。把每个页面都拆成一个 UIAbility,会让任务栈复杂、启动成本增加、回退行为难以预测;反过来,把所有入口都塞进一个页面栈,也不利于系统调度和外部拉起。比较稳妥的策略是:少量 UIAbility 承担入口边界,大部分业务页面放在 Ability 内部导航。

图 3  常见 ExtensionAbility 场景

7. Want 参数与跨入口拉起

Want 是系统寻找目标能力的重要载体。它可以包含 bundleName、abilityName、moduleName、parameters 等信息。对于应用内跳转,Want 帮你定位目标入口;对于跨应用拉起,Want 还涉及隐式匹配、权限和安全边界。开发时不要把 Want 当成普通参数 Map 随便传,尤其是涉及用户隐私、账号态、文件路径时,要确认目标方是否可信、参数是否必要、是否存在越权访问风险。

图 4  Ability 任务栈与页面内导航

8. 生命周期不要堆业务

从工程实践看,Stage 模型最怕“生命周期里堆业务”。有些项目会在 onCreate 里做网络请求、数据库迁移、图片预加载、远端服务连接,最后启动变慢、失败路径混乱。更好的方式是把生命周期回调当成调度点:它只决定何时启动、暂停、释放某类任务,具体业务逻辑放进独立服务类或状态管理层。这样页面、服务、卡片都可以复用同一套业务能力,而不是每个入口写一份。

9. 后台能力要遵守系统边界

还有一个容易被忽视的点是后台能力边界。HarmonyOS 对后台行为有明确约束,不是页面退到后台后仍然可以无限运行。需要持续播放、位置、下载、同步等场景时,要查看对应后台任务或扩展能力要求,按系统规范申请和实现。否则在测试机上偶尔可用,上线后就可能遇到后台被挂起、服务被回收、通知不合规等问题。

10. 团队协作的三张清单

在团队协作中,可以把 Stage 模型设计拆成三张清单。第一张是入口清单,列出哪些能力会被桌面、通知、卡片、其他应用或系统拉起;第二张是生命周期清单,列出每个入口在创建、前台、后台、销毁阶段要启动和停止哪些资源;第三张是依赖清单,列出 entry、feature、shared 之间的依赖方向。只要这三张清单清楚,后续新增功能时就不容易把代码塞错位置。

11. 登录态、进程恢复与冷启动

如果应用包含登录态,还要特别注意 Ability 重建和进程恢复。系统可能在资源紧张时回收应用进程,用户再次进入时并不一定沿着你想象中的完整启动路径恢复。重要状态不应该只放在内存里,必要时要通过首选项、数据库、缓存文件或服务端重新拉取恢复。UIAbility 收到 Want 后也要能处理冷启动和热启动两种情况,不能默认所有管理器都已经初始化完成。

12. 测试入口不能只测桌面图标

测试 Stage 模型时,不要只测点击桌面图标进入。还应覆盖从通知进入、从卡片进入、从分享面板进入、从外部 Want 拉起、旋转屏幕、切到后台再返回、清理后台后重新进入等路径。很多生命周期问题只会在这些路径里暴露,例如页面重复加载、订阅没有注销、后台回来后数据不刷新、重复创建窗口资源等。把这些路径列入自测用例,比上线后凭日志猜问题可靠得多。

13. 项目复盘中的坏味道

做项目复盘时,可以重点检查三个典型坏味道。第一,某个 Ability 文件越来越长,既管页面加载,又管账号、网络、定位、数据库,说明边界已经失控;第二,多个 Extension 重复初始化同一套业务对象,说明公共能力没有下沉;第三,页面跳转链路里到处拼 Want 参数,说明路由和参数协议没有统一封装。发现这些问题时,不要急着重写全部代码,先抽出启动管理、路由管理和业务服务三层,逐步把职责挪回正确位置。

14. 本文小结

总结一下,Stage 模型不是一组生命周期函数的集合,而是一套应用组织方式。它回答三个问题:应用有哪些入口,系统如何调度这些入口,不同场景的能力应放在哪种扩展里。写鸿蒙应用时,先把模块、Ability、Extension、Navigation 和 Want 的边界画清楚,再写页面和业务代码,后期维护会轻松很多。对于新人来说,先学会画入口关系图,再写实现代码,是理解鸿蒙工程结构最快的方法。

15. Stage 模型实践对比表

设计点

不推荐写法

推荐写法

生命周期

在 onCreate 堆网络、数据库和页面逻辑

生命周期只做调度,耗时逻辑下沉到服务类

页面组织

每个页面都做成 UIAbility

入口用 UIAbility,内部页面用 Navigation 管理

后台能力

页面退后台后继续随意运行

按场景选择 Extension 或后台任务能力

模块划分

所有业务都放 entry 模块

entry 承担入口,feature/shared 承担业务和复用

16. 案例代码:轻量 UIAbility 入口

export default class EntryAbility extends UIAbility {

  onCreate(want: Want): void {

    // 只做轻量初始化和启动参数解析

  }

  onWindowStageCreate(windowStage: window.WindowStage): void {

    windowStage.loadContent('pages/Index');

  }

  onBackground(): void {

    // 暂停高频刷新、保存临时状态

  }

}

这段代码体现的是“生命周期只做调度”的思路:onCreate 不直接加载页面,也不做耗时业务;onWindowStageCreate 负责窗口内容加载;onBackground 负责暂停或保存状态。真实项目里可以把账号恢复、埋点初始化、数据预热等逻辑封装到单独的 AppStartupManager 中,再由生命周期回调触发。

17. 案例代码:使用 Want 打开指定业务入口

let want: Want = {

  bundleName: 'com.example.shopping',

  abilityName: 'EntryAbility',

  parameters: {

    targetPage: 'OrderDetail',

    orderId: '202606010001'

  }

};

this.context.startAbility(want).then(() => {

  console.info('start ability success');

}).catch((err: BusinessError) => {

  console.error(`start ability failed: ${err.code}, ${err.message}`);

});

Want 适合描述“我要打开哪个能力,并携带哪些启动参数”。如果是应用内部页面跳转,可以在 EntryAbility 接收参数后交给路由层处理;如果是跨应用拉起,要特别关注目标 Ability 是否导出、参数是否包含敏感数据、失败时是否有兜底页面。

18. 案例代码:ServiceExtensionAbility 暴露后台能力

export default class SyncService extends ServiceExtensionAbility {

  onCreate(): void {

    console.info('sync service created');

  }

  onConnect(want: Want): rpc.RemoteObject {

    // 返回远端对象,供 UIAbility 通过 RPC 调用后台能力

    return new SyncRemoteObject('com.example.SyncService');

  }

  onDisconnect(want: Want): void {

    console.info('sync service disconnected');

  }

}

ServiceExtensionAbility 更适合封装“页面不应该直接做”的能力,例如设备连接、后台同步、长任务调度或跨进程接口。它不是万能后台常驻方案,仍然要遵守系统生命周期和权限规则。页面只负责发起连接和展示状态,服务负责业务能力。

19. 案例代码:Ability 内部用 Navigation 管理页面流

@Entry

@Component

struct MainPage {

  private pathStack: NavPathStack = new NavPathStack();

  build() {

    Navigation(this.pathStack) {

      Column() {

        Button('查看订单详情')

          .onClick(() => {

            this.pathStack.pushPath({ name: 'OrderDetail', param: '202606010001' });

          })

      }

    }.navDestination(this.pageMap)

  }

}

如果只是从首页进入详情页、编辑页、结果页,通常不需要再启动一个新的 UIAbility。Navigation 可以把页面流控制在同一个 Ability 内,回退行为更清晰,启动成本也更低。UIAbility 负责入口边界,Navigation 负责页面层级,这是更推荐的分工。

Logo

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

更多推荐