HarmonyOS NEXT——使用 AppServiceExtensionAbility 实现真正的后台服务

79 阅读5分钟

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 做客户端。

客户端可以通过两种方式使用后台服务:

  1. startAppServiceExtensionAbility() —— 启动型
  2. connectAppServiceExtensionAbility() —— 连接型

这两种方式的区别稍后细讲,这里先看「谁有资格拉起服务」。

3.1 谁可以启动 / 连接服务?

关键在 module.json5extensionAbilities 的配置项:

 "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

  • 根据需要实现生命周期回调:

    • onCreate
    • onRequest
    • onConnect
    • onDisconnect
    • onDestroy
  • 若要支持 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. 安全性:如何校验客户端身份?

如果后台服务要提供 敏感能力(比如访问隐私数据、执行关键操作),就必须对客户端做身份校验。

一个常见做法:

  1. 通过 rpc.IPCSkeleton.getCallingUid() 拿到调用方 UID;
  2. bundleManager.getBundleNameByUid() 反查出调用方的 bundleName;
  3. 检查 bundleName 是否在自己的“信任名单”中;
  4. 再通过 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 本身就不能用)。