我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

老实说,每次看到“某某功能需要常驻后台”这句需求,我脑门都会“咔”地一紧:到底是应该“启动服务就完事”(startAbility),还是要“连接服务走 RPC”(connectAbility),抑或两者兼用? 再加上 Harmony 的 FA/PA 模型Stage 模型并存,名词一上来就“外焦里嫩”。
  别怵,这篇就按你的大纲一路捋顺:Ability 模型 → ServiceAbility 实现 → FA/PA 交互,把 startAbility / connectAbility / RPC 的选型与代码姿势讲清楚;顺带把 ServiceExtensionAbility(Stage 模型) 的差异也标出来,方便你做“新老并轨”。文末再给一套“工程化”清单,帮你避坑。参考点我都贴了官方文档,想深挖直接跳。(华为开发者官方网站)

一、先把地图摊开:Ability 模型长啥样?

1)两代模型要对齐记忆

  • FA 模型(早期)

    • FA(Feature Ability):多为可见页面(PageAbility)
    • PA(Particle Ability):多为无界面服务(ServiceAbility / DataAbility / FormAbility)
    • 交互套路startAbility()(启动/驱动)+ connectAbility()(绑定后 RPC)
    • 官方“连接 ServiceAbility”一文把流程讲得很直白:连接需要回调;onConnect() 要返回 IRemoteObject;通信靠 RPC。(华为开发者官方网站)
  • Stage 模型(API 9+)

    • UIAbility ≈ FA 的 PageAbility
    • ServiceExtensionAbility ≈ PA 的 ServiceAbility 的“Stage 版”
    • 交互套路context.startAbility(...) / startServiceExtensionAbility(...)
      context.connectServiceExtensionAbility(...)/disconnect...;上下文类型是 ServiceExtensionContext。(GitCode)

经验口诀:“启动干活用 startAbility,交互沟通用 connectAbility + RPC”。启动负责“让服务活起来”,连接负责“建立全双工通道”。

二、ServiceAbility/ServiceExtensionAbility:怎么“注册 + 实现 + 启停 + 连接”?

2.1 清单注册(FA vs Stage)

  • FA(旧):在 config.json/module/abilities 里注册,type: "service",可见性、图标/描述等按需配置(不同版本字段略有差异,注意与 SDK 对齐)。很多实战总结也强调:用 startAbility 仅启动即可,不必实现 onConnect;若要 RPC 就必须支持 connectAbility/onConnect。(ost.51cto.com)

  • Stage(新):在 module.json5abilities 里声明一个 type: "service" 的扩展能力(即 ServiceExtensionAbility),启动/连接 API 均从上下文拿(ServiceExtensionContext),文档列出了 start/stop/connect/disconnect 的全家桶。(GitCode)

2.2 生命周期要点

  • FA/ServiceAbilityonStart(初始化)→ onCommand(每次 startAbility 调用都会到)→ onConnect(绑定时返回 IRemoteObject)→ onDisconnectonStop。(ost.51cto.com)
  • Stage/ServiceExtensionAbility:方法名略有不同,但语义一致:创建/请求/连接/断开/销毁;连接时同样需要返回远端对象用于 RPC。(GitCode)

口诀 2:“start 负责唤醒,command 接任务;connect 负责建通道,disconnect 收尾”

三、两种启动姿势:startAbility vs connectAbility

场景 方式 生命周期触发 是否拿到远端对象 典型用途
只需让服务开始工作(下载、巡检、预热…) startAbility() / startServiceExtensionAbility() onStartonCommand 纯“启动”,不交互
需要与服务交互(播放控制、查询状态…) connectAbility() / connectServiceExtensionAbility() onConnect(首次) 是(IRemoteObject 建立 RPC 会话
  • FA 文档强调:连接 Service 需实现 IAbilityConnection 回调,成功后拿到 IRemoteObject;服务端的 onConnect() 必须 return 一个 IRemoteObject(通常继承自 rpc.RemoteObject)。(华为开发者官方网站)
  • Stage 文档给出同等 API 在 ServiceExtensionContext 上的对应方法。(GitCode)

四、RPC 怎么写:RemoteObject/Proxy/Stub 三件套

**核心对象(ArkTS/JS 层)**在 @kit.IPCKitrpc 模块中:

  • IRemoteObject:对端对象的抽象
  • RemoteObject:服务端“桩”(Stub)的默认实现基类(重写 onRemoteRequest
  • MessageSequence:消息载体(序列化/反序列化)
  • 典型流程见 “IPC/RPC 开发指导”与 js-apis-rpc 参考。(GitCode)

口诀 3:“服务端写 Stub(RemoteObject),客户端包 Proxy,双方约定 code + 序列化协议”


五、完整示例(ArkTS/Stage 版):音乐播放服务(播放/暂停/查询状态)

说明:示例以 Stage 模型 为主(ServiceExtensionAbility),FA 的 ServiceAbility 写法在架构和 RPC 思路上一致,只是入口/上下文 API 命名不同;文档映射见上文。以下代码为示意,不同 API 版本的命名可能略有差异,落地时以已安装 SDK 文档为准。(GitCode)

5.1 module.json5 注册(Stage)

{
  "module": {
    "name": "entry",
    "type": "entry",
    "abilities": [
      {
        "name": "EntryAbility",
        "type": "page",
        "srcEntry": "./ets/entryability/EntryAbility.ets"
      },
      {
        "name": "PlayerService",
        "type": "service",                      // ServiceExtensionAbility
        "srcEntry": "./ets/service/PlayerService.ets",
        "exported": true                        // 允许跨应用连接(按需)
      }
    ],
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

5.2 服务端:PlayerService.ets(Stub + Service)

// ets/service/PlayerService.ets
import { ServiceExtensionAbility, common } from '@kit.AbilityKit'
import { rpc } from '@kit.IPCKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

// 定义消息码
const TRANS = {
  PLAY: 1,
  PAUSE: 2,
  GET_STATUS: 3,
} as const

// 服务端 Stub:继承 RemoteObject,重写 onRemoteRequest
class PlayerStub extends rpc.RemoteObject {
  private playing = false

  constructor(descriptor = 'com.example.player.IPC') {
    super(descriptor)
  }

  // 处理来自客户端的请求
  onRemoteRequest(code: number, data: rpc.MessageSequence, reply: rpc.MessageSequence, option: rpc.MessageOption): boolean {
    try {
      switch (code) {
        case TRANS.PLAY: {
          const url = data.readString() // 假设客户端写入了一个字符串 URL
          hilog.info(0x0, 'Player', `PLAY ${url}`)
          this.playing = true
          reply.writeInt(0)             // 0 表示 OK
          return true
        }
        case TRANS.PAUSE: {
          hilog.info(0x0, 'Player', `PAUSE`)
          this.playing = false
          reply.writeInt(0)
          return true
        }
        case TRANS.GET_STATUS: {
          reply.writeBool(this.playing)
          return true
        }
        default:
          return super.onRemoteRequest(code, data, reply, option)
      }
    } catch (e) {
      hilog.error(0x0, 'Player', `onRemoteRequest error: ${JSON.stringify(e)}`)
      return false
    }
  }
}

export default class PlayerService extends ServiceExtensionAbility {
  private stub = new PlayerStub()

  // startAbility / startServiceExtensionAbility 触发
  onCreate(want: common.Want): void {
    hilog.info(0x0, 'Player', 'Service onCreate')
  }

  // connectAbility / connectServiceExtensionAbility 触发,必须返回 IRemoteObject
  onConnect(want: common.Want): rpc.IRemoteObject {
    hilog.info(0x0, 'Player', 'Service onConnect')
    return this.stub as unknown as rpc.IRemoteObject
  }

  onDisconnect(want: common.Want): void {
    hilog.info(0x0, 'Player', 'Service onDisconnect')
  }

  onDestroy(): void {
    hilog.info(0x0, 'Player', 'Service onDestroy')
  }
}

要点回看:onConnect 必须返回 IRemoteObject,文档也明确了这一点;你通常继承 rpc.RemoteObject 来实现。(GitCode)

5.3 客户端(UIAbility 内):启动与连接

// ets/pages/Home.ets(或 UIAbility 中的某处)
import { common } from '@kit.AbilityKit'
import { rpc } from '@kit.IPCKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

const TRANS = { PLAY:1, PAUSE:2, GET_STATUS:3 } as const

// 简易 Proxy,把 IRemoteObject 包一层,屏蔽 MessageSequence 细节
class PlayerProxy {
  constructor(private remote: rpc.IRemoteObject) {}

  async play(url: string): Promise<number> {
    const data = rpc.MessageSequence.create()
    const reply = rpc.MessageSequence.create()
    const opt = new rpc.MessageOption()
    data.writeString(url)
    const ok = this.remote.sendRequest(TRANS.PLAY, data, reply, opt)
    const code = ok ? reply.readInt() : -1
    data.reclaim(); reply.reclaim()
    return code
  }

  async pause(): Promise<number> {
    const data = rpc.MessageSequence.create()
    const reply = rpc.MessageSequence.create()
    const opt = new rpc.MessageOption()
    const ok = this.remote.sendRequest(TRANS.PAUSE, data, reply, opt)
    const code = ok ? reply.readInt() : -1
    data.reclaim(); reply.reclaim()
    return code
  }

  async getStatus(): Promise<boolean> {
    const data = rpc.MessageSequence.create()
    const reply = rpc.MessageSequence.create()
    const opt = new rpc.MessageOption()
    const ok = this.remote.sendRequest(TRANS.GET_STATUS, data, reply, opt)
    const playing = ok ? reply.readBool() : false
    data.reclaim(); reply.reclaim()
    return playing
  }
}

@Entry
@Component
struct Home {
  private connection?: number
  private proxy?: PlayerProxy

  build() {
    Column({ space: 12 }) {
      Button('Start Service (fire & forget)').onClick(async () => {
        // 仅启动(不建立连接)
        // Stage:可用 this.getContext() as UIAbilityContext,再调用 startServiceExtensionAbility
        const want: common.Want = {
          bundleName: 'com.example.app',
          abilityName: 'PlayerService'
        }
        // @ts-ignore 伪示例:以你项目的 context API 为准
        await this.getUIContext().startServiceExtensionAbility(want)
      })

      Button('Connect Service (RPC)').onClick(async () => {
        const want: common.Want = {
          bundleName: 'com.example.app',
          abilityName: 'PlayerService'
        }
        // 连接:拿到 token(连接 id)与 IRemoteObject
        // @ts-ignore 伪示例:实际需传入回调/IAbilityConnection,按 SDK 写
        const { token, remote } = await this.getUIContext().connectServiceExtensionAbility(want)
        this.connection = token
        this.proxy = new PlayerProxy(remote)
      }).backgroundColor('#238636').fontColor('#fff')

      Button('PLAY').onClick(async () => {
        const code = await this.proxy?.play('https://example.com/song.mp3')
        hilog.info(0x0, 'Home', `PLAY -> ${code}`)
      })

      Button('PAUSE').onClick(async () => {
        const code = await this.proxy?.pause()
        hilog.info(0x0, 'Home', `PAUSE -> ${code}`)
      })

      Button('Query status').onClick(async () => {
        const playing = await this.proxy?.getStatus()
        hilog.info(0x0, 'Home', `playing=${playing}`)
      })

      Button('Disconnect').onClick(async () => {
        if (this.connection != null) {
          // @ts-ignore
          await this.getUIContext().disconnectServiceExtensionAbility(this.connection)
          this.connection = undefined
          this.proxy = undefined
        }
      }).backgroundColor('#C9510C').fontColor('#fff')
    }.padding(16)
  }
}

对照文档:连接 API上下文类型以及 MessageSequence 的读写方法都在官方能力包与 IPC 参考里列得很细,实际编码时以你工程的 API 版本为准。(GitCode)


六、FA/PA 交互怎么落地(老项目怎么办)?

  • FA→PA(PageAbility 调 ServiceAbility)

    • “只启动”就用 featureAbility.startAbility(want)(触发 onStart/onCommand);
    • “要交互”用 featureAbility.connectAbility(want, connection);成功后在 onAbilityConnectDone(...) 里拿到 IRemoteObject,包一层 Proxy,后续就和上面的 PlayerProxy 同理。官方在“连接 ServiceAbility”的指南里强调 onConnect 返回 IRemoteObject连接回调,照抄即可。(华为开发者官方网站)
  • 迁移到 Stage

    • PageAbility → UIAbility;ServiceAbility → ServiceExtensionAbility
    • featureAbility.startAbility/connectAbility上下文上的 start/connectServiceExtensionAbility
    • IRemoteObject/RemoteObject/MessageSequence 的用法不变(只是导入来源模块不同)。(GitCode)

七、跨设备与跨应用:就差一个 Want

  • 跨应用want.bundleName 指向对方包名,exported: true 或相应可见性要打开。
  • 跨设备:在 Want(或 Operation)里带上 deviceId(不同层语言名字略有差异),connectAbility 就会连到远端服务;这在华为开发者站“连接 Service”示例里有说明(Java 版 OperationBuilder 例子,但思路一致)。(华为开发者官方网站)

友情提示:跨设备 = IPC 变 RPC,链路更长,超时/重试/断线重连务必做全。


八、工程化清单(别到线上才想起来)

  1. 线程与耗时:服务回调在主线程,绝对不要onRemoteRequest 里做重活;把重活丢到线程池/任务队列,再把结果写回。

  2. 协议与版本:RPC 的 code/参数结构 要有版本号;两端不同版本并存时,向下兼容

  3. 连接管理

    • 首次连接才会触发 onConnect 生成 IRemoteObject,同进程多连接可能共享同一对象(文档有说明)(Geek大学);
    • 记得 disconnect;服务端适时回收资源。
  4. 异常与错误码js-apis-rpc 列出了错误码与 MessageSequence 生命周期管理(reclaim());异常要兜住。(GitCode)

  5. 权限与可见性:最小权限、exported 慎开;系统/三方能力边界以当期 SDK 政策为准(系统文档会注明限制)。(GitCode)

  6. 日志与监控:启动/连接/断连/远端死亡(onAbilityDisconnectDone)都要打点;跨设备 RPC 要记录 RTT。(华为开发者官方网站)


九、常见“坑位”我先替你踩

  • 只 start 不 connect 却想拿接口:start 只是“让它动起来”,拿不到远端对象;要交互必须 connect。(华为开发者官方网站)
  • onConnect 不返回 IRemoteObject:那客户端连上也没法 RPC;记得继承 rpc.RemoteObjectreturn。(GitCode)
  • 在 onRemoteRequest 里做大 IO:主线程卡死 + ANR 警告,线上稳崩。
  • MessageSequence 不 reclaim():长连场景内存涨跌肉眼可见(参考 js-apis-rpc 的资源回收说明)。(GitCode)
  • 跨设备忽略掉断连:弱网/休眠/切后台会频繁掉线,重连机制一定要做。
  • 混用 FA/Stage API:团队要先定“当前应用跑哪套模型”,迁移分支要明确。

十、收个尾:把“后台服务”从概念落到秩序

看完这套链路,你就会发现:ServiceAbility/ServiceExtensionAbility 并不玄学

  • 能力模型:FA/PA 与 Stage 对齐心里有数;
  • 实现路径:服务注册 + 生命周期 + onConnect 返回 IRemoteObject
  • 交互策略:start 负责“开机”,connect + RPC 负责“对话”;
  • 工程护栏:线程、权限、错误码、连接管理一个都别落。
      下一步?把你项目里那个“常驻下载/播放/同步”的服务挑出来,按上面的 Stub/Proxy 模版改造一次。一回生二回熟,第三回你就能写成团队基建

参考(可直接对照 SDK)

  • 华为开发者:连接 ServiceAbility(FA)——连接回调、onConnect 返回 IRemoteObject、跨设备/跨应用要点。(华为开发者官方网站)
  • OpenHarmony 文档:连接 ServiceAbility(通用指南)——onConnect 返回值与 rpc.RemoteObject 默认实现。(GitCode)
  • OpenHarmony 文档:ServiceExtensionContext(Stage)——start/stop/connect/disconnect 全部接口。(GitCode)
  • OpenHarmony 文档:IPC/RPC 开发指导 & js-apis-rpc——MessageSequence 读写、错误码、对象模型。(GitCode)

(未完待续)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐