引言
Stage 模型把应用拆成「进程到模块、模块到 Ability、Ability 到窗口、窗口到页面」四层嵌套,其中 Ability 是系统调度的基本单位。很多开发者对 UIAbility 的回调顺序背得很熟,却对「一个应用到底能有哪几种 Ability」「什么场景必须写 ExtensionAbility」缺少整体认识,结果是该拆的后台能力没拆、不该常驻的服务硬起了,后台耗电与上架审核都会出问题。
Ability 体系真正的难点在边界:UIAbility 承载有界面的前台交互,ExtensionAbility 负责无界面的系统能力对接,两者被系统用完全不同的调度策略管理。同一段业务逻辑放在哪一侧,直接决定了它能否在应用退出后继续运行、能否被其他应用拉起、以及会不会被内存压力优先回收。
本文按「分类、配置、拉起、传参」四条线展开。生命周期回调的逐个职责已经在 鸿蒙应用与页面生命周期 里详细拆过,本篇不再重复那部分内容,重点补齐它没有覆盖的 ExtensionAbility 体系、module.json5 的字段语义,以及 Ability 之间的通信机制。
目录
- Ability 体系全景
- UIAbility 与 WindowStage 的职责边界
- ExtensionAbility 类型总览
- FormExtensionAbility 与服务卡片
- ServiceExtensionAbility 后台服务
- DataShareExtensionAbility 数据共享
- InputMethodExtensionAbility 输入法
- AbilityStage 与模块级初始化
- module.json5 配置详解
- Want 结构与显式隐式匹配
- startAbility 系列接口与返回值
- Ability 间数据传递的四种方式
- 权衡取舍
- 常见坑清单
- 小结
1. Ability 体系全景
Stage 模型把 Ability 分成两大类,理解这两类的差别比记住具体类型更重要。
| 类别 | 是否有 UI | 运行形态 | 调度方式 | 典型用途 |
|---|---|---|---|---|
| UIAbility | 有 | 前台交互单元 | 与窗口绑定,随窗口生命周期 | 应用页面入口、多窗口 |
| ExtensionAbility | 无 | 能力扩展单元 | 由系统按需拉起,可独立进程 | 卡片、后台任务、数据共享、输入法 |
两类 Ability 共享同一套进程与模块机制,差别在于是否与窗口绑定。UIAbility 一旦失去窗口就失去意义;ExtensionAbility 本来就没有窗口,它的存活只取决于系统是否需要这项能力。
一个常见误解是「ExtensionAbility 是 UIAbility 的子类」。它们都继承自 Ability 基类,但属于并列的两条继承链,API 与生命周期回调没有继承关系,不要指望把 UIAbility 的代码直接搬进 ExtensionAbility。
从进程角度看,系统默认把 ExtensionAbility 与主应用放在同一进程,但像卡片、输入法这类需要长期存活或跨应用共享的能力会被放到独立进程。这意味着跨进程的内存不共享,任何状态传递都必须走序列化通道。
2. UIAbility 与 WindowStage 的职责边界
UIAbility 与 WindowStage 是「能力实例」与「窗口舞台」的关系。一个 UIAbility 实例在 onWindowStageCreate 中拿到一个 WindowStage,由它负责加载页面内容;WindowStage 销毁时,UIAbility 可能仍然存活(例如切到后台再回来),也可能随之销毁。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
private stage: window.WindowStage | undefined = undefined;
onWindowStageCreate(windowStage: window.WindowStage): void {
// WindowStage 是 UIAbility 与窗口系统的唯一桥梁
this.stage = windowStage;
windowStage.loadContent('pages/Index');
}
onWindowStageDestroy(): void {
// 舞台销毁后旧引用立即失效,必须置空
this.stage = undefined;
}
}
职责划分可以压缩成一句话:UIAbility 管实例与全局状态,WindowStage 管窗口与内容加载。想在 Ability 里直接改 UI 是行不通的,Ability 只能把数据写进 AppStorage 或 LocalStorage,由页面侧消费。
多窗口场景下,一个 WindowStage 可以持有多个 Window 对象(例如平板分屏下的两个子窗口),此时 getMainWindowSync() 只返回主窗口,其余窗口要通过 windowStage.getMainWindow() 的异步版本或 window.getLastWindow() 逐个获取。
3. ExtensionAbility 类型总览
HarmonyOS NEXT 提供的 ExtensionAbility 类型按能力域划分,每种类型在 module.json5 中用 type 字段声明,写错会导致系统找不到该能力。
| 类型 | type 取值 | 触发方式 | 典型场景 |
|---|---|---|---|
| 服务卡片 | form | 用户添加卡片 / 定时刷新 | 桌面卡片、负一屏 |
| 后台服务 | service | startServiceExtensionAbility | 无界面长时任务 |
| 数据共享 | dataShare | 其他应用通过 DataShareHelper 访问 | 跨应用数据读写 |
| 输入法 | inputMethod | 系统输入法框架拉起 | 第三方输入法 |
| 无障碍 | accessibility | 系统无障碍框架拉起 | 屏幕朗读扩展 |
| 工作调度 | workScheduler | 系统按条件调度 | 定时同步、延迟任务 |
选型原则是「能用系统调度就不自己常驻」。工作调度与卡片刷新都属于系统托管的触发方式,应用不需要自己保活;只有确实需要跨应用暴露数据或提供输入法这类系统级能力时,才写对应的 ExtensionAbility。
4. FormExtensionAbility 与服务卡片
FormExtensionAbility 是最常用的一类扩展能力,负责卡片的创建、刷新与事件处理。它的完整生命周期、form_config.json 字段与刷新配额在 鸿蒙元服务与卡片开发
里已经展开,这里只强调它在 Ability 体系中的位置。
卡片能力必须同时在 module.json5 的 extensionAbilities 中注册,否则用户添加卡片时系统会提示找不到能力:
{
"module": {
"name": "entry",
"extensionAbilities": [
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"type": "form",
"metadata": [
{ "name": "ohos.extension.form", "resource": "$profile:form_config" }
]
}
]
}
}
一个容易被忽略的事实是:卡片运行在独立进程,FormExtensionAbility 里访问不到 UIAbility 的内存对象。要让卡片拿到最新数据,正确做法是主应用把数据写入 Preferences 或 RDB,再调用 formProvider.requestForm 请求刷新,由卡片进程重新读取。
5. ServiceExtensionAbility 后台服务
ServiceExtensionAbility 提供无界面的后台服务能力,通过 startServiceExtensionAbility 拉起,通过 connectServiceExtensionAbility 建立连接后做双向调用。
import { ServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
class SyncStub extends rpc.RemoteObject {
constructor() {
super('sync_stub');
}
onRemoteMessageRequest(code: number, data: rpc.MessageSequence,
reply: rpc.MessageSequence, options: rpc.MessageOption): boolean {
if (code === 1001) {
const payload = data.readString();
reply.writeString(`echo:${payload}`);
return true;
}
return false;
}
}
export default class SyncService extends ServiceExtensionAbility {
onCreate(want: Want): void {
// 服务实例创建,适合做一次性初始化
}
onRequest(want: Want, startId: number): void {
// 每次 startServiceExtensionAbility 都会触发
}
onConnect(want: Want): rpc.RemoteObject {
// 返回远程对象,调用方拿到后可跨进程调用
return new SyncStub();
}
onDisconnect(want: Want): void {
// 连接断开,注意这里不一定会被调用
}
onDestroy(): void {
// 释放资源
}
}
必须清楚一条限制:ServiceExtensionAbility 不能自行常驻。应用退到后台后,系统会按内存压力回收进程;真正需要长时间运行的任务要走 backgroundTaskManager 申请对应类型的长时任务,或者改用 WorkSchedulerExtensionAbility 交给系统按条件调度。把后台服务当常驻进程用,是耗电与审核被拒的高频原因。
6. DataShareExtensionAbility 数据共享
DataShareExtensionAbility 用于把自己的数据以标准接口暴露给其他应用,是鸿蒙上跨应用数据访问的官方通道。它的实现分两部分:Provider 侧继承 DataShareExtensionAbility 并实现增删改查,Consumer 侧用 DataShareHelper 访问。
import { DataShareExtensionAbility, dataShare, Want } from '@kit.ArkData';
import { rdb } from '@kit.ArkData';
export default class NotesDataShare extends DataShareExtensionAbility {
async query(uri: string, predicates: dataShare.DataSharePredicates,
columns: Array<string>, callback: AsyncCallback<dataShare.ResultSet>): Promise<void> {
const store = await rdb.getRdbStore(this.context, {
name: 'notes.db',
securityLevel: rdb.SecurityLevel.S1
});
const resultSet = await store.query(predicates as rdb.RdbPredicates, columns);
callback(null, resultSet);
}
}
Provider 侧必须在 module.json5 中声明 type: "dataShare" 并配置 metadata 指向 data_share_config,同时在 data_share_config 里通过 readPermission / writePermission 声明访问方需要的权限。权限漏配的表现是调用方查询返回空结果而不是报错,排查时优先检查这一项。
数据分级是另一个关键点:DataShare 暴露的数据会受 securityLevel 约束,S1 到 S4 对应不同敏感度,S3 及以上的数据在跨设备与备份场景下会受限。业务数据默认用 S1,涉及用户身份与支付信息的才升级。
7. InputMethodExtensionAbility 输入法
InputMethodExtensionAbility 是第三方输入法的入口能力,它由系统输入法框架拉起,与普通 ExtensionAbility 最大的差别是进程模型更严格:输入法进程在用户切换输入法时被拉起,长时间无交互后被回收,且必须能容忍随时被销毁。
{
"module": {
"extensionAbilities": [
{
"name": "InputMethodExtAbility",
"srcEntry": "./ets/inputmethodextability/InputMethodExtAbility.ets",
"type": "inputMethod",
"metadata": [
{ "name": "ohos.extension.input_method", "resource": "$profile:input_method_config" }
]
}
]
}
}
输入法开发的实际难点不在 Ability 本身,而在与 InputMethodEngine 的会话管理:每一次用户聚焦输入框都会创建一次输入会话,输入法必须在 onStartInput 与 onFinishInput 之间维护候选词状态,并且不能假设会话之间的内存是连续的。绝大多数输入法卡顿都来自会话切换时没有清理上一轮的候选词缓存。
8. AbilityStage 与模块级初始化
AbilityStage 是模块级入口,对应 module.json5 中模块级的 srcEntry 字段。它只在该模块的进程首次启动时创建一次,早于模块内任何 UIAbility 与 ExtensionAbility。
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/entryability/MyAbilityStage.ets"
}
}
从 Ability 体系的角度看,AbilityStage 有三个值得记住的特性:
- 多模块各有一个:HAP 与 HSP 模块都可以有自己的 AbilityStage,进程启动时会按依赖顺序依次创建。
- 跨 Ability 共享初始化:日志框架、崩溃捕获、全局配置读取这类「与具体 Ability 无关」的初始化放在这里,避免在每个 Ability 的
onCreate里重复执行。 onMemoryLevel的响应点:系统内存告警会回调到 AbilityStage,是集中释放缓存的最佳位置,比在每个页面里各写一套更可控。
职责划分的判断标准很简单:跟具体 Ability 无关、整个模块只需做一次的事放 AbilityStage;跟某次拉起参数相关的事放 UIAbility。
9. module.json5 配置详解
module.json5 是模块的元信息声明文件,abilities 与 extensionAbilities 两个数组决定了模块对外暴露的能力。
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/entryability/MyAbilityStage.ets",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"launchType": "singleton",
"exported": true,
"skills": [
{ "entities": ["entity.system.home"], "actions": ["action.system.home"] }
]
}
],
"extensionAbilities": [
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"type": "form",
"exported": false
}
]
}
}
| 字段 | 作用 | 常见误用 |
|---|---|---|
| name | Ability 的唯一标识,与类名无关 | 改名后忘改 startAbility 里的名字 |
| srcEntry | 相对模块根目录的入口文件路径 | 写成绝对路径或漏掉 ./ |
| exported | 是否允许其他应用拉起 | 内部 Ability 设为 true 导致越权入口 |
| skills | 隐式匹配条件(entities、actions) | 入口 Ability 漏配 action.system.home |
| launchType | 实例复用策略 | 需要多实例的场景仍用 singleton |
| type | ExtensionAbility 能力类型 | 与 metadata 的 name 不匹配 |
exported 是最需要谨慎的字段。设为 true 意味着任何应用都可以通过 startAbility 拉起这个 Ability,如果它没有做参数校验,就相当于开了一个任意入口。只有确实需要被外部拉起的能力才设为 true,入口 Ability 通常必须为 true,其余一律 false。
10. Want 结构与显式隐式匹配
Want 是 Ability 拉起时的意图描述对象,分为显式与隐式两种形态。
import { Want } from '@kit.AbilityKit';
// 显式:直接指定 bundleName 与 abilityName
const explicit: Want = {
bundleName: 'com.example.notes',
abilityName: 'EntryAbility',
parameters: { docId: '1001', from: 'search' }
};
// 隐式:只给 action 与 entities,由系统匹配 skills
const implicit: Want = {
action: 'ohos.want.action.viewData',
entities: ['entity.system.browsable'],
uri: 'https://example.com/article/1',
parameters: { referrer: 'app' }
};
| 形态 | 必需字段 | 匹配方式 | 适用场景 |
|---|---|---|---|
| 显式 | bundleName、abilityName | 直接定位,跳过 skills | 应用内部跳转 |
| 隐式 | action(可选 entities、uri) | 与目标 skills 逐条比对 | 拉起系统能力或其他应用 |
隐式匹配的规则是全集包含:Want 里写的每一项都必须被目标的 skills 覆盖,多写一项不匹配就失败。例如目标只声明了 action.system.home,你多带一个 entities: ["entity.system.browsable"],匹配就会失败,且系统只抛一个笼统的「无法找到匹配的 Ability」错误,非常难排查。
11. startAbility 系列接口与返回值
拉起 Ability 的接口都挂在 UIAbilityContext 上,选哪个取决于「是否需要返回值」与「是否需要跨设备」。
import { common, Want, StartOptions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
const context = getContext(this) as common.UIAbilityContext;
const want: Want = { bundleName: 'com.example.notes', abilityName: 'EditAbility' };
const options: StartOptions = { windowMode: 1, displayId: 0 };
// 无返回值,最常用
context.startAbility(want, options).catch((err: BusinessError) => {
console.error(`startAbility failed: ${err.code}`);
});
// 需要返回值:目标 Ability 通过 terminateSelfWithResult 回传
context.startAbilityForResult(want).then((result) => {
if (result.resultCode === 0) {
const data = result.want?.parameters?.['editedText'] as string;
console.info(`edited: ${data}`);
}
});
startAbilityForResult 必须配对使用:调用方拿到的是 Promise,被调用方必须在合适的时机调用 context.terminateSelfWithResult({ resultCode, want }),否则调用方会一直等不到结果。跨设备拉起用 startAbility 的 StartOptions 里带 deviceId,但需要先申请分布式相关权限。
12. Ability 间数据传递的四种方式
Ability 之间不能直接共享内存,传递数据必须走下面四条通道之一。
| 方式 | 方向 | 数据规模 | 是否跨进程 | 适用场景 |
|---|---|---|---|---|
| want.parameters | 单向传入 | 小,需可序列化 | 是 | 拉起参数、页面标识 |
| startAbilityForResult | 双向 | 小,需可序列化 | 是 | 编辑页回传结果 |
| EventHub | 同一 Ability 内 | 任意 | 否 | Ability 与页面通信 |
| commonEventManager | 广播 | 小 | 是 | 系统事件、跨应用通知 |
// EventHub:UIAbility 与页面在同一进程内的高效通道
// Ability 侧发布
this.context.eventHub.emit('dataReady', { id: 1, title: '草稿' });
// 页面侧订阅
@Entry
@Component
struct Index {
aboutToAppear(): void {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
ctx.eventHub.on('dataReady', (payload: Record<string, Object>) => {
console.info(`received: ${payload.title}`);
});
}
aboutToDisappear(): void {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
ctx.eventHub.off('dataReady');
}
}
四条通道里最容易误用的是 want.parameters:它只支持可序列化的基础类型与简单对象,传 PixelMap、UIContext 这类宿主对象会在运行时失败;而且参数总大小有上限,超过几百 KB 会被截断。大块数据走持久化再传标识符,永远不要把大对象塞进 want。
权衡取舍
Ability 体系的设计取舍集中在三个问题上,不同选择会显著影响耗电、审核与架构复杂度。
| 决策点 | 方案 A | 方案 B | 建议 |
|---|---|---|---|
| 后台任务形态 | ServiceExtensionAbility 常驻 | WorkSchedulerExtensionAbility 按条件调度 | 优先 B,只有需要即时响应才用 A |
| 卡片数据来源 | 卡片进程自己发请求 | 主应用写库、卡片读库 | 优先 B,避免重复请求与配额冲突 |
| 跨应用数据 | DataShareExtensionAbility | 拉起对方页面传参 | 需要批量读写用 A,单次交互用 B |
| Ability 与页面通信 | EventHub | AppStorage | 同一 Ability 内用 A,跨 Ability 用 B |
一个反直觉的结论是:ExtensionAbility 越多不等于架构越好。每个扩展能力都是一条独立的进程生命周期与一套配置,配置错误的表现往往又是静默失败。能用系统托管的能力(卡片刷新、工作调度)就不要自己起服务,能用同一 Ability 内的 EventHub 解决的就不要引入广播。
常见坑清单
- ExtensionAbility 未在 module.json5 注册。 编译期不报错,运行时系统提示找不到能力,添加卡片或调用数据共享时直接失败。
type与 metadata 的 name 不匹配。 例如 type 写form但 metadata 写ohos.extension.data_share,能力注册成功但永远不被拉起。- 把 ServiceExtensionAbility 当常驻进程。 应用退到后台后被系统回收,表现为「同步任务跑一半就断」;长时任务必须申请 backgroundTaskManager 类型。
- 内部 Ability 的 exported 设为 true。 任何应用都能拉起,等于开了任意入口,上架审核与安全扫描都会提示风险。
- 隐式 Want 多写 entities 导致匹配失败。 匹配是全集包含关系,多一项即失败,且错误信息笼统难以定位。
- startAbilityForResult 后忘了 terminateSelfWithResult。 调用方 Promise 永远不 resolve,页面卡在 loading 态。
- 往 want.parameters 塞大对象或宿主对象。 序列化失败或超出大小上限被截断,表现为目标页拿到 undefined。
- 卡片进程里访问主应用全局单例。 跨进程内存不共享,读到的是初始值或过期值。
- DataShare 漏配 readPermission。 调用方查询返回空结果而不是权限错误,容易误判为数据没写进去。
- EventHub 订阅未在 aboutToDisappear 里 off。 页面销毁后回调仍触发,闭包持有的状态无法回收。
小结
Ability 体系的核心是两类能力的边界:UIAbility 面向有界面的前台交互,ExtensionAbility 面向无界面的系统能力对接,二者共享模块与进程机制但不共享生命周期语义。配置侧要记住三件事:exported 默认为 false、type 必须与 metadata 一致、srcEntry 是相对模块根目录的路径。拉起侧要记住显式与隐式的区别,以及隐式匹配的全集包含规则。传参侧则要守住一条底线:跨 Ability 只传可序列化的小数据,大块内容先落盘再传标识符。
理解了这套体系,再看生命周期回调就有了位置感——回调的触发时机由 Ability 的类型与进程模型决定,而不是由代码写在哪儿决定。想继续深入窗口与页面的关系,可以回看 鸿蒙应用与页面生命周期 ;想了解卡片这条最常见的 ExtensionAbility 链路,见 鸿蒙元服务与卡片开发 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。