OpenHarmony 后台服务(ServiceExtensionAbility)开发总结

文档概述

说明:

  1. 本文结合 OpenHarmony 官方文档《ServiceExtensionAbility(仅对系统应用开放)》与后台服务工程的开发经验整理而成;
  2. 以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改。

一、前言 & 背景

在很多 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 白名单(参考官方应用特权配置指南):

  1. 先注释 module.json5 中的 extensionAbilities 字段,安装应用;
  2. 获取应用证书指纹:
hdc shell bm dump -n <bundleName> | findstr finger
  1. 在 install_list_capability.json 中添加白名单条目:
{
  "install_list": [
    {
      "bundleName": "com.example.devicemanager",
      "app_signature": ["8E93863FC32EE238060BF69A9B37E2608FFFB21F93C862DD511CBAC9F30024B5"],
      "allowAppUsePrivilegeExtension": true
    }
  ]
}
  1. 推送回设备并重启:
hdc shell mount -o rw,remount /
hdc file send install_list_capability.json /system/etc/app/install_list_capability.json
hdc shell reboot
  1. 重新安装 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;
  }
}

五、服务端对客户端身份校验

服务提供敏感能力时,必须校验客户端身份。官方推荐两种方式:

  1. 通过 callerUid 识别客户端应用:rpc.IPCSkeleton.getCallingUid() + bundleManager.getBundleNameByUid(),适合异步任务场景(getBundleNameByUid 为异步接口,无法同步返回校验结果);
  2. 通过 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;
}

六、编译 & 验证流程

  1. 编译:使用 Full SDK 编译服务端 HAP,产物通过白名单方式安装(见 3.2 节);
  2. 启动服务验证:hdc shell bm dump -n 确认服务已安装;通过 hdc shell hidumper 或 hilog 过滤服务 TAG 查看 onCreate/onConnect 日志;
  3. 连接验证:客户端 App 调 connectServiceExtensionAbility,确认 onConnect 回调触发、代理对象非空;
  4. 功能验证:调用扫描/连接/指令等命令,观察服务端日志与设备响应;
  5. 断连验证:客户端退出或调 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 二进制通道

参考文档

  1. ServiceExtensionAbility(仅对系统应用开放)
  2. 应用特权配置指南(install_list_capability.json)
  3. Full SDK 替换指南
  4. IPC/RPC 开发概述
  5. https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-ipc-kit/js-apis-rpc.md@ohos.rpc (RPC模块)
Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐