OpenHarmony 后台服务(ServiceExtensionAbility)开发总结
OpenHarmony 后台服务(ServiceExtensionAbility)开发总结
文档概述
说明:
- 本文结合 OpenHarmony 官方文档《ServiceExtensionAbility(仅对系统应用开放)》与后台服务工程的开发经验整理而成;
- 以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改。
一、前言 & 背景
在很多 OpenHarmony 产品上,应用需要处理"与 UI 生命周期解耦"的持续型任务,典型场景包括:
- 持续监听网络状态变化(WiFi 连接/断开、热点变化);
- 管理蓝牙设备的发现、连接、指令下发与事件上报;
- 作为系统级能力中心,向多个 App 提供统一的设备管理接口。
如果把这些逻辑放在普通 UIAbility 里,会遇到明显的限制:
- UIAbility 退到后台或被用户划掉后,进程会被冻结甚至销毁,任务随之中断;
- 每个业务 App 各自实现一套设备管理逻辑,重复建设且无法共享状态。
OpenHarmony 官方提供的解决方案是 ServiceExtensionAbility——一种 SERVICE 类型的 ExtensionAbility 后台服务组件。它拥有独立进程、独立生命周期,通过 RPC 向外部提供能力,是官方唯一的"后台服务"载体。本文以一个"设备管理后台服务"示例工程为实例,完整走通"定义能力 → 实现服务 → 注册配置 → 客户端连接 → 安全校验"的开发链路。
二、后台服务基本概念
2.1 ServiceExtensionAbility 是什么
ServiceExtensionAbility 是 SERVICE 类型的 ExtensionAbility 组件,提供后台服务能力。它内部持有一个 ServiceExtensionContext,通过该 Context 提供丰富的接口供外部使用。本文称被启动的 ServiceExtensionAbility 为服务端,启动/连接它的组件为客户端。
2.2 启动 vs 连接(两种运行形式)
ServiceExtensionAbility 支持以"启动"和"连接"两种形式运行,区别非常关键:
| 形式 | 接口 | 关联关系 | 退出规则 |
|---|---|---|---|
| 启动 | startServiceExtensionAbility()(仅系统应用) | 弱关联:客户端退出后服务可继续存在 | 不会自动退出,需 stopServiceExtensionAbility() 或自身 terminateSelf() |
| 连接 | connectServiceExtensionAbility() | 强关联:客户端退出后服务一起退出 | 所有连接断开后,服务自动退出 |
需要注意的细节:
- 服务只通过 connect 方式被拉起时,生命周期受客户端控制——客户端调用一次 connect 建立一个连接,客户端退出或调用 disconnectServiceExtensionAbility() 后连接断开,全部断开后服务自动退出;
- 服务一旦通过 start 方式拉起,将不会自动退出,完成后需及时销毁避免资源浪费;
- 只能在主线程执行 connect/disconnect 操作,不要在 Worker、TaskPool 等子线程中执行。
2.3 生命周期
服务端提供 5 个生命周期回调:
- onCreate:服务首次创建时触发,适合做初始化(注册监听、初始化安全模块等);服务已创建时再次启动/连接不会重复触发;
- onRequest:被 startServiceExtensionAbility() 启动时触发,每调用一次均触发;
- onConnect:被 connectServiceExtensionAbility() 连接时触发,必须在此返回远端代理对象(rpc.IRemoteObject),客户端拿到后即可进行 RPC 通信;后续再有组件连接,系统会直接返回已保存的代理对象而不再触发该回调;
- onDisconnect:最后一个连接断开时触发;
- onDestroy:服务销毁时触发,在此清理资源。
2.4 约束与限制
- 当前不支持三方应用实现 ServiceExtensionAbility,仅系统应用可开发;三方应用只能连接系统提供的服务(且需在前台获焦时才能连接);
- ServiceExtensionAbility 相关接口为 System-API,需要替换 Full SDK 才能开发编译;
- 需要申请 AllowAppUsePrivilegeExtension 特权(install_list_capability.json 白名单配置)。
三、开发环境与前置准备
| 项目 | 说明 |
|---|---|
| 系统版本 | OpenHarmony 5.0+(标准系统) |
| 开发语言 | ArkTS (ETS) |
| 编译环境 | DevEco Studio |
| 涉及模块 | Ability Kit、IPC Kit(RPC)、bundle 管理 |
3.1 替换 Full SDK
ServiceExtensionAbility 相关接口被标记为 System-API,默认对开发者隐藏。需从镜像站点获取 Full SDK,并在 DevEco Studio 中替换,具体参考官方替换指南。
3.2 申请应用特权(安装白名单)
type: "service" 的 ExtensionAbility 属于特权扩展,普通方式安装会失败。需要把应用加入设备 install_list_capability.json 白名单(参考官方应用特权配置指南):
- 先注释 module.json5 中的 extensionAbilities 字段,安装应用;
- 获取应用证书指纹:
hdc shell bm dump -n <bundleName> | findstr finger
- 在 install_list_capability.json 中添加白名单条目:
{
"install_list": [
{
"bundleName": "com.example.devicemanager",
"app_signature": ["8E93863FC32EE238060BF69A9B37E2608FFFB21F93C862DD511CBAC9F30024B5"],
"allowAppUsePrivilegeExtension": true
}
]
}
- 推送回设备并重启:
hdc shell mount -o rw,remount /
hdc file send install_list_capability.json /system/etc/app/install_list_capability.json
hdc shell reboot
- 重新安装 HAP。
四、实现一个后台服务
4.1 服务端:创建 ServiceExtensionAbility
在工程 Module 的 ets 目录下新建 ServiceExtAbility 目录,创建 DeviceServiceExtAbility.ets,继承 ServiceExtensionAbility 并实现生命周期回调,在 onConnect 中返回 RPC Stub 对象:
import Want from '@ohos.app.ability.Want';
import ServiceExtensionAbility from '@ohos.app.ability.ServiceExtensionAbility';
import deviceInfo from '@ohos.deviceInfo';
import { DeviceManageService } from '../idl/DeviceManageService';
import { SecurityManager, NetStatusManager, LogUtil } from '../common/Utils';
const TAG: string = 'DeviceServiceExtAbility';
export default class DeviceServiceExtAbility extends ServiceExtensionAbility {
onCreate(want: Want) {
LogUtil.info(TAG, 'onCreate called');
// 其他初始化:读取公共配置、恢复上次运行状态等
}
onRequest(want: Want, startId: number) {
LogUtil.info(TAG, 'onRequest called');
}
async onConnect(want: Want) {
// 从 want.parameters 读取系统注入的调用方信息,用于后续鉴权与统计
let callerBundleName: string = want.parameters['ohos.aafwk.param.callerBundleName'] as string;
LogUtil.info(TAG, 'callerBundleName: ' + callerBundleName);
// 返回 RPC Stub,客户端获取后即可进行通信
let binder = new DeviceManageService();
return binder;
}
onDisconnect(want: Want) {
LogUtil.info(TAG, 'onDisconnect called');
}
onDestroy() {
NetStatusManager.getInstance().resetInstance();
LogUtil.info(TAG, 'onDestroy');
}
}
4.2 注册配置(module.json5)
在 Module 的 module.json5 中注册,type 必须为 "service",exported 置为 true 才允许其他应用连接:
{
"module": {
"name": "entry",
"mainElement": "DeviceServiceExtAbility",
"extensionAbilities": [
{
"name": "DeviceServiceExtAbility",
"srcEntry": "./ets/ServiceExtAbility/DeviceServiceExtAbility.ets",
"type": "service",
"exported": true,
"description": "device manager background service"
}
]
}
}
4.3 定义 RPC 接口(服务端 Stub)
服务端 Stub 继承 rpc.RemoteObject,通过 onRemoteMessageRequest 接收客户端的命令并分发处理。示例工程按命令码维护了同步/异步两张分发表:
import rpc from '@ohos.rpc';
import { LogUtil } from '../common/Utils';
const TAG: string = 'DeviceManageService';
const DESCRIPTOR: string = 'DeviceManageService';
/**
* 命令码定义(客户端与服务端必须保持一致)
*/
export enum CommandCode {
COMMAND_CONNECT_DEVICE = 10001, // 连接设备(同步)
COMMAND_SEND_COMMAND = 10002, // 下发指令(同步)
COMMAND_DISCONNECT_DEVICE = 10003, // 断开设备(同步)
COMMAND_START_SCAN = 20001, // 开始扫描(异步,结果回调上报)
COMMAND_SUBSCRIBE_EVENT = 20002 // 订阅设备事件(异步)
}
export class DeviceManageService extends rpc.RemoteObject {
// 同步命令表:处理完成后通过 reply 序列同步返回结果
private mSyncEventMap = new Map<number, Function>([
[CommandCode.COMMAND_CONNECT_DEVICE, this.startConnectDevice],
[CommandCode.COMMAND_SEND_COMMAND, this.startSendCommand],
[CommandCode.COMMAND_DISCONNECT_DEVICE, this.executeDisconnectDevice]
]);
// 异步命令表:结果稍后通过客户端传入的回调对象反向推送
private mAsyncEventMap = new Map<number, Function>([
[CommandCode.COMMAND_START_SCAN, this.startScanDevice],
[CommandCode.COMMAND_SUBSCRIBE_EVENT, this.subscribeDeviceEvent]
]);
constructor() {
super(DESCRIPTOR);
}
async onRemoteMessageRequest(code: number, data: rpc.MessageSequence,
reply: rpc.MessageSequence, option: rpc.MessageOption): Promise<boolean> {
// 1. InterfaceToken 校验:调用方写入的 descriptor 必须与自身一致
let descriptor = data.readInterfaceToken();
if (descriptor !== this.getDescriptor()) {
LogUtil.error(TAG, 'check interface token fail, code: ' + code);
return false;
}
// 2. 按命令码分发:异步命令读取回调对象后交给业务,同步命令等待业务结果
if (this.mAsyncEventMap.has(code)) {
this.handleAsyncEvent(code, data, option);
return true;
}
if (this.mSyncEventMap.has(code)) {
return this.handleSyncEvent(code, data, reply);
}
LogUtil.warn(TAG, 'invalid request code: ' + code);
return false;
}
private async handleSyncEvent(code: number, data: rpc.MessageSequence,
reply: rpc.MessageSequence): Promise<boolean> {
let param: string = data.readString();
let replyStr: string = '';
let replyCode: number = 0;
try {
let func: Function | undefined = this.mSyncEventMap.get(code);
if (func) {
replyStr = await func(this, param); // 执行业务并等待结果
}
} catch (err) {
replyCode = err?.code ?? -1;
replyStr = err?.data ?? '';
}
reply.writeInt(replyCode); // 写错误码
this.writeResponseData(reply, replyStr); // 写业务数据
return true;
}
private handleAsyncEvent(code: number, data: rpc.MessageSequence, option: rpc.MessageOption) {
let remoteObj: rpc.IRemoteObject | null = null;
if (option.isAsync()) {
remoteObj = data.readRemoteObject(); // 取出客户端回调对象
}
let param: string = data.readString();
let func: Function | undefined = this.mAsyncEventMap.get(code);
if (func) {
func(this, param, remoteObj);
}
}
/**
* 应答数据协议:writeBoolean(是否超长) + 超长走 writeRawData,否则 writeString
*/
private writeResponseData(sequence: rpc.MessageSequence, data: string): void {
let needSendRawData = data.length >= 35 * 1024; // 超过 35KB 走二进制通道
sequence.writeBoolean(needSendRawData);
if (needSendRawData) {
let bytes: number[] = this.stringToBytes(data);
sequence.writeInt(bytes.length);
sequence.writeRawData(bytes, bytes.length);
} else {
sequence.writeString(data);
}
}
/**
* 异步结果推送:服务端反向调用客户端回调对象
*/
public sendResponseMessage(code: number, error: number, response: string,
obj: rpc.IRemoteObject): void {
if (!obj || obj.isObjectDead()) {
LogUtil.warn(TAG, 'callback obj is invalid or dead');
return;
}
let option: rpc.MessageOption = new rpc.MessageOption();
option.setAsync(true);
let resultSequence = rpc.MessageSequence.create();
let replySequence = rpc.MessageSequence.create();
try {
resultSequence.writeInterfaceToken(obj.getDescriptor());
resultSequence.writeInt(error);
this.writeResponseData(resultSequence, response);
obj.sendMessageRequest(code, resultSequence, replySequence, option);
} finally {
resultSequence.reclaim();
replySequence.reclaim();
}
}
private stringToBytes(str: string): number[] {
let encoder = new util.TextEncoder();
return Array.from(encoder.encodeInto(str));
}
// ============ 业务实现示例 ============
public startConnectDevice(service: DeviceManageService, param: string): Promise<string> {
// 解析参数、调用底层连接逻辑(蓝牙/网络等),返回连接结果
return Promise.resolve('connect success');
}
public startSendCommand(service: DeviceManageService, param: string): Promise<string> {
// 下发控制指令,等待设备响应
return Promise.resolve('command response: {}');
}
public executeDisconnectDevice(service: DeviceManageService, param: string): string {
return 'disconnect success';
}
public startScanDevice(service: DeviceManageService, param: string, obj: rpc.IRemoteObject): void {
// 发起扫描,扫描结果通过 service.sendResponseMessage 推回客户端
}
public subscribeDeviceEvent(service: DeviceManageService, param: string, obj: rpc.IRemoteObject): void {
// 订阅设备事件,设备数据上报时通过回调对象推送给客户端
}
}
关键点:
- descriptor 校验:客户端 writeInterfaceToken() 写入服务端 descriptor,服务端 readInterfaceToken() 比对,防止陌生调用方直连;
- 同步应答协议:writeInt(错误码) + writeBoolean(是否超长) + 超长(>35KB)走 writeRawData 二进制,否则 writeString;
- 异步推送:服务端通过客户端传入的 RemoteObject 回调 sendResponseMessage 反向调用,实现事件订阅上报。
4.4 客户端:连接后台服务
客户端通过 connectServiceExtensionAbility(want, options) 建立连接,onConnect 中拿到远端代理:
import common from '@ohos.app.ability.common';
import rpc from '@ohos.rpc';
import { DeviceManageServiceProxy } from '../rpc/DeviceManageServiceProxy';
import { LogUtil } from '../common/Utils';
const TAG: string = 'ServiceConnector';
export class ServiceConnector {
private mContext: common.UIAbilityContext | null = null;
private mConnection: number = -1;
private mProxy: DeviceManageServiceProxy | null = null;
private mConnectOptions: common.ConnectOptions | null = null;
public connectService(context: common.UIAbilityContext): number {
this.mContext = context;
let want: Want = {
bundleName: 'com.example.devicemanager', // 服务端包名
abilityName: 'DeviceServiceExtAbility', // 服务端扩展名
// deviceId 缺省/空串表示连接本设备服务;跨设备需填对端 networkId
};
this.mConnectOptions = {
onConnect: (elementName, remoteProxy: rpc.IRemoteObject) => {
LogUtil.info(TAG, 'onConnect called');
// 拿到服务端返回的 RemoteObject,封装为业务代理
this.mProxy = new DeviceManageServiceProxy(remoteProxy);
},
onDisconnect: (elementName) => {
LogUtil.info(TAG, 'onDisconnect called');
this.mProxy = null;
// 断连后可在此触发自动重连
},
onFailed: (code: number) => {
LogUtil.error(TAG, 'connect failed, code is ' + code);
}
};
this.mConnection = this.mContext.connectServiceExtensionAbility(want, this.mConnectOptions);
return this.mConnection;
}
public disconnectService(): void {
if (this.mConnection !== -1 && this.mContext) {
this.mContext.disconnectServiceExtensionAbility(this.mConnection);
this.mConnection = -1;
this.mProxy = null;
}
}
public getProxy(): DeviceManageServiceProxy | null {
return this.mProxy;
}
}
工程实践建议:
- 客户端封装单例管理器,统一维护连接状态与代理对象,其他业务模块只面向代理调用;
- 断连自动重连:onDisconnect 触发后按 100ms 间隔最多重试 3 次,提升服务稳定性;
- 连接中的请求放入观察者队列,onConnect 成功后统一回调,避免业务在未连接时误调用。
4.5 消息序列的参数布局
客户端发送请求时按固定顺序写入(顺序是两端约定的协议,服务端必须按相同顺序读取):
import rpc from '@ohos.rpc';
export class DeviceManageServiceProxy extends rpc.RemoteObject {
private mProxy: rpc.IRemoteObject | null = null;
private mDescriptor: string = '';
constructor(proxy: rpc.IRemoteObject) {
this.mProxy = proxy;
try {
this.mDescriptor = proxy.getDescriptor(); // 被调接口的 descriptor
} catch (e) {
LogUtil.warn(TAG, 'getDescriptor fail');
}
}
private async sendRequestMessage(code: number, data: string | null, isAsync: boolean,
object: rpc.IRemoteObject | null = null): Promise<rpc.RequestResult | null> {
let _option = new rpc.MessageOption();
let _data = rpc.MessageSequence.create();
let _reply = rpc.MessageSequence.create();
_data.writeInterfaceToken(this.mDescriptor); // ① 被调接口的 descriptor
if (isAsync && object !== null) {
_data.writeRemoteObject(object); // ② 异步请求:客户端回调对象
}
_option.setAsync(isAsync); // ③ 标记同步/异步
_data.writeString(data ?? ''); // ④ 业务 JSON 参数字符串(空则写空串,保证解析统一)
if (!this.mProxy) {
return null;
}
return await this.mProxy.sendMessageRequest(code, _data, _reply, _option);
}
// 同步命令示例:连接设备
public async connectDevice(param: string): Promise<string> {
let result = await this.sendRequestMessage(CommandCode.COMMAND_CONNECT_DEVICE, param, false);
return this.parseRequestResult(result);
}
// 异步命令示例:订阅设备事件
public subscribeDeviceEvent(param: string, callback: rpc.IRemoteObject): void {
this.sendRequestMessage(CommandCode.COMMAND_SUBSCRIBE_EVENT, param, true, callback);
}
private parseRequestResult(result: rpc.RequestResult | null): string {
if (!result || result.errCode !== 0) {
return '';
}
let errCode: number = result.reply.readInt(); // 读错误码
let needReadRawData: boolean = result.reply.readBoolean(); // 是否二进制
let returnValue: string = '';
if (needReadRawData) {
let size: number = result.reply.readInt();
returnValue = this.bytesToString(result.reply.readRawData(size));
} else {
returnValue = result.reply.readString();
}
result.reply.reclaim();
return returnValue;
}
}
五、服务端对客户端身份校验
服务提供敏感能力时,必须校验客户端身份。官方推荐两种方式:
- 通过 callerUid 识别客户端应用:rpc.IPCSkeleton.getCallingUid() + bundleManager.getBundleNameByUid(),适合异步任务场景(getBundleNameByUid 为异步接口,无法同步返回校验结果);
- 通过 callerTokenId 鉴权:rpc.IPCSkeleton.getCallingTokenId() + abilityAccessCtrl.verifyAccessTokenSync() 校验客户端是否持有某权限。
在此基础上,示例工程进一步实现了签名级白名单校验:不仅校验包名,还通过 getBundleInfo 的 signatureInfo.appIdentifier(应用签名标识)比对白名单,防止同名伪造:
import bundleManager from '@ohos.bundle.bundleManager';
import { LogUtil } from '../common/Utils';
export class AuthorizeManager {
public static readonly NOT_AUTHORIZED = 0;
public static readonly NORMAL_AUTHORIZED = 1;
// 受信客户端:包名 -> 应用签名标识
private static readonly TRUST_LIST: Map<string, string> = new Map([
['com.example.clientapp', '6917559050239470646'],
['com.example.controlapp', '5765880207856345975']
]);
private static isInTrustList(bundleName: string, appIdentifier: string): boolean {
if (!AuthorizeManager.TRUST_LIST.has(bundleName)) {
return false;
}
return AuthorizeManager.TRUST_LIST.get(bundleName) === appIdentifier;
}
public static async checkAuthorizedByUid(uid: number): Promise<number> {
try {
let callerBundleName: string = bundleManager.getBundleNameByUidSync(uid);
let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_SIGNATURE_INFO;
let bundleInfo = await bundleManager.getBundleInfo(callerBundleName, bundleFlags);
return AuthorizeManager.isInTrustList(callerBundleName, bundleInfo.signatureInfo.appIdentifier) ?
AuthorizeManager.NORMAL_AUTHORIZED : AuthorizeManager.NOT_AUTHORIZED;
} catch (err) {
LogUtil.error(TAG, 'checkAuthorizedByUid failed. Cause: ' + err?.message);
}
return AuthorizeManager.NOT_AUTHORIZED;
}
}
服务端在 onRemoteMessageRequest 中对每个请求先做 Token 校验 + UID 授权校验,未授权则返回 COMMON_FINGERPRINT_INVALID 拒绝,实现"接口级 + 身份级"双重防线:
// 在 onRemoteMessageRequest 中增加鉴权:
let callingUid: number = rpc.IPCSkeleton.getCallingUid();
let authored: number = await AuthorizeManager.checkAuthorizedByUid(callingUid);
if (authored === AuthorizeManager.NOT_AUTHORIZED) {
LogUtil.warn(TAG, 'callingUid: ' + callingUid + ' is not authorized, code: ' + code);
this.replyAuthorizedState(code, data, reply, option);
return true;
}
六、编译 & 验证流程
- 编译:使用 Full SDK 编译服务端 HAP,产物通过白名单方式安装(见 3.2 节);
- 启动服务验证:hdc shell bm dump -n 确认服务已安装;通过 hdc shell hidumper 或 hilog 过滤服务 TAG 查看 onCreate/onConnect 日志;
- 连接验证:客户端 App 调 connectServiceExtensionAbility,确认 onConnect 回调触发、代理对象非空;
- 功能验证:调用扫描/连接/指令等命令,观察服务端日志与设备响应;
- 断连验证:客户端退出或调 disconnectServiceExtensionAbility,观察服务端 onDisconnect 与自动退出行为。
七、常见问题与踩坑总结
| 现象 | 原因 | 解决方案 |
|---|---|---|
| onFailed 返回 201(权限校验失败) | 跨设备连接缺少 ohos.permission.DISTRIBUTED_DATASYNC;设备未组网;deviceId 无效 | 声明权限并运行时授权,用 getAvailableDeviceListSync() 获取真实 networkId;本设备连接不传 deviceId |
| 服务 HAP 安装失败 | type: "service" 扩展需要特权,普通安装被拒 | 按 3.2 节加入 install_list_capability.json 白名单后重装 |
| 编译报 System-API 未定义 | 使用了公开 SDK,System-API 被隐藏 | 替换 Full SDK |
| 客户端连接后服务频繁退出 | connect 形式为强关联,客户端进程被杀/退出导致全部连接断开 | 客户端维护连接生命周期,需要常驻时改用 start 方式(系统应用)并配合 terminateSelf() 管理 |
| onConnect 只触发一次 | 系统缓存了已返回的代理对象,后续连接直接复用 | 属正常行为;如需感知新客户端,可在请求头携带调用方信息(ohos.aafwk.param.callerUid 等) |
| RPC 报文解析错位 | 两端写入/读取顺序不一致 | 严格按 InterfaceToken → 回调对象 → 参数串顺序读写,字段变更需两端同步 |
| 大字符串传输失败 | 超过序列化大小限制(200KB) | 业务数据尽量精简;可约定超过 35KB 走 writeRawData 二进制通道 |
参考文档
更多推荐
所有评论(0)