HarmonyOS IPC/RPC 实战:用 ArkTS 跑通 Proxy--Stub 整条链路
前一篇我写了一个偏概念向的 IPC Kit 笔记,这一篇就直接上"实战视角":
怎么从 ServiceExtensionAbility + Stub 到 UIAbility + Proxy,把 IPC / RPC 整条链路用 ArkTS 跑起来。
官方文档那一套"PC 与 RPC 通信开发指导(ArkTS)"我基本按行啃了一遍,这里用我自己的理解,顺一遍完整流程,顺便夹带一点实际开发时的习惯和小坑提示。
1. 先把场景说白:我们到底在干嘛?
IPC / RPC 做的事情本质只有一件:
在两个进程之间,建立一对 Proxy--Stub,让客户端像调本地方法一样,去调服务端的逻辑。
- Proxy 在客户端进程里,暴露方法给你调用;
- Stub 在服务端进程里,负责收包、解包、分发、回包;
- 底层走的是 IPC(Binder)或者 RPC(软总线),对我们来说就是
sendMessageRequest那一发。
官方还有一句约束非常关键(一定要有印象):
- 当前 不支持三方应用自己实现 ServiceExtensionAbility;
- 三方应用更多是当 客户端 UIAbility ,通过
Context去连接 系统应用/服务 暴露出来的 ServiceExtensionAbility。
也就是说,这篇文里的"服务端"那部分代码,更偏 "系统服务 / Sample 工程" 的视角。对我们这种三方应用来说,把服务端那块 看懂 就已经很有价值了。
2. 总流程我先用大白话捋一遍
一条典型 IPC/RPC 链路,大概长这样:
- 服务端:
- 写一个
ServiceExtensionAbility; - 在里面 new 一个继承自
rpc.RemoteObject的 Stub; - 在 Stub 里重写
onRemoteMessageRequest,处理各种请求; onConnect的时候把 Stub 返回给客户端。
- 写一个
- 客户端:
- 准备好一个
Want(指明 package + abilityName,RPC 场景还要带deviceId); - 准备好一个
ConnectOptions(onConnect / onDisconnect / onFailed); - 调
connectAbility或connectServiceExtensionAbility,拿到proxy; - 构造
MessageSequence,写入参数,调用proxy.sendMessageRequest(...); - 拿到
result.reply,从里面把服务端写回来的结果读出来。
- 准备好一个
- 用完记得断开:
disconnectAbility(connectId, callback)或
disconnectServiceExtensionAbility(connectId)。
下面按这个顺序展开。
3. 服务端:ServiceExtensionAbility + Stub 怎么写?
3.1 工程结构
官方示例里大概是这样一个目录:
├── ets
│ ├── ServiceExtAbility
│ │ ├── ServiceExtAbility.ets
└── ...
ServiceExtAbility.ets 里做两件事:
- 写一个继承
ServiceExtensionAbility的类(后台服务本体); - 写一个继承
rpc.RemoteObject的 Stub 类(真正处理 IPC 请求的家伙)。
3.2 Stub:服务端"收发中心"
核心代码长这样(我稍微标了点注释):
import { ServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
// 1. 定义服务端 Stub
class Stub extends rpc.RemoteObject {
constructor(descriptor: string) {
super(descriptor);
}
// 2. 处理客户端请求的入口
onRemoteMessageRequest(
code: number,
data: rpc.MessageSequence,
reply: rpc.MessageSequence,
option: rpc.MessageOption
): boolean | Promise<boolean> {
switch (code) {
case 1:
// 读客户端写进来的数据(顺序一定要对得上)
const clientStr = data.readString();
hilog.info(0x0000, 'testTag', `Stub receive: ${clientStr}`);
// 写回给客户端的结果
reply.writeString('huichuanxinxi'); // 这里只是示例字符串
return true;
default:
hilog.info(0x0000, 'testTag', 'Stub unknown code: ' + code);
return false;
}
}
}
几个点我自己记得比较牢:
code就是"方法编号" :以后要扩服务,就多定义几个case,比如1=发字符串、2=做加法、3=查配置......- 读写顺序必须对应 :
- 客户端写什么 & 写的顺序;
- 服务端就按相同顺序读回来;
- 返回
true/false表示这次请求有没有被正常处理。
如果后面逻辑比较复杂、需要异步,可以直接 return Promise<boolean>,ArkTS 这边是支持的。
3.3 ServiceExtensionAbility:把 Stub 暴露出去
服务端本体就是继承 ServiceExtensionAbility,几个生命周期都很标准:
export default class ServiceAbility extends ServiceExtensionAbility {
onCreate(want: Want): void {
hilog.info(0x0000, 'testTag', 'onCreate');
}
onRequest(want: Want, startId: number): void {
hilog.info(0x0000, 'testTag', 'onRequest');
}
onConnect(want: Want): rpc.RemoteObject {
hilog.info(0x0000, 'testTag', 'onConnect');
// 关键:这里返回 Stub,客户端才能拿到 Proxy
return new Stub('rpcTestAbility');
}
onDisconnect(want: Want): void {
hilog.info(0x0000, 'testTag', 'onDisconnect');
}
onDestroy(): void {
hilog.info(0x0000, 'testTag', 'onDestroy');
}
}
真正的"连接动作"其实就在 onConnect:
- 客户端连上来的时候,这里会被调;
- 你 new 一个 Stub 返回出去;
- 客户端那边就能拿到对应的 Proxy。
4. 客户端(IPC 场景):本机进程连接服务
4.1 准备 want + connect
import { Want, common } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
let proxy: rpc.IRemoteObject | undefined = undefined;
const want: Want = {
// 换成实际包名 / 组件名
bundleName: 'ohos.rpc.test.server',
abilityName: 'ohos.rpc.test.server.ServiceAbility',
};
const connect: common.ConnectOptions = {
onConnect: (elementName, remoteProxy) => {
hilog.info(0x0000, 'testTag', 'RpcClient: js onConnect called');
proxy = remoteProxy; // 这里就拿到了 Proxy
},
onDisconnect: (elementName) => {
hilog.info(0x0000, 'testTag', 'RpcClient: onDisconnect');
proxy = undefined;
},
onFailed: () => {
hilog.info(0x0000, 'testTag', 'RpcClient: onFailed');
},
};
要点:
want决定你连的是谁;connect是整条链路的回调入口 :- 成功拿 Proxy;
- 断开 / 失败时做清理。
5. 客户端(RPC 场景):跨设备多一步拿 NetworkId
跨设备就多了一步:用 distributedDeviceManager 先把目标设备找出来。
import { Want, common } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
let dmInstance: distributedDeviceManager.DeviceManager | undefined;
let proxy: rpc.IRemoteObject | undefined;
let deviceList: Array<distributedDeviceManager.DeviceBasicInfo> | undefined;
let networkId: string | undefined;
let want: Want | undefined;
let connect: common.ConnectOptions | undefined;
try {
dmInstance = distributedDeviceManager.createDeviceManager('ohos.rpc.test');
} catch (error) {
const err = error as BusinessError;
hilog.error(
0x0000,
'testTag',
'createDeviceManager errCode:' + err.code + ', errMessage:' + err.message,
);
}
if (dmInstance !== undefined) {
try {
deviceList = dmInstance.getAvailableDeviceListSync();
if (deviceList.length !== 0) {
networkId = deviceList[0].networkId;
want = {
bundleName: 'ohos.rpc.test.server',
abilityName: 'ohos.rpc.test.service.ServiceAbility',
deviceId: networkId, // 关键:跨设备要带 deviceId
};
connect = {
onConnect: (elementName, remoteProxy) => {
hilog.info(0x0000, 'testTag', 'RpcClient: js onConnect called');
proxy = remoteProxy;
},
onDisconnect: (elementName) => {
hilog.info(0x0000, 'testTag', 'RpcClient: onDisconnect');
},
onFailed: () => {
hilog.info(0x0000, 'testTag', 'RpcClient: onFailed');
},
};
}
} catch (error) {
const err = error as BusinessError;
hilog.error(0x0000, 'testTag', 'createDeviceManager err:' + err);
}
}
RPC 相比 IPC 多注意两点:
- 设备列表可能为空(对端没在线 / 没发现);
deviceId一定要传对,不然你压根连不到服务端。
6. 建立连接:FA 模型 vs Stage 模型
这里就是老生常谈的两套模型的差异了。
6.1 FA 模型:featureAbility.connectAbility
import { featureAbility } from '@kit.AbilityKit';
// 建立连接后返回的 connectId 记得保存,断开要用
const connectId = featureAbility.connectAbility(want, connect);
6.2 Stage 模型:UIAbilityContext.connectServiceExtensionAbility
在 Stage 模型里,一般需要先拿到 UIAbilityContext:
let context: common.UIAbilityContext = this.getUIContext().getHostContext(); // UIAbilityContext
// 建立连接,拿到 connectId
const connectId = context.connectServiceExtensionAbility(want, connect);
我自己的习惯是:connectId 直接丢到类成员里,统一管理,不要写成局部变量,后面断开连接会用到。
7. 客户端发消息:sendMessageRequest 一次走通
拿到 proxy 后,就可以构造消息了。
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
let proxy: rpc.IRemoteObject | undefined;
// 发送字符串示例
function sendStrToServer() {
if (!proxy) {
hilog.error(0x0000, 'testTag', 'proxy is undefined');
return;
}
const option = new rpc.MessageOption();
const data = rpc.MessageSequence.create();
const reply = rpc.MessageSequence.create();
// 按顺序写入参数,这里的示例是一个字符串
data.writeString('hello world');
proxy
.sendMessageRequest(1, data, reply, option)
.then((result: rpc.RequestResult) => {
if (result.errCode !== 0) {
hilog.error(
0x0000,
'testTag',
'sendMessageRequest failed, errCode: ' + result.errCode,
);
return;
}
// 从 result.reply 里读取结果
const serverStr = result.reply.readString();
hilog.info(0x0000, 'testTag', 'receive from server: ' + serverStr);
})
.catch((e: Error) => {
hilog.error(0x0000, 'testTag', 'sendMessageRequest got exception: ' + e);
})
.finally(() => {
// 用完一定要释放
data.reclaim();
reply.reclaim();
});
}
几个点我个人会特别注意:
-
Proxy 判空 :
把
proxy抽成一个封装类,所有调用前统一做if (!proxy)的检查,会比到处判空干净很多。 -
code推荐用常量 / enum 管理:enum RpcCode { SEND_STRING = 1, // FUTURE_ADD = 2, }后面服务越写越多的时候,不然你到处都是裸
1, 2, 3,很难维护。 -
reclaim()必须记得调 :
MessageSequence用完就回收,不然时间一长容易出问题。
8. 服务端再看一眼:onRemoteMessageRequest 的套路
官方示例服务端的处理逻辑其实非常直给:
class Stub extends rpc.RemoteObject {
constructor(descriptor: string) {
super(descriptor);
}
onRemoteMessageRequest(
code: number,
data: rpc.MessageSequence,
reply: rpc.MessageSequence,
option: rpc.MessageOption
): boolean | Promise<boolean> {
if (code === 1) {
const str = data.readString();
hilog.info(0x0000, 'testTag', 'stub receive str : ' + str);
// 回写给客户端
reply.writeString('hello rpc');
return true;
} else {
hilog.info(0x0000, 'testTag', 'stub unknown code: ' + code);
return false;
}
}
}
以后如果要扩:
- 可以把不同 code 的逻辑拆成独立方法;
- 或者搞一个
Map<number, Handler>做分发表; - 如果有耗时操作,可以直接
return new Promise<boolean>(...),配合async/await写清晰点。
9. 断开连接:别忘了给系统"说再见"
通信结束后,记得把连接解绑,不然服务端那边会认为你还挂着。
9.1 FA 模型:featureAbility.disconnectAbility
import { featureAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
function disconnectCallback() {
hilog.info(0x0000, 'testTag', 'disconnect ability done');
}
// 使用之前保存下来的 connectId
featureAbility.disconnectAbility(connectId, disconnectCallback);
9.2 Stage 模型:UIAbilityContext.disconnectServiceExtensionAbility
let context: common.UIAbilityContext = this.getUIContext().getHostContext(); // UIAbilityContext
context.disconnectServiceExtensionAbility(connectId);
我自己的做法是:在 UIAbility 的某个生命周期(比如 onForeground/onBackground 或页面退出时机)做统一的 disconnect,确保不会有长时间闲置的连接。
10. 最后一点个人总结
把这篇文拆开看的话,其实就是几步:
- 理解"Proxy--Stub 对"的角色分工;
- 服务端:写一个 Stub,并在 ServiceExtensionAbility 的
onConnect里把它返回出去; - 客户端:准备 want + connect,连上之后拿到 Proxy;
- 调用
sendMessageRequest,用MessageSequence完成参数的读写; - 最后别忘断开连接、回收
MessageSequence。
对我自己来说,这一套走通之后,再回头看官方文档,就不会只停留在"复制粘贴示例代码",而是能在脑子里有一张完整的"消息从 UI 按钮点击 → 到对端服务处理 → 再回来的 流程图"。