HarmonyOS IPC/RPC 实战:用 ArkTS 跑通 Proxy–Stub 整条链路

HarmonyOS IPC/RPC 实战:用 ArkTS 跑通 Proxy--Stub 整条链路

前一篇我写了一个偏概念向的 IPC Kit 笔记,这一篇就直接上"实战视角":

怎么从 ServiceExtensionAbility + StubUIAbility + 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 链路,大概长这样:

  1. 服务端:
    • 写一个 ServiceExtensionAbility
    • 在里面 new 一个继承自 rpc.RemoteObject 的 Stub;
    • 在 Stub 里重写 onRemoteMessageRequest,处理各种请求;
    • onConnect 的时候把 Stub 返回给客户端。
  2. 客户端:
    • 准备好一个 Want(指明 package + abilityName,RPC 场景还要带 deviceId);
    • 准备好一个 ConnectOptionsonConnect / onDisconnect / onFailed);
    • connectAbilityconnectServiceExtensionAbility,拿到 proxy
    • 构造 MessageSequence,写入参数,调用 proxy.sendMessageRequest(...)
    • 拿到 result.reply,从里面把服务端写回来的结果读出来。
  3. 用完记得断开:
    • disconnectAbility(connectId, callback)
      disconnectServiceExtensionAbility(connectId)

下面按这个顺序展开。


3. 服务端:ServiceExtensionAbility + Stub 怎么写?

3.1 工程结构

官方示例里大概是这样一个目录:

复制代码
├── ets
│   ├── ServiceExtAbility
│   │   ├── ServiceExtAbility.ets
└── ...

ServiceExtAbility.ets 里做两件事:

  1. 写一个继承 ServiceExtensionAbility 的类(后台服务本体);
  2. 写一个继承 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 多注意两点:

  1. 设备列表可能为空(对端没在线 / 没发现);
  2. 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();
    });
}

几个点我个人会特别注意:

  1. Proxy 判空

    proxy 抽成一个封装类,所有调用前统一做 if (!proxy) 的检查,会比到处判空干净很多。

  2. code 推荐用常量 / enum 管理

    复制代码
    enum RpcCode {
      SEND_STRING = 1,
      // FUTURE_ADD = 2,
    }

    后面服务越写越多的时候,不然你到处都是裸 1, 2, 3,很难维护。

  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. 最后一点个人总结

把这篇文拆开看的话,其实就是几步:

  1. 理解"Proxy--Stub 对"的角色分工
  2. 服务端:写一个 Stub,并在 ServiceExtensionAbility 的 onConnect 里把它返回出去
  3. 客户端:准备 want + connect,连上之后拿到 Proxy
  4. 调用 sendMessageRequest,用 MessageSequence 完成参数的读写
  5. 最后别忘断开连接、回收 MessageSequence

对我自己来说,这一套走通之后,再回头看官方文档,就不会只停留在"复制粘贴示例代码",而是能在脑子里有一张完整的"消息从 UI 按钮点击 → 到对端服务处理 → 再回来的 流程图"。

相关推荐
●VON2 小时前
Flutter 与鸿蒙深度整合:如何实现原生功能调用
flutter·华为·harmonyos
食品一少年9 小时前
【Day7-10】开源鸿蒙组件封装实战(3)仿知乎日报的首页轮播图实现
华为·开源·harmonyos
q***01779 小时前
spring loC&DI 详解
java·spring·rpc
寻找华年的锦瑟10 小时前
Qt-视频九宫格布局
开发语言·qt
HONG````10 小时前
鸿蒙应用HTTP网络请求实战指南:从基础到进阶优化
网络·http·harmonyos
赵财猫._.10 小时前
HarmonyOS内存优化实战:泄漏检测、大对象管理与垃圾回收策略
华为·wpf·harmonyos
风浅月明10 小时前
[Harmony]跳转应用商店进行版本更新
harmonyos·版本更新
欧学明10 小时前
希影RS80 Ultra 鸿蒙巨幕 4K投影仪:2㎡阳台的多元光影体验
harmonyos·希影 rs80 ultra
马剑威(威哥爱编程)10 小时前
【鸿蒙开发实战篇】鸿蒙跨设备的碰一碰文件分享
华为·harmonyos