ArkTS UIAbility生命周期与启动模式实战指南
第一次在 ArkTS 工程里看到UIAbility这个类很多人会愣一下明明是写页面的为什么要先跟一个能力打交道我最初也这么想直到把日志一行行打出来才明白UIAbility是 ArkTS 应用里应用进程与界面窗口之间的那个枢纽。这份笔记就围绕ArkTS 应用的 UIAbility 组件生命周期展开讲清楚六个回调到底在什么时候触发、为什么这么设计、第一个工程怎么一步步跑通、启动模式又是怎么把生命周期剧本改写的。刚接触 ArkTS 的同学可以照着抄一遍流程已经写过几个页面的同学可以重点看启动模式和踩坑排查那两节——那部分是我在实际项目里反复被绊倒才总结出来的。全文的代码都可以直接落进工程里跑日志怎么打、参数怎么算、坑在哪我都会写明白。1. 先把 UIAbility 摆到正确的位置上1.1 它到底是什么又容易被误解成什么UIAbility是 ArkTS 应用框架里的应用组件一个应用可以有多个UIAbility每一个都代表一份可以被系统调起、并且能承载界面的能力。它不是一个页面也不是一个进程更不是路由容器。准确的说法是它是应用与系统之间的契约单位。系统通过它来启动应用、把应用切到前台、切到后台、销毁应用应用通过它来声明我能处理什么样的启动请求。新手最常见的误解是把UIAbility当成Activity或者ViewController的等价物。思路接近但边界不一样。UIAbility本身不负责布局也不负责组件树界面由它持有的WindowStage去加载具体页面。所以职责上分成三层UIAbility管生命周期和业务入口WindowStage管窗口舞台页面管视图与交互。这个分层一旦记住后面所有回调的位置就都顺了。我在带新人的时候喜欢用一个类比UIAbility像一家店的店长WindowStage是店面的卷帘门和橱窗页面是店里陈列的商品。店长到岗onCreate不等于开门营业onWindowStageCreate门开了也不等于顾客已经进来了onForeground。1.2 一个 Ability 实例、一个窗口舞台、一棵页面树三者的数量关系需要说清楚否则调 API 时很容易找错对象。一个UIAbility实例在生命周期内通常对应一个WindowStage实例一个WindowStage下挂着一个主窗口Window主窗口里加载一棵页面树。多实例模式下一个UIAbility类可以有多个实例每个实例各自持有一套窗口舞台。这里有个细节值得提前知道onWindowStageCreate回调的参数就是那个WindowStage对象你要做的第一件事是调用loadContent把首页塞进去。很多人卡在这一步是因为把loadContent写在了onCreate里——那时候窗口舞台根本还没准备好对象是拿不到的强行拿只会拿到undefined然后一运行就白屏。另外一个容易忽视的点UIAbility有独立的Context即UIAbilityContext。它能做的事情比页面的Context更重一些比如启动另一个UIAbility、主动结束自己、申请某些系统能力。这也是为什么很多初始化逻辑放在UIAbility里更合适——它拿到的上下文权限层级更高。1.3 为什么总是把 Ability 生命周期和页面生命周期搞混因为两套生命周期确实会同时在日志里刷出来而且看起来顺序还挺像。实际上它们由完全不同的机制驱动UIAbility的生命周期由系统对应用的前后台管理驱动页面生命周期由路由和页面栈驱动。举个能立刻区分两者的场景应用在前台时弹出一个系统级对话框或者进入分屏页面可能触发onPageHide但UIAbility不一定走onBackground。反过来应用被切到后台UIAbility一定走onBackground页面通常也会走onPageHide但某些页面栈操作不会引起UIAbility状态变化。所以判断标准很干脆跟应用整体是否可见相关的逻辑放UIAbility跟某个页面是否被覆盖相关的逻辑放页面。埋点统计里的应用前台时长用onForeground/onBackground算页面停留时长用onPageShow/onPageHide算。混用就会出现数据对不上的情况这类问题排查起来非常费时间。2. 六个生命周期回调的触发链路拆解2.1 onCreate只做轻量初始化别在这里干重活onCreate(want, launchParam)是UIAbility实例被创建后的第一个回调整个实例生命周期内只会触发一次。它拿到两个关键参数want描述这次启动的意图和携带的数据launchParam描述启动原因比如是普通启动还是从其他设备迁移过来的。这个回调的核心价值是接参数。用户在桌面点击图标、从其他应用跳转过来、从通知栏点进来携带的数据都在want里。我一般的做法是在这里解析want.parameters把必要字段存到成员变量或者交给一个轻量的状态管理对象供后续onWindowStageCreate加载页面时使用。要避免的事情很明确不要在这里做数据库大批量读写、不要同步拉网络、不要初始化特别重的 SDK。原因很实际——onCreate到首帧渲染的这段时间是用户感知启动快慢的窗口期任何同步阻塞都会直接变成点击图标后愣一下的体感。正确做法是把重活扔到异步任务里或者延迟到首帧出来之后再触发比如在onForeground里起一个异步任务或者用空闲任务机制延后执行。还有一个容易被忽略的点onCreate里this.context已经可用了但窗口相关的一切都不可用。所以任何需要windowStage的操作都别写在这里。2.2 onWindowStageCreate界面真正落地的地方onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent(pages/Index, (err) { if (err.code) { // 加载失败要打日志不要静默吞掉 return; } }); }这是六个回调里最实在的一个因为它是唯一一个能拿到WindowStage的创建类回调。你在里面调用loadContent指定首页路径页面树才真正建立起来。loadContent的第二个参数是回调必须处理错误分支。我见过不少工程直接把回调写成空的结果页面路径写错、首页文件不存在、或者组件编译失败时表现就是干净的白屏日志里什么都没有排查时只能靠猜。把err.code和err.message打出来能省掉大量时间。除了加载首页这个回调还适合做与窗口相关的设置设置窗口背景色、调整状态栏样式、设置窗口尺寸限制、注册窗口事件监听。这类操作都依赖windowStage放在这里最合适。注意要放在loadContent之前还是之后取决于操作对象——对窗口本身的属性设置放在loadContent前后都行但如果要拿页面内容相关的对象就得等加载完成之后再操作。有一个进阶点较新的接口版本里还提供了onWindowStageWillDestroy(windowStage)在窗口舞台即将销毁时回调可以用来做一些窗口资源的提前释放。这个接口在不同版本上存在差异用之前建议先核对当前工程使用的接口版本别直接照搬网上的代码。2.3 onForeground 与 onBackground一对镜像状态onForeground在UIAbility切换到前台、界面即将可见时触发onBackground在切换到后台、界面完全不可见时触发。这两个回调会反复触发是使用频率最高的一对。它们的执行顺序值得记住冷启动链路是onCreate→onWindowStageCreate→onForeground→ 页面aboutToAppear→ 页面onPageShow。从后台回到前台是onForeground→ 页面onPageShowonCreate不会再走。返回桌面是onBackground→ 页面onPageHide。onForeground的典型用途是恢复恢复定时器、恢复轮询、恢复传感器订阅、刷新一次可能已经过期的数据。onBackground的典型用途是暂停清定时器、取消订阅、暂停媒体播放、落一次内存中的草稿。注意onBackground触发后应用并不会立刻被销毁但后台存活时间是有约束的系统在资源紧张时会回收进程。所以不要把唯一一份数据只放在内存里等onDestroy去持久化那个回调不保证一定执行。实践里我强烈建议把暂停和恢复写成严格配对的两个方法而不是散落在各处否则很容易出现定时器越开越多、订阅泄漏的问题。我在一个项目里就是因为onForeground里重复注册了监听器却没在onBackground里注销切了十几次前后台之后同一个事件被回调了十几遍排查了半天才定位到。2.4 onWindowStageDestroy 与 onDestroy顺序不能反应用退出时回调顺序是onWindowStageDestroy→onDestroy。先拆窗口再拆能力实例这个顺序是有道理的视图层先释放业务层再收尾。onWindowStageDestroy里适合做与界面强相关的清理比如注销窗口事件监听、释放与windowStage绑定的资源。onDestroy里则做业务层面的收尾比如断开连接、保存状态、释放单例。但有一点必须说清楚onDestroy不是一定会执行的保证。用户主动返回退出、应用调用terminateSelf结束自己这类正常路径会走完整链路但系统因内存压力直接回收进程时可能不走任何回调。所以关键数据要在状态变化的当下就落地而不是攒到最后。回调触发时机是否重复触发典型用途大忌onCreate实例创建仅一次否解析 want、轻量初始化同步重活、拿 windowStageonWindowStageCreate窗口舞台就绪否loadContent、窗口属性设置空回调不处理错误onForeground切到前台可见是恢复定时器、刷新数据重复注册不注销onBackground切到后台不可见是暂停任务、存草稿依赖它做关键持久化onWindowStageDestroy窗口即将销毁否注销窗口监听在这里做业务收尾onDestroy实例销毁否释放资源、断开连接把它当唯一保存时机这张表我建议直接存进工程文档里团队里每个人写生命周期相关代码之前扫一眼能规避掉一大半低级问题。3. 从零跑通第一个 ArkTS 应用可复现的实操流程3.1 工程创建与目录结构确认用官方 IDE 新建一个空模板工程选好模块类型和应用模型创建完成后先别急着写代码把目录结构看一遍。与生命周期直接相关的几个位置是固定的entry/src/main/ets/entryability/EntryAbility.etsUIAbility的实现文件生命周期回调都在这里entry/src/main/ets/pages/Index.ets首页带Entry装饰器的自定义组件entry/src/main/module.json5模块配置声明UIAbility、入口能力、启动模式entry/src/main/resources/base/profile/main_pages.json页面路径注册表我建议第一步就是在这个路径下找到自动生成的EntryAbility.ets把里面的回调全删掉只留onCreate然后逐个加回来边加边看日志。这个做减法再加法的过程看起来笨但能把每个回调的位置刻进脑子里比看十遍文档都管用。main_pages.json里要确认首页路径注册了。常见的白屏问题之一就是loadContent里写的路径和这个文件里注册的不一致或者大小写对不上。这个文件里的路径是相对于ets目录的写pages/Index而不是./pages/Index.ets这点别搞混。3.2 module.json5 里那几个必须看懂的字段{ module: { name: entry, abilities: [ { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ets, launchType: singleton, skills: [ { entities: [entity.system.home], actions: [ohos.want.action.home] } ] } ] } }srcEntry指向实现文件路径写错会在编译期或启动期报错这个最容易发现。launchType决定多实例行为后面单独讲。skills里的两个声明决定了这个UIAbility能不能作为桌面入口被点开——entity.system.home和ohos.want.action.home同时存在桌面图标才能正常拉起它。我遇到过一次很典型的配置问题新建了一个UIAbility想作为独立入口测试srcEntry写对了代码也写了但桌面上就是多不出一个图标。折腾半天才发现新加的那个UIAbility没配skills或者说桌面的入口能力只认配了那两项的那个。所以如果你要做多入口应用每个入口的skills都要单独确认。3.3 一份可以照着抄的 EntryAbility 实现import { AbilityConstant, UIAbility, Want } from kit.AbilityKit; import { window } from kit.ArkUI; import { hilog } from kit.PerformanceAnalysisKit; const DOMAIN 0x0000; const TAG LifeCycleDemo; export default class EntryAbility extends UIAbility { private startTs: number 0; onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { this.startTs Date.now(); hilog.info(DOMAIN, TAG, onCreate, action%{public}s, want.action ?? none); hilog.info(DOMAIN, TAG, launchReason%{public}d, launchParam.launchReason); } onWindowStageCreate(windowStage: window.WindowStage): void { hilog.info(DOMAIN, TAG, onWindowStageCreate); windowStage.loadContent(pages/Index, (err) { if (err.code) { hilog.error(DOMAIN, TAG, loadContent failed, code%{public}d, msg%{public}s, err.code, err.message); return; } hilog.info(DOMAIN, TAG, loadContent success); }); } onForeground(): void { hilog.info(DOMAIN, TAG, onForeground, cost%{public}dms, Date.now() - this.startTs); } onBackground(): void { hilog.info(DOMAIN, TAG, onBackground); } onWindowStageDestroy(): void { hilog.info(DOMAIN, TAG, onWindowStageDestroy); } onDestroy(): void { hilog.info(DOMAIN, TAG, onDestroy); } }这段代码的重点不在功能而在可观测。每一行日志都带固定 TAG方便过滤onCreate里记了时间戳在onForeground里算出启动耗时。这个耗时数值很实用同一台设备、同一个版本多次冷启动的数值应该比较接近如果某次明显变大通常意味着你又往onCreate里加了阻塞逻辑。启动耗时的计算方式就是简单相减onForeground时刻减去onCreate时刻。我一般在调试版本里保留这个打点看到数值从 200 毫秒左右涨到 500 毫秒以上就知道该去查初始化链路了。数值本身依设备和工程复杂度差异很大重点看趋势而不是绝对值。3.4 把日志跑出来一轮完整验证流程代码写好之后按下面的顺序操作每一步都去看日志窗口用 TAG 过滤观察回调序列是否符合预期冷启动应用预期看到onCreate→onWindowStageCreate→loadContent success→onForeground中间可能夹着页面的aboutToAppear和onPageShow。按 Home 键退到桌面预期看到onBackground页面侧看到onPageHide。从最近任务列表点回应用预期看到onForeground不会出现onCreate。在应用内退出比如调用terminateSelf预期看到onWindowStageDestroy→onDestroy。重复第 1 到第 3 步三次观察onForeground打印的耗时是否稳定。第 3 步是很多人第一次发现自己误判的地方从后台回来时onCreate不会触发如果你把数据初始化写在onCreate里同时又在页面aboutToAppear里依赖那份数据那从后台回来时页面重建可能就拿不到数据了。解决办法是把只做一次的初始化和每次可见都做的刷新明确分开分别放在onCreate和onForeground里。4. 启动模式如何把生命周期剧本改写4.1 三种 launchType 的取舍逻辑module.json5里launchType有三个取值直接决定重复启动同一个UIAbility时会发生什么。launchType重复启动行为生命周期表现适用场景singleton复用已有实例不走 onCreate走 onNewWant主入口、唯一实例的业务multiton每次新建实例每次都走完整创建链路多窗口、多文档编辑specified按 key 复用实例同 key 复用走 onNewWant同一类界面开多份但可去重默认是singleton这也是大多数应用主入口该用的模式。理由很直接主界面在系统里应该只有一份重复点图标应该是回到那个界面而不是再开一个一模一样的界面。如果你把主入口设成multiton用户反复点图标就可能开出好几个实例每个实例都占一份窗口和内存体验和资源都说不过去。specified稍微特殊它依赖一个 key 来做实例区分。适合那种同一类页面可以开多份但相同的 key 不应该开第二份的场景比如按会话 ID 打开的聊天界面。这类场景如果不做去重用户从通知点进来就可能叠出两个相同的会话窗口。4.2 onNewWant单实例模式下第二封信的投递口在singleton模式下UIAbility已经存在时再次被启动不会走onCreate而是走onNewWant(want, launchParam)。这个回调是接收第二次及以后启动意图的唯一入口。onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN, TAG, onNewWant, action%{public}s, want.action ?? none); this.handleNewWant(want); }这里有个非常容易踩的坑只在onCreate里解析want忘了onNewWant。后果是应用已经在运行时从外部带参数跳进来参数直接被丢掉界面没有任何变化。用户看到的现象是点了没反应开发者看到的现象是日志里什么都没打。所以只要你的UIAbility可能被外部带参数启动就必须把参数解析抽成一个方法onCreate和onNewWant都调它。注意onNewWant触发时界面可能已经在前台也可能是从后台被拉起。如果你需要根据新参数刷新界面最好通过状态管理或者事件机制通知页面而不是直接操作 UI 对象。4.3 冷启动、热启动、后台唤醒的实测差异把三种情况混为一谈是新手常犯的错。我按实际打点整理一下差异冷启动是进程不存在从零开始链路最长onCreate→onWindowStageCreate→onForeground→ 页面创建 → 页面显示。热启动是进程还在、UIAbility实例还在、只是从后台回到前台链路最短onForeground→ 页面显示。还有一种介于两者之间的情况进程在UIAbility实例被销毁过重新启动时会走onCreate但进程级初始化可以复用。这个差异带来的直接结论是不要假设onCreate每次都跑也不要假设页面每次都是新建的。所有每次可见都要正确的状态都应该在onForeground或页面onPageShow里校准一次所有只该做一次的初始化才放在onCreate或进程级单例里。我在一个项目里就吃过这个亏某个配置项只在onCreate时从存储里读一次结果在配置变更后从后台切回来读到的还是旧值界面上显示的状态和实际不符。后来改成在onForeground里比对版本号按需刷新问题才彻底消失。5. 踩坑记录与排查速查表5.1 启动即白屏先看三处白屏是最常见的首屏问题按顺序查这三处基本能覆盖大多数情况。第一处是loadContent的路径和main_pages.json里注册的是否完全一致包括大小写。第二处是loadContent的错误回调有没有写没写就把err.code打出来。第三处是页面文件本身Entry装饰器有没有加组件是否编译通过。有一次我排查了将近一个小时最后发现是页面文件名里多了一个空格路径看起来对实际匹配不上而错误回调又被我图省事写成了空函数。自那以后我所有工程的loadContent回调都会打错误日志成本几行代码收益巨大。5.2 onForeground 走了但界面没刷新现象是从后台切回来日志显示onForeground正常触发但界面数据是旧的。原因通常是把刷新逻辑放在了只执行一次的地方。定位方法是看数据来源如果数据来自onCreate里的一次性读取就把它挪到onForeground或者加一个是否需要刷新的判断。如果数据来自页面aboutToAppear而页面实例被复用了没重建就得改成在onPageShow里刷新。这里有个判断技巧在onForeground和页面onPageShow里各打一条带时间戳的日志看每次切回前台时哪条打了、哪条没打。没打的那条所在的层级就是刷新逻辑不该放的地方。5.3 参数丢失与重复注册参数丢失在 4.2 节已经说过核心是onNewWant别忘了。重复注册的表现更隐蔽功能看起来正常但某些操作会执行多次或者内存占用随着前后台切换次数增长。排查方法是给每个注册动作配一个注销动作用日志成对打点切换五次前后台看注册次数和注销次数是否相等。不相等就说明有泄漏。我现在的习惯是只要写了onForeground里的恢复逻辑就立刻在同一屏代码里把onBackground的暂停逻辑写完绝不分开写这样物理上就杜绝了漏写。现象高概率原因处理方式启动白屏无日志错误回调为空、路径不匹配打err.code、核对页面注册表从后台回来数据旧刷新写在一次性回调里挪到 onForeground 或 onPageShow外部跳转无反应只解析 onCreate 的 want抽公共方法onNewWant 也调用前后台切换后卡顿监听器重复注册未注销注册注销成对写日志校验次数退出时数据没保存依赖 onDestroy 持久化状态变化当下即落地多开窗口互相干扰launchType 用了 multiton主入口改 singleton必要时用 specified这张表我放在工程文档的第一页新人接手时先读一遍比口头交代有效得多。5.4 关于异步与线程的两点经验onCreate和onWindowStageCreate都在主线程执行任何耗时操作都会直接拖慢首屏。判断标准很简单如果一段逻辑可能超过一帧的时间就不该同步跑在这两个回调里。可以扔到异步任务里或者延迟到首帧之后。onBackground里也是同理。虽然此时界面不可见但主线程阻塞仍然会影响系统对应用状态的判定。需要写文件的场景用异步接口别用同步接口硬写。我早期为了图省事在onBackground里用同步方式写了一个小文件数据量小的时候没感觉数据量一大就出现过切后台时短暂无响应的情况。6. 几处只有踩过才知道的细节onWindowStageCreate里加载完首页之后如果你想拿页面里的某个对象做交互别急回调里页面组件树可能还没完全就绪。稳妥做法是通过状态管理或者事件通知让页面在自己准备好的时候主动来取而不是从UIAbility侧去够页面里的东西。这个方向反了就会出现拿不到对象、拿到的是旧对象之类的怪问题。UIAbilityContext和页面的Context混用也是高频问题。启动另一个UIAbility、结束当前实例这类操作要用能力侧的上下文获取资源、读配置文件用页面侧的上下文更顺手。两者的生命周期长度不同把能力侧上下文长期持有在页面的成员变量里容易造成意料之外的引用问题。我现在的习惯是需要跨层传递时只传必要的字段不传上下文对象本身。日志方面再补一句hilog的格式化占位符里字符串参数要加%{public}s才会原样输出不加就可能被打成脱敏形式。我第一次打日志看到一堆星号还以为数据没传进来折腾了半天。数值用%{public}d字符串用%{public}s这个习惯早点养成调试时能省很多时间。最后一个细节关于进程回收。应用退到后台后系统会根据资源情况回收进程回收时不保证走onDestroy。所以任何退出前保存的设计都不可靠。我现在的做法是在数据发生变化的当下就写一份轻量草稿onBackground时再补一次完整保存双保险。这样即使进程被直接回收用户下次进来也不会丢太多东西。这套东西写下来其实核心就一句话把应用整体状态和界面局部状态分清楚把只做一次和每次都要做分清楚。剩下的六个回调、三种启动模式都只是这两条原则的具体落点。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →