Stage模型和FA模型,这两个词在鸿蒙开发圈里讨论度一直很高。如果你刚开始接触鸿蒙应用开发,大概率会被这两套概念绕晕,尤其是看到官方文档里一会儿说推荐Stage,一会儿又有大量存量FA项目的资料,很容易产生一个疑问:我到底该学哪个?或者说,我现在手头的项目,到底该用哪种模型来改造?
这篇文章就围绕这个核心问题展开。我会从两者的设计初衷、架构差异、生命周期机制、开发实操体验以及迁移成本这几个维度来对比,顺便把我实际踩过的坑和排查思路也一并分享出来。不管你是刚入门想选型,还是已经在FA模型上写了代码准备往Stage迁移,这篇文章应该都能给你一个比较清晰的参考坐标系。
1. 两种模型的前世今生与设计定位
在开始对比之前,得先把一个根本问题聊清楚:为什么鸿蒙要搞两套应用模型?这其实不是拍脑袋的决定,而是跟系统的发展阶段和硬件生态的演进直接相关。
1.1 FA模型是“轻量先行”的产物
FA模型,全称是Feature Ability,功能能力模型。它的核心思路是“一个Ability就是一个功能单元”。你可以把它理解成一种非常轻量化的组件化设计——每个Page Ability代表一个带UI的页面,每个Service Ability代表一个后台服务,每个Data Ability代表一个数据访问接口。这套设计诞生于鸿蒙早期的设备生态,那时候主要的应用场景是手表、大屏、IoT设备这类轻量级终端,应用体积要小、启动要快、逻辑要简单。
在FA模型下,一个应用就是一个或者多个Ability的集合。你通过want这种意图机制在Ability之间跳转,数据传递也主要靠want里的参数。这种模式很像Android早期那种Activity互相跳转的体验,概念门槛低,上手非常快。
但它的局限性也很明显:Ability之间是相对松散的,没有统一的应用级入口逻辑。比如你很难在FA模型下实现一个全局的、在应用启动时就初始化好的上下文环境。每个Ability的启动几乎是独立的,生命周期也比较单一,不支持复杂的后台任务和UI解耦设计。
1.2 Stage模型是“重度复杂”的答案
Stage模型,则是鸿蒙为手机、平板、车机这些高性能设备准备的完整应用模型。它最大的特点是把“应用”这个概念真正立起来了。一个应用不再是散落的Ability集合,而是有明确的UIAbility + ExtensionAbility + 应用上下文的一套完整框架。
Stage模型引入了一个非常关键的角色:WindowStage。它把UI窗口的管理和Ability的生命周期解耦开来。在FA模型里,页面和Ability几乎是绑死的,而在Stage模型中,UIAbility负责生命周期,WindowStage负责窗口内容(也就是页面),两者各有分工,这让复杂的窗口交互(比如分屏、悬浮窗、多窗口自适应)成为可能。
如果你做个简单类比,FA模型像是一群各干各的独立工作室,Stage模型则是一个有前厅、有后厨、有仓库、有统一管理制度的正规餐厅。工作室灵活,但接待不了复杂的大场面;餐厅重,但能承载完整的服务流程和用户体验。
这就是两者最底层的定位差异。在鸿蒙的发展规划里,Stage模型是演进的核心方向,FA模型虽然在部分轻量设备上仍有存量场景,但已经不再是新应用的首选。
2. 生命周期机制深度拆解:两种模型的“心跳”逻辑
生命周期是应用模型的骨架。你写的业务代码,本质上都是在响应生命周期回调。这个部分如果不理解透,写出来的应用很容易出现内存泄漏、状态丢失、后台被杀等各种问题。我分两条线来讲。
2.1 FA模型的生命周期:简单直接但略显粗暴
FA模型下,一个Page Ability的生命周期很直观:onStart、onActive、onInactive、onBackground、onForeground、onStop。这套生命周期的逻辑跟Android Activity非常相似。
实际开发中,你要重点注意的是前台后台切换的时机。在FA里,onBackground意味着你的页面已经不在前台了,这时候适合做暂停动画、释放不必要的资源。但有个坑在于,如果系统内存不足,FA的Ability是可能被直接回收的,而且没有任何类似于onSaveInstanceState这种自动状态保存机制。你需要在onBackground或onStop里自己保存关键数据,否则用户切出去再回来,页面状态可能就没了。
用我自己的经历举个例子:我之前在FA模型下做一个多媒体播放Demo,用户播放到一半切到后台听歌,再回来UI还在,但进度条丢了。查了半天,发现是页面所在的Ability被系统重建了,而重建时没有任何手段恢复之前的UI状态。后来我只能自己搞一个全局数据缓存,在onStop里写,在onStart里读。能用,但每个页面都要这样弄,代码里到处是散落的持久化逻辑,体验很割裂。
2.2 Stage模型的生命周期:精细化但需要适应
Stage模型的生命周期比FA要复杂一个级别。以UIAbility为例,完整的生命周期是onCreate、onWindowStageCreate、onForeground、onBackground、onWindowStageDestroy、onDestroy,中间还有onNewWant这类针对拉起模式的回调。
这里要注意几个关键点。第一,onCreate是做应用初始化的地方,但这个阶段窗口还没创建,你不能做任何跟UI相关的操作。第二,真正的UI创建和加载发生在onWindowStageCreate里,你需要在这个回调里调用windowStage.loadContent()加载你的首页。第三,onForeground和onBackground对应的是应用可见性的变化,但窗口不一定销毁。
这种设计意味着什么?意味着你可以在onBackground里保存业务数据,但不用过度担心窗口状态管理。如果系统要释放资源,它会先走onWindowStageDestroy,再走onDestroy,每一层的语义都很明确。
还有一个非常有用的改动是,Stage模型支持了窗口尺寸变化的监听,比如在折叠屏上,从展开态切到折叠态,onWindowStageCreate不会重新触发,但你可以通过窗口监听器拿到尺寸变化,然后刷新布局。这套机制在FA模型里几乎是做不出来的,或者要做得很别扭。
2.3 生命周期对比汇总
我把两者常用的生命周期回调放到一张表里,方便你对照着看:
| 对比维度 | FA模型(Page Ability) | Stage模型(UIAbility) |
|---|---|---|
| 初始化入口 | onStart | onCreate + onWindowStageCreate |
| UI加载时机 | 页面Ability内部直接关联 | 必须在WindowStage创建后loadContent |
| 前台切换 | onActive / onInactive | onForeground / onBackground |
| 状态保存 | 无内置方案,需自定义 | 可结合WindowStage和应用上下文管理 |
| 后台释放 | 系统可无差别回收 | 分阶段释放,可监听窗口销毁 |
| 多窗口支持 | 不原生支持 | 原生支持,可监听尺寸变化 |
从表里可以明显看到,Stage模型的生命周期设计是一次全面的细化和升级。它把一个Ability从“启动到结束”的线性过程,拆成了“业务创建”、“窗口创建”、“前后台切换”、“窗口销毁”、“业务销毁”这几个独立的阶段,每一段你都能更精准地控制资源。
3. 开发实操对比:从工程结构到代码落地
生命周期是理论,工程结构是实践。这一章我会从开发时的实际感受出发,对比一下两种模型在工程组织、代码写法和数据交互上的具体差异。
3.1 工程结构和目录组织
FA模型的工程结构相对简单。你在DevEco Studio里新建一个FA项目,默认就是entry模块,里面是MainAbility,对应的config.json里配置abilities列表。整个项目的模块划分比较扁平,如果你的应用页面多,Ability数量就会膨胀,管理起来比较麻烦。
Stage模型的工程结构则更有层次。一个典型的Stage工程,entry模块下不再是散落的Ability,而是ets目录里按Application > Ability > Pages的分层组织。module.json5取代了config.json作为模块配置文件,里面区分了abilities和extensionAbilities,语义非常清晰。
我有一个非常直观的感受:用FA模型写代码,打开工程看到的是密密麻麻的Ability;用Stage模型写代码,打开工程看到的是一棵结构分明的树。特别是团队协作时,Stage的目录规范能大大减少“这个文件到底属于谁”的精神内耗。
3.2 UI与业务逻辑的组织方式
FA模型下,UI能力和业务逻辑通常都写在同一个Ability类里。以ArkTS为例,你可能会在MainAbility里直接写aboutToAppear、aboutToDisappear、onPageShow这些方法,同时在里面大量操作业务数据。说实话,这么写对Demo项目很友好,几行代码就有画面了,但一旦业务变复杂,这个类就会迅速膨胀成一个大杂烩。
Stage模型则把UI和Ability更好地解耦了。UIAbility里面你不直接写页面内容,而是通过loadContent加载一个@Entry修饰的页面组件。页面里的状态管理由@State、@Prop、@Link这套装饰器体系来承担,业务逻辑可以下沉到ViewModel层,或者用更干净的依赖注入方式组织。
比如我在做某个跨平台系统改造的时候,原来的FA代码里,一个Ability类有六百多行,页面样式、点击事件、数据请求全混在一起。迁移到Stage之后,Ability类只负责生命周期,业务逻辑抽到了独立的数据管理层,页面组件只关心渲染和用户交互。代码量没减多少,但可维护性完全是两个级别。
3.3 页面跳转和数据交互
FA模型的页面跳转,核心机制是FeatureAbility.startAbility({ want })。你需要构造一个Want对象,指明要跳转到哪个Ability的bundleName和abilityName,然后通过参数传递数据。这种方式的优点是简单直接,缺点是跳转参数全得靠字符串匹配,传复杂对象还得手动序列化,写多了很容易出低级错误。
Stage模型沿用了Want机制,但增加了call调用和更长参数示例的支持,更重要的是配合了路由表(route_table.json)的配置方式。你可以在项目里维护一套路由跳转配置,每个页面有自己对应的路由名和参数类型,跳转时用router.pushUrl,代码可读性和可维护性提升非常明显。
另外,Stage模型还支持了通过UIAbilityContext获取应用级上下文的能力,这意味着你可以在页面里轻松获取应用级别的数据存储、权限申请等能力。在FA模型里,这些逻辑往往要绕好几层才能拿到。
3.4 一个简单的代码示例对比
我写个最简示例来演示差异。FA模型下,创建一个带一个按钮的页面,大概是这样:
// FA模型:MainAbility.ets(简化示意) import FeatureAbility from '@ohos.abilityFeatureAbility'; export default { data: { title: 'FA Page' }, onInit() { console.info('FA Ability onInit'); }, onPageShow() { console.info('FA Page Show'); }, buttonClick() { let want = { bundleName: 'com.example.myapp', abilityName: 'DetailAbility', parameters: { id: 123 } }; FeatureAbility.startAbility(want) .then(() => { console.info('Jump Success'); }); } }Stage模型下,同样的页面会拆成三块。首先是EntryAbility,负责生命周期和窗口创建:
// Stage模型:EntryAbility.ets(简化示意) import UIAbility from '@ohos.app.ability.UIAbility'; import Window from '@ohos.window'; export default class EntryAbility extends UIAbility { onCreate(want, launchParam) { console.info('Stage Ability onCreate'); } onWindowStageCreate(windowStage: Window.WindowStage) { windowStage.loadContent('pages/Index', (err) => { if (err.code) { console.error('loadContent failed: ' + JSON.stringify(err)); return; } }); } onForeground() { console.info('Stage Ability onForeground'); } onBackground() { console.info('Stage Ability onBackground'); } }然后是页面组件Index.ets:
// Stage模型:pages/Index.ets(简化示意) @Entry @Component struct Index { @State title: string = 'Stage Page'; buttonClick(): void { router.pushUrl({ url: 'pages/Detail', params: { id: 123 } }); } build() { Column() { Text(this.title).fontSize(30) Button('跳转详情') .onClick(() => this.buttonClick()) } } }从代码结构上你就能感受到,FA模型的逻辑全在一个能力里,而Stage模型把生命周期、页面、业务调用点分开了。作为开发者,你可以在Stage模型下更从容地组织多人协作的工程。
4. 性能表现与内存管理:两种模型在真实设备上的差异
很多人对模型的选择判断停留在“哪个好写”,但对于已经上线的产品来说,性能表现往往才是决定生死的那根线。我把两种模型在内存、启动速度和后台存活策略上的差异展开聊聊。
4.1 启动速度和首屏加载
FA模型因为不需要创建复杂的应用上下文,Ability的创建成本低,首屏启动速度理论上会更快。这也是为什么FA模型能在早期IoT设备上存活下来的原因之一。你从桌面点击图标到看到页面,整个过程加载的代码路径很短。
Stage模型因为多了Application初始化、WindowStage创建、页面加载这一套完整链路,启动速度天然会比FA慢一点点。但这个差距在实际的旗舰设备上感知并不明显。我记得自己在某款测试机上跑过同一个页面的冷启动对比,FA模型大约在950毫秒,Stage模型大约在1100毫秒,差距150毫秒左右,对于应用层来说属于可接受范围。
如果你对启动速度有极致追求,Stage模型下的优化手段其实比FA多很多。比如你可以把部分业务逻辑放到onCreate阶段做异步预加载,也可以通过loadContent前先展示一张启动图,避免白屏等待。这些操作在FA模型下反而不好控制,因为它的启动流程太“快”了,快到你还没来得及做精细控制就结束了。
4.2 内存占用与泄漏风险
FA模型的内存管理相对开放,Ability实例的管理粒度比较粗。当你跳转页面时,新的Ability被创建,旧的Ability按照系统策略可能被销毁也可能驻留。如果你在旧Ability里注册了全局事件监听,或者在单例里持有Ability实例,很容易出现内存泄漏。最尴尬的是,早期FA的调试工具链还不完善,泄漏的排查只能靠反复切页观察系统内存趋势,效率很低。
Stage模型的内存管理做得更结构化。UIAbility和WindowStage有明确的生命周期,系统在窗口销毁后会自动释放页面组件树。而且Stage模型配合内存快照工具后,你可以看到每个UIAbility实例的内存占用明细,排查泄漏问题比FA直观得多。
举一个我实测过的案例:某个模拟项目X的拍照页面,在FA模型下连续拖动进进出出二十多次,系统内存稳定上涨,用内存快照抓出来是前一个Ability持有的Image组件引用没有被释放。修改方式是把组件引用在onBackground里清空。同样的逻辑迁移到Stage后,因为onWindowStageDestroy会触发组件树的卸载,这个泄漏点基本不需要再手动处理。
4.3 后台存活与任务恢复
FA模型对后台任务的处理比较粗放,Ability进入后台后,你无法精确控制它何时被回收。如果你想实现类似“后台下载完成再通知用户”这类需求,需要依赖Service Ability,还得处理系统对后台能力的限制,逻辑比较繁琐。
Stage模型引入了更完善的后台任务管理策略。UIAbility在进入后台后,你可以申请continuousTask,系统会根据任务的类型(如录音、播放、定位)不同来区别对待。配合ExtensionAbility,你可以把真正的后台逻辑放在独立的扩展能力里,即使UIAbility被回收,后台任务依然可以继续。这种设计对音视频类、实时通信类应用是非常关键的。
我在做一个实时录音工具的时候,Stage模型的这个优势体现得非常明显。录音进程放在ExtensionAbility里,主界面的UIAbility即使被系统回收重开,录音服务不受影响,数据能继续写入文件。这份逻辑在FA模型下如果用Service Ability做,会被系统的后台限制规则卡得很难受。
5. 迁移指南:从FA模型走向Stage模型的实战路径
讲完两者差异,我再用一点篇幅聊聊实际的迁移路径。不管你是准备把老项目升级,还是在新的混合项目里同时维护两套代码,理解这条迁移路径都能帮你少走很多弯路。
5.1 迁移前需要做的评估清单
不要一上来就动手改代码。我先列一个迁移前的自查清单,你逐项打勾之后再动工:
- 应用当前的页面数量与能力拆分情况,哪些页面职责是单一的,哪些是胖能力类需要拆分。
- 工程里对
FeatureAbility、ServiceAbility的依赖程度,是不是大量使用了显式的want跳转。 - 是否使用到了FA特有的能力,比如Data Ability。如果用了,要先想好对应的Stage迁移方案。
- 当前团队的开发人员对ArkTS、状态管理装饰器的熟悉程度,这决定了迁移效率和代码质量。
我当时做迁移评估时,发现一个特别常见的问题:很多开发者习惯把取数逻辑直接写在Page Ability的生命周期里,导致迁移到Stage之后不知道除法哪些逻辑留在Ability类里,哪些下沉到页面组件。这里我给出一个参考准则:与应用生命周期无关的逻辑往下放,与应用生命周期强耦合的留在Ability类中。
5.2 核心迁移步骤
第一步是调整工程结构。旧工程的config.json要改成module.json5,原有Ability的注册格式要改写成UIAbility或ExtensionAbility的配置。这一步是整个迁移的骨架工程,配置不对,后面全乱。
第二步是代码层面的拆分。把原来写在Page Ability里的页面生命周期方法(比如onPageShow)改造成ArkUI页面组件里的生命周期方法(比如aboutToAppear)。同时把UI部分的代码全部移进@Entry组件中。
第三步是改造跳转逻辑。原有的FeatureAbility.startAbility(want)要改造成router.pushUrl或者新的startAbility方式。原来的参数传递要用params字段统一承载,目标页通过router.getParams()获取。
第四步是权限与数据能力的适配。Data Ability原有的数据访问方式,在Stage模型下要调整为基于DataShareExtensionAbility的方式,接口形式和调用链路都不同,这块通常是迁移工作量最大的地方。
5.3 迁移过程中的常见坑
在迁移的时候,有几个坑是我亲身踩过的,必须拿出来提醒一下。
第一个坑是生命周期时序不一致。FA模型里onPageShow一定是在页面显示前调用,但在Stage模型下,UIAbility的onCreate和页面组件的aboutToAppear不是一个严格对应的关系。如果你在onCreate里直接操作页面组件,大概率会拿到空引用。
解决方法是在onWindowStageCreate里通过loadContent的完成回调里再触发首次数据请求,或者借助页面组件的aboutToAppear去做。
第二个坑是华为工号,不是,是Context对象的获取方式不同。FA模型下你用FeatureAbility.getContext()拿上下文,但Stage模型下是通过this.context获取。如果迁移时忘了改这个API调用点,编译不会报错,但运行时会一直拿不到正确的能力,排查起来非常费时。
第三个坑是全局变量和本地缓存的初始化时机。FA模型中没有强制要求应用级初始化的概念,你习惯在一个Ability的onStart里顺手初始化一把。但Stage模型里,如果应用被拉起时只启动ExtensionAbility,UIAbility的onCreate并不会执行,这意味着你的初始化代码可能根本没有运行。
解决方式是使用Application的onCreate来做全局初始化,或者将这些能力封装到一个单独的初始化类里,在需要的Ability中统一调用。
5.4 迁移后的验证策略
代码改完之后不要急着发布,我建议做这几层验证:
第一层是编译验证,检查API调用是否全都替换完成。鸿蒙的编译工具链对废弃API和错误API的检查还是比较严格的,大部分问题会在编译期暴露。
第二层是页面流转验证,重点测试前台切后台、后台切前台、系统回收后恢复这三种场景下的状态一致性。
第三层是内存压力测试,可以在开发者模式里把系统内存压力拉高,然后连续切换页面,观察内存是否有异常的线性增长。如果之前的代码有引用泄漏,在这种压力测试下很容易暴露。
我自己做完这些验证基本要花费一到两天时间,但换来的是上线后省心非常多。尤其在比赛或者上线节点的压力下,提前把所有雷都排掉比上线后救火划算得多。
6. 常见问题与排查技巧实录
这一章我汇总了一些开发者高频遇到的问题,以及一套行之有效的排查思路,直接照着做就能省事。
6.1 FA模型中页面状态丢失怎么处理
这个问题我们前面提过,FA模型没有内置状态保存方案。搜索结果里会有不少人问“为什么我App切后台再回来,页面数据全没了”,其实本质就是系统回收了Ability但没有任何恢复机制。
排查思路分三步走:第一步,在onStop里打印日志,确认这个Ability是否真的被销毁了。第二步,把需要恢复的数据统一保存到App级存储或者文件里。第三步,在onStart里判断是否存在恢复标记,有就读取数据做回填。这个方法脏但可靠,也是FA模型下最通用的方案。
6.2 Stage模型下窗口配置不生效
有的开发者在改造到Stage模型后,按文档设置了窗口属性(比如全屏、横屏),但运行时没有效果,接口又没报错。这个问题多出现在onWindowStageCreate里调用窗口属性的时机。
排查方法很简单:确认你是在loadContent之前设置的窗口属性,还是之后。如果你想设顶层窗口属性,应该先设属性再加载内容,否则内容加载后一些布局参数会被默认值覆盖掉。
正确的示例代码大概是:
onWindowStageCreate(windowStage: Window.WindowStage) { // 先获取主窗口并设置属性 let mainWindow = windowStage.getMainWindowSync(); mainWindow.setWindowLayoutFullScreen(true); // 再加载页面内容 windowStage.loadContent('pages/Index', (err) => { // 处理结果 }); }如果你设置时机没问题但依然无效,可以检查一下当前窗口是否处于分屏状态。分屏状态下部分属性会被系统约束,开发者工具里看不出来,但真机上表现异常。这种问题要结合设备窗口模式来做判断。
6.3 后台任务被系统清理
在Stage模型下,如果你做后台播放或后台下载,仅仅把逻辑放在UIAbility里是不够的。当应用退到后台一段时间,系统会根据运行策略清理UIAbility,你的后台任务就会中断。
排查逻辑:先确认你是不是申请了长时间运行任务。如果没有,系统会在应用退后台后按策略清理。解决方式是使用ExtensionAbility处理后台任务,同时搭配continuousTaskManager做任务声明。如果两者都做到了,还是被清理,再排查是不是没有获取对应的权限或者申请方式不对。
一个我实践过的稳妥方案是:后台播放和定位类任务放在独立的ExtensionAbility里,日志打印到单独的文件,方便随时检查运行状态。UIAbility退后台时不要主动释放ExtensionAbility持有的资源,只释放UI相关的内存。
6.4 状态管理装饰器导致UI刷新异常
这个坑更多出现在Stage模型搭配ArkUI的写法时。很多从FA迁过来的人会不习惯@State的变迁机制,以为声明了@State就万事大吉。实际上,当一个对象内部属性改变时,@State能监听到,但如果你改了对象引用本身,而没有通过@State重新赋值,UI就可能不刷新。
排查这个问题有一个很实用的思路:在属性变更处打印日志,看是否真正触发了装饰器的监听逻辑,再检查是不是赋值方式弄成了直接对对象内部属性操作而没有触发setter。如果确认监听没生效,优先考虑改成@Observed和@ObjectLink的组合来管理复杂对象。
这个情况在FA模型里不会出现,因为FA模型下本身没有这么强的数据绑定体系,UI刷新都是命令式的。正因为如此,从FA迁过来的人尤其容易在这个问题上卡壳,我身边就有分享者在这上面折腾了一整天。
7. 选型建议与个人实操体会
最后聊一下选型建议。我的立场非常明确:新项目无脑选Stage模型,除非你的目标设备跑不动Stage,或者你是做一个极轻量级的工具类应用,否则Framework的演进方向已经不会再回头了。
从团队长远维护的角度看,Stage模型带来的工程结构规范、类型体系完整、生态文档丰富,这些都是隐性收益。你可以想象一下,如果三年前用FA写的项目,现在想加一个多窗口小部件功能,改造成本会多高。而Stage模型在设计初期就把这些扩展能力考虑了进来,做多设备适配时能省掉大量工作量。
我个人在实际操作中的体会是:选模型不是一个纯技术决策,它更多是对未来生态投入的一个判断。你在FA模型上积累的很多代码迁移后并不是直接的语法替换,而是一次架构思想的升级。所以,与其考虑“哪个模型简单”,不如考虑“哪个模型能支撑你的应用走得更远”。
最后分享一个我的工作习惯:在正式写核心业务前,先花半天时间把工程里最复杂的页面用Stage模型完整跑通一遍,不做任何业务,只做页面加载、生命周期日志、窗口状态切换这三件事。等这三件事都摸透了,再开始迁移业务代码,你会发现自己对模型的理解会直观很多。这个习惯帮我避开了至少五次大范围重构的坑,也推荐你试试。