Stage 模型、UIAbility 与 ExtensionAbility 到底怎么分工?
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 负责页面层级,这是更推荐的分工。
更多推荐

所有评论(0)