HarmonyOS NEXT——使用 AppServiceExtensionAbility 实现真正的后台服务
这篇是我给 AppServiceExtensionAbility 写的学习+实践笔记,整理完就是一篇可以直接发的博客。 关键词:后台长驻服务、start / connect 两种拉起方式、RPC 通信、客户端鉴权。
1. AppServiceExtensionAbility 是什么?
从 API version 20 开始,HarmonyOS 提供了一个专门用于后台服务的组件类型: AppServiceExtensionAbility。
它的定位非常清晰:
- 适合 长期在后台无界面运行 的业务;
- 可以被 自身应用 或 白名单应用 拉起/连接;
- 支持通过 RPC 和多个客户端通信;
- 生命周期独立于前台 UIAbility,可以“不依赖界面一直跑”。
一个典型场景就是企业的 数据防泄漏(DLP)软件:
- 没有前台 UI,也不能让用户频繁感知;
- 需要长期监听:文件操作、网络行为;
- 一旦发现违规要立刻拦截;
- 这类核心逻辑就非常适合放在 AppServiceExtensionAbility 里。
简单一句话: AppServiceExtensionAbility = 给普通应用开放的「可控后台常驻服务能力」。
2. 使用前提 & 限制
这部分一定要提前说清楚,否则踩环境坑会很烦。
2.1 设备限制
目前 AppServiceExtensionAbility:
- 仅支持 2in1 设备 (也就是说在很多手机/其他设备上暂时不能用)。
2.2 权限限制
集成 AppServiceExtensionAbility 的应用,需要申请一个 ACL 权限:
ohos.permission.SUPPORT_APP_SERVICE_EXTENSION
- 这个权限目前只对 企业普通应用 开放申请;
- 没有这个权限,服务是跑不起来的。
2.3 能力限制
在 AppServiceExtensionAbility 中:
- 不能调用 window 相关 API,也就是不能直接操作窗口、UI;
- 它本质上就是纯后台服务逻辑。
3. 运作机制:谁是服务端,谁是客户端?
文档里是这样约定的:
- 服务端: 被启动 / 被连接的
AppServiceExtensionAbility实例; - 客户端: 发起启动/连接请求的组件,目前只支持 UIAbility 做客户端。
客户端可以通过两种方式使用后台服务:
startAppServiceExtensionAbility()—— 启动型connectAppServiceExtensionAbility()—— 连接型
这两种方式的区别稍后细讲,这里先看「谁有资格拉起服务」。
3.1 谁可以启动 / 连接服务?
关键在 module.json5 里 extensionAbilities 的配置项:
"appIdentifierAllowList": [
// 填允许启动或连接该后台服务的客户端应用的 appIdentifier 列表
]
核心规则可以总结成下面这张逻辑表(用文字说一遍):
| 操作方式 | 服务端当前状态 | 客户端是否可信(同应用或在 allowList 中) | 结果 |
|---|---|---|---|
| start | 未启动 | 是 | ✅ 启动成功 |
| start | 未启动 | 否 | ❌ 拒绝启动 |
| start | 已启动 | 是 | ✅ 直接返回成功 |
| start | 已启动 | 否 | ❌ 拒绝启动 |
| connect | 未启动 | 是 | ✅ 启动并建立连接 |
| connect | 未启动 | 否 | ❌ 拒绝 |
| connect | 已启动 | 是 | ✅ 直接连接 |
| connect | 已启动 | 否 | ✅ 直接连接(因为服务已经启动) |
可以看到:
- 启动型(start)对「客户端是否可信」要求一直很严格;
- 连接型(connect)在服务已启动的情况下,非白名单也可以连。
4. 实现一个后台服务(服务端)
先从服务端开始,看怎么写一个最小可用的后台服务。
4.1 新建 Extension 目录和文件
在某个 Module 下:
├── ets
│ ├── MyAppServiceExtAbility
│ │ └── MyAppServiceExtAbility.ets
└── ...
4.2 编写 AppServiceExtensionAbility
核心思路:
-
继承
AppServiceExtensionAbility; -
根据需要实现生命周期回调:
onCreateonRequestonConnectonDisconnectonDestroy
-
若要支持 RPC,就自定义一个继承
rpc.RemoteObject的 Stub。
示例(服务端骨架):
import { AppServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = '[MyAppServiceExtAbility]';
const DOMAIN_NUMBER: number = 0xFF00;
// 自定义 RPC Stub,用于处理客户端消息
class StubTest extends rpc.RemoteObject {
constructor(des: string) {
super(des);
}
onRemoteMessageRequest(
code: number,
data: rpc.MessageSequence,
reply: rpc.MessageSequence,
options: rpc.MessageOption
): boolean | Promise<boolean> {
// TODO: 在这里处理客户端请求数据,并写入回复
return true;
}
}
export default class MyAppServiceExtAbility extends AppServiceExtensionAbility {
onCreate(want: Want): void {
hilog.info(DOMAIN_NUMBER, TAG, `onCreate, want: ${want.abilityName}`);
// 初始化后台任务、启动监控逻辑等
}
// 以 start 方式启动时会回调
onRequest(want: Want, startId: number): void {
hilog.info(DOMAIN_NUMBER, TAG, `onRequest, want: ${want.abilityName}, startId: ${startId}`);
// 根据 need 执行业务
}
// 以 connect 方式连接时会回调,并返回 RemoteObject 给客户端
onConnect(want: Want): rpc.RemoteObject {
hilog.info(DOMAIN_NUMBER, TAG, `onConnect, want: ${want.abilityName}`);
return new StubTest('test');
}
onDisconnect(want: Want): void {
hilog.info(DOMAIN_NUMBER, TAG, `onDisconnect, want: ${want.abilityName}`);
}
onDestroy(): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onDestroy');
}
}
4.3 在 module.json5 中注册
{
"module": {
// ...
"extensionAbilities": [
{
"name": "MyAppServiceExtAbility",
"description": "appService",
"type": "appService", // ★ 必须是 appService
"exported": true, // 对其他应用可见
"srcEntry": "./ets/MyAppServiceExtAbility/MyAppServiceExtAbility.ets",
"appIdentifierAllowList": [
// 在这里配置允许启动/连接该服务的 appIdentifier
]
}
]
}
}
到这里,一个最小后台服务就搭好了。
5. 启动 / 停止后台服务(start 型)
5.1 客户端启动服务
在 UIAbility 页面里,通过 startAppServiceExtensionAbility() 启动:
import { common, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = '[Page_AppServiceExtensionAbility]';
const DOMAIN_NUMBER: number = 0xFF00;
@Entry
@Component
struct Page_AppServiceExtensionAbility {
build() {
Column() {
List() {
ListItem() {
Row() {
Button('启动后台服务')
.onClick(() => {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let want: Want = {
deviceId: '',
bundleName: 'com.samples.stagemodelabilitydevelop',
abilityName: 'MyAppServiceExtAbility'
};
context.startAppServiceExtensionAbility(want)
.then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'start AppServiceExtensionAbility success');
this.getUIContext().getPromptAction().showToast({
message: 'SuccessfullyStartBackendService'
});
})
.catch((err: BusinessError) => {
hilog.error(
DOMAIN_NUMBER,
TAG,
`startAppServiceExtensionAbility failed, code: ${err.code}, msg: ${err.message}`
);
});
})
}
}
}
}
}
}
注意: 以 start 方式启动、且没有任何连接时,该服务进程可能会被系统挂起(参考 Background Tasks Kit),所以要根据业务场景合理设计「多久结束」。
5.2 客户端停止服务
同样在 UIAbility 中,使用 stopAppServiceExtensionAbility():
let want: Want = {
deviceId: '',
bundleName: 'com.samples.stagemodelabilitydevelop',
abilityName: 'MyAppServiceExtAbility'
};
context.stopAppServiceExtensionAbility(want)
.then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'stop AppServiceExtensionAbility success');
this.getUIContext().getPromptAction().showToast({
message: 'SuccessfullyStoppedAStartedBackendService'
});
})
.catch((err: BusinessError) => {
hilog.error(
DOMAIN_NUMBER,
TAG,
`stopAppServiceExtensionAbility failed, code: ${err.code}, msg: ${err.message}`
);
});
5.3 服务端主动停止自身
后台服务自己判断「任务完成」后,可以自杀式结束:
import { AppServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = '[MyAppServiceExtAbility]';
export default class MyAppServiceExtAbility extends AppServiceExtensionAbility {
onCreate(want: Want) {
// 执行完业务后,主动终止
this.context.terminateSelf()
.then(() => {
hilog.info(0x0000, TAG, 'terminateSelf succeed');
})
.catch((error: BusinessError) => {
hilog.error(0x0000, TAG, `terminateSelf failed, code: ${error.code}, msg: ${error.message}`);
});
}
}
6. 连接服务 + RPC 通信(connect 型)
如果后台服务只是「自己悄悄跑」,那只用 start 即可。 但很多场景还需要 客户端和服务端双向通信,这就需要 connectAppServiceExtensionAbility() + rpc.RemoteObject。
6.1 客户端连接服务
import { common, Want } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = '[Page_AppServiceExtensionAbility]';
const DOMAIN_NUMBER: number = 0xFF00;
let connectionId: number;
let want: Want = {
deviceId: '',
bundleName: 'com.samples.stagemodelabilitydevelop',
abilityName: 'MyAppServiceExtAbility'
};
let options: common.ConnectOptions = {
onConnect(elementName, remote: rpc.IRemoteObject): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onConnect callback');
if (!remote) {
hilog.info(DOMAIN_NUMBER, TAG, 'remote is null');
return;
}
// 拿到了 remote,就可以用 RPC 和服务端通信了
},
onDisconnect(elementName): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onDisconnect callback');
},
onFailed(code: number): void {
hilog.info(DOMAIN_NUMBER, TAG, `onFailed callback, code: ${code}`);
}
};
@Entry
@Component
struct Page_AppServiceExtensionAbility {
build() {
Column() {
Button('连接后台服务')
.onClick(() => {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
connectionId = context.connectAppServiceExtensionAbility(want, options);
hilog.info(DOMAIN_NUMBER, TAG, `connectionId: ${connectionId}`);
})
}
}
}
6.2 断开连接
context.disconnectAppServiceExtensionAbility(connectionId)
.then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'disconnectAppServiceExtensionAbility success');
})
.catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `disconnectAppServiceExtensionAbility failed, code: ${err.code}`);
});
注意: 当所有客户端都断开连接后,系统会自动销毁该后台服务。
7. 客户端与服务端的 RPC 通信
7.1 客户端:sendMessageRequest
在 onConnect 中拿到 remote 后,可以用 sendMessageRequest 调用服务:
import { rpc } from '@kit.IPCKit';
import { BusinessError } from '@kit.BasicServicesKit';
const REQUEST_CODE = 1;
let options: common.ConnectOptions = {
onConnect(elementName, remote: rpc.IRemoteObject): void {
if (!remote) return;
let option = new rpc.MessageOption();
let data = new rpc.MessageSequence();
let reply = new rpc.MessageSequence();
// 写入两个整数,交给后台计算
data.writeInt(1);
data.writeInt(2);
remote.sendMessageRequest(REQUEST_CODE, data, reply, option)
.then((ret: rpc.RequestResult) => {
if (ret.errCode === 0) {
let sum = ret.reply.readInt();
hilog.info(DOMAIN_NUMBER, TAG, `sum = ${sum}`);
} else {
hilog.error(DOMAIN_NUMBER, TAG, 'sendRequest failed');
}
})
.catch((error: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `sendRequest error: ${JSON.stringify(error)}`);
});
},
// ...
};
7.2 服务端:onRemoteMessageRequest
import { AppServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = '[MyAppServiceExtAbility]';
const DOMAIN_NUMBER: number = 0xFF00;
class Stub extends rpc.RemoteObject {
onRemoteMessageRequest(
code: number,
data: rpc.MessageSequence,
reply: rpc.MessageSequence,
options: rpc.MessageOption
): boolean | Promise<boolean> {
hilog.info(DOMAIN_NUMBER, TAG, 'onRemoteMessageRequest');
const a = data.readInt();
const b = data.readInt();
const sum = a + b;
reply.writeInt(sum);
return true;
}
}
export default class MyAppServiceExtAbility extends AppServiceExtensionAbility {
onConnect(want: Want): rpc.RemoteObject {
hilog.info(DOMAIN_NUMBER, TAG, 'MyAppServiceExtAbility onConnect');
return new Stub('test');
}
}
到这一步,客户端就能通过 RPC 和后台服务进行比较复杂的交互了,比如:
- 配置下发;
- 实时查询状态;
- 请求后台执行某种动作等。
8. 安全性:如何校验客户端身份?
如果后台服务要提供 敏感能力(比如访问隐私数据、执行关键操作),就必须对客户端做身份校验。
一个常见做法:
- 通过
rpc.IPCSkeleton.getCallingUid()拿到调用方 UID; - 用
bundleManager.getBundleNameByUid()反查出调用方的 bundleName; - 检查 bundleName 是否在自己的“信任名单”中;
- 再通过
getCallingTokenId()+verifyAccessTokenSync()验证权限。
示例:
import { AppServiceExtensionAbility, abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = '[AppServiceExtImpl]';
const DOMAIN_NUMBER: number = 0xFF00;
class Stub extends rpc.RemoteObject {
onRemoteMessageRequest(
code: number,
data: rpc.MessageSequence,
reply: rpc.MessageSequence,
options: rpc.MessageOption
): boolean | Promise<boolean> {
// 1. 通过 UID 获取调用方包名
let callerUid = rpc.IPCSkeleton.getCallingUid();
bundleManager.getBundleNameByUid(callerUid)
.then((callerBundleName) => {
hilog.info(DOMAIN_NUMBER, TAG, `caller bundle: ${callerBundleName}`);
if (callerBundleName !== 'com.samples.stagemodelabilitydevelop') {
// 不在信任名单内,直接拒绝
hilog.info(DOMAIN_NUMBER, TAG, 'Caller not in trust list, reject');
return;
}
// 2. 校验调用方是否具有指定系统权限
let callerTokenId = rpc.IPCSkeleton.getCallingTokenId();
let accessManager = abilityAccessCtrl.createAtManager();
let grantStatus = accessManager.verifyAccessTokenSync(
callerTokenId,
'ohos.permission.GET_BUNDLE_INFO_PRIVILEGED' // 示例权限
);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
hilog.error(DOMAIN_NUMBER, TAG, 'PERMISSION_DENIED');
return;
}
// 3. 通过所有校验,正常执行业务逻辑
hilog.info(DOMAIN_NUMBER, TAG, 'verify access token success');
// ...处理具体请求...
})
.catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `getBundleNameByUid failed: ${err.message}`);
});
return true;
}
}
export default class MyAppServiceExtAbility extends AppServiceExtensionAbility {
onConnect(want: Want): rpc.RemoteObject {
return new Stub('test');
}
}
小建议: 可以把「信任名单」封装成一个独立模块,方便集中管理和审计。
9. 总结:什么时候适合用 AppServiceExtensionAbility?
比较适合的场景:
-
企业级 / 安全类 / 系统类业务:
- DLP、家长控制、企业策略管控、审计日志收集;
-
长时间运行、对 UI 无依赖、但又需要与前台交互的模块:
- 后台同步、规则引擎、持续监控任务等;
-
多个应用需要复用同一后台服务逻辑:
- 通过
appIdentifierAllowList控制可信客户端名单。
- 通过
不太适合的场景:
- 只是偶尔后台跑一下、生命周期短、可以用普通后台任务解决的;
- 强依赖 UI、必须跟窗口绑定的场景(window API 本身就不能用)。