写点什么

ArkUI-x 跨平台 Bridge 最佳实践

作者:龙儿筝
  • 2025-06-10
    湖北
  • 本文字数:7025 字

    阅读完需:约 23 分钟

bridge 核心架构思想

平台桥接机制是 ArkUI-X 框架提供的⼀种 ArkTs 语⾔和平台原⽣语⾔(Java、OC)之间通信的机制,⽅便⼆者互相调⽤。需要说明的是,平台桥接机制必须在打开 ArkUI 界⾯时才能进⾏,不能在⾮ArkUI 界⾯触发。平台桥接机制有两种应⽤场景:


1.ArkUI 界⾯需要和原⽣应⽤底座进⾏业务层⾯通信,⽐如应用中,需要借助宿主通道获取设备状态信息、下发控制命令等;


2.跨平台代码中⽤到了不⽀持跨平台的 API,此时⼜想跨平台可以利⽤此机制将不⽀持跨平台的 API 中转到原⽣实现。包括部分开源 API 以及全部的闭源 API,当前对应 API 尚不⽀持跨平台,可以基于原⽣平台语⾔封装业务接⼝,通过平台桥接机制供跨平台界⾯调⽤。


bridge 支持的能力现状

能力一:支持多种桥接模式

指定桥接模式的时机是在创建平台桥接示例时,创建时的条件:需指定名称,该名称 ArkTS 侧与平台侧保持一致,为了满足应用不同业务场景的 bridge 诉求,bridge 支持设置不同的桥接模式来进行两端通信(默认为 JSON 编解码模式):


模式一:JSON 编解码模式


用户场景: JSON 模式可以满足用户大多数场景使用,支持基础数据类型、数组类型和结构化数据传递


模式二:二进制编解码模式


用户场景: 二进制模式相比 JSON 模式其他能力不变,新增加 Buffer 数据格式,支持传输如图片等数据量大的场景


模式三:线程并发模式


用户场景: 应用 Bridge 场景如果希望不阻塞主 UI 线程场景下使用线程并发模式


优势: 用户调度 bridge 都在 Platform 线程,然后由 Platform 线程切换到 JS 线程,数据编解码都会阻塞主 UI 线程,为了不阻塞主 UI 线程,将耗时处理放到后台异步线程中处理, 让 Bridge 调用者可连续发送数据


创建方式: 线程并发模式目前只能在原生平台侧创建平台桥接实例时指定

能力二:支持原生平台和 ArkTS 侧数据互相传递

发送数据, 接口参考: sendMessage(ArkTS)支持直接传递 callback), sendMessage(Android) sendMessage(iOS)


接收数据, 接口参考: setMessageListener(ArkTS)接收原生平台 sendMessage 发送的数据), setMessageListener(Android) messageListener(iOS)onMessage->接收 ArkTS 侧 sendMessage 发送的消息、onMessageResponse->原生平台 sendmessage 后接收 ArkTS 侧的响应


数据类型支持, 参考:数据类型支持

能力三:支持原生平台和 ArkTS 侧方法互相调用

定义被调用方法, Android 侧定义被调用方法时需将访问修饰符定义为 public


方法注册, ArkTS 侧需要通过方法registerMethod定义被原生平台侧调用的方法,原生平台侧供 ArkTS 侧调用的方法无需注册


方法移除和监听, ArkTS 侧可以通过方法unregisterMethod来移除已注册的 ArkTS 端的方法,原生平台侧通过实现IMethodResult接口,基于其中的onMethodCancel方法来监听 ArkTS 侧的事件注销通知


方法调用, 参考: callMethod(ArkTS)callMethodWithCallback(ArkTS)callMethod(Android) callMethod(iOS)均支持带参数调用和无参数调用

bridge 如何做到"一码三平台"

前面讲到的 bridge 主要是解决开发者在进行 ArkTS 代码开发时,需要使用的鸿蒙 API 不支持跨平台的问题,在 Android 和 iOS 平台上,可以借助 bridge 调用原生能力来完成任务。同时,对于任何跨平台框架的开发者而言,最终的目的肯定是写一套代码,能够同时运行到三个平台上,即“一码三平台”的思想,所以,如何让开发者使用 Bridge 适配时,修改最少量的原有架构和逻辑代码是 Bridge 最佳实践中需要讨论的一个重点。


接下来我们以调用相机管理的能力(该能力提供的 api 当前不支持跨平台),来介绍跨平台的 Bridge 实现“一码三平台”的推荐写法



如上图所示,HarmonyOS 应用的分层架构主要包括三个层次:产品定制层、基础特性层和公共能力层,具体参考分层架构设计,基础特性层包含的主要是应用中的页面 UI 逻辑以及其中包含的核心功能逻辑。


这里我们的核心思想便是,上层业务统一调用 CameraImpl 的一套接口,无需区分平台,CameraImpl 中实现平台差异化,即鸿蒙平台 getInterface 返回的是 CameraLocal 类,安卓和 iOS 平台返回的是 CameraArkUIX 类,下层 CameraInterface 包含 camera 能力暴露出去的所有接口,CameraLocal 和 CameraArkUIX 差异化实现了 CameraInterface 中的所有接口,即 CameraArkUIX 借助 bridge 调用原生能力,CameraLocal 直接调用鸿蒙 API 完成鸿蒙应用的能力实现。



上层业务,feature1 的页面中有一个按钮,用来查询当前相机是否被禁用,其中“media”需要在 feature1 模块的 oh-package.json5 文件中进行依赖配置:


// feature1/src/main/ets/pages/Index.etsimport { CameraImpl, CameraInterface } from "media";
@Entry@Componentstruct Index { build() { Row() { Column() { Button('相机状态查询') .width(300) .margin({top: 30}) .onClick(() => { CameraImpl.getInterface().isCameraMuted(); }) } } }}
复制代码


改造时,该 UI 业务逻辑部分保持不变,依赖的相机管理能力统一位于 commons 层,commons 层文件夹下有一个 module 包含媒体能力,暂且称为 media,其中可能包含 video、audio 和 camera 等能力。


首先先介绍改造后 media 中涉及 camera 能力的目录结构:


├── commons│   ├── media                    // commons层级功能模块 media│   │   ├── src\main\ets│   │   │   ├── camera│   │   │   │   ├── interface              // interface 文件目录|   |   |   |   |   └── CameraInterface.ets          // Camera 模块功能接口定义|   |   |   |   └── impl                // class 文件目录|   |   |   |       ├── Camera.ets              // Camera|   |   |   |       ├── CameraArkUIX.ets        // ArkUIX实现|   |   |   |       └── CameraLocal.ets            // HarmonyOS实现|   |   |   └── 功能...│   │   ├── Index.ets│   │   └── oh-package.json5│   └── 其他模块...
复制代码


// commons/media/src/main/ets/camera/interface/CameraInterface.etsexport interface CameraInterface {  isCameraMuted(): void;}
复制代码


这里的‘@ohos.bridge’、'utils'分别来自于下文中 host_bridge、utils 模块,需要在 media 模块的 oh-package.json5 文件中进行依赖配置。


// commons/media/src/main/ets/camera/impl/CameraArkUIX.etsimport { CameraInterface } from '../interface/CameraInterface';import bridge from '@arkui-x.bridge';import { bridgeApi } from '@ohos.bridge';
export class CameraArkUIX implements CameraInterface { private static instance: CameraArkUIX;
public static getInstance(): CameraArkUIX { if (!CameraArkUIX.instance) { CameraArkUIX.instance = new CameraArkUIX(); } return CameraArkUIX.instance; }
public isCameraMuted(): boolean { console.log("getInteractive enter arkuix"); return bridgeApi.getBridge().isCameraMuted(); }}
复制代码


// commons/media/src/main/ets/camera/impl/CameraLocal.etsimport camera from '@ohos.multimedia.camera';import { CameraInterface } from '../interface/CameraInterface';
export class CameraLocal implements CameraInterface { private static instance: CameraLocal;
public static getInstance(): CameraLocal { if (!CameraLocal.instance) { CameraLocal.instance = new CameraLocal(); } return CameraLocal.instance; }
public isCameraMuted(): boolean { let cameraManager = camera.getCameraManager(getContext()); let isMuted: boolean = cameraManager.isCameraMuted(); return isMuted; }}
复制代码


// commons/media/src/main/ets/camera/impl/Camera.etsimport { CameraArkUIX } from './CameraArkUIX';import { CameraLocal } from './CameraLocal';import { CameraInterface } from './interface/CameraInterface';import { PlatformInfo, PlatformTypeEnum } from 'utils';
export class CameraImpl { public static getInterface(): CameraInterface { let platform: PlatformTypeEnum = PlatformInfo.getPlatform(); if (platform == PlatformTypeEnum.ANDROID || platform == PlatformTypeEnum.IOS) { return CameraArkUIX.getInstance(); } else { return CameraLocal.getInstance(); } }}
复制代码


此外,需要新建 host_bridge 模块,用来管理 bridge 的相关实现方法,具体目录结构及实现如下:


├── commons│   ├── host_bridge                   // 定义bridge的一些公共方法模块│   │   ├── src\main\ets│   │   │   ├── interface                // 可以定义Callback、Response等文件接口|   |   |   └── BridgeApi.ets              // 对外暴露的bridge实现类,在应用生命周期初始化创建│   │   ├── Index.ets                // 对外导出组件实例文件│   │   └── oh-package.json5            // host_bridge 依赖项配置文件│   └── 其他模块...
复制代码


// commons/host_bridge/src/main/ets/BridgeApi.etsimport bridge from '@arkui-x.bridge';import { PlatformInfo, PlatformTypeEnum } from 'utils';
export class bridgeApi { private static bridgeImpl: bridge.BridgeObject;
constructor() { let platform: PlatformTypeEnum = PlatformInfo.getPlatform(); if (platform == PlatformTypeEnum.ANDROID || platform == PlatformTypeEnum.IOS) { bridgeApi.bridgeImpl = bridge.createBridge('arkuixbridge'); } }
static getBridge(): bridge.BridgeObject | null { return bridgeApi.bridgeImpl? bridgeApi.bridgeImpl:null; }}
复制代码


这里需要注意的是,bridgeApi 的构造函数中创建了名为 arkuixbridge 的 bridge 实例对象,用来和安卓或者 iOS 原生侧进行通信,那么在什么时机去构建 bridgeApi 的实例对象呢,这里推荐在跨平台入口 Ability 的 onCreate 生命周期中初始化,例如:


// feature1/src/main/ets/entryability/EntryAbility.etsimport { bridgeApi } from '@ohos.bridge';
export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { let bridgeImpl: bridgeApi = new bridgeApi(); hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); } ...}
复制代码


common 层中的很多能力都依赖平台差异化,所以 utils 中新建一个 PlatformInfo.ets 用来提供接口类及平台枚举,具体的目录结构及实现如下:


├── commons│   ├── utils                    // commons层级功能模块 utils 通用方法│   │   ├── src\main\ets│   │   │   ├── common                // 常量、数据结构等定义文件目录|   |   |   └── utils                // class 文件目录|   |   |        └── PlatformInfo.ets          // 区分当前设备平台│   │   ├── Index.ets                // utils 对外暴露接口导出文件│   │   └── oh-package.json5            // utils 依赖项配置文件│   └── 其他模块...
复制代码


// commons/utils/src/main/ets/utils/PlatformInfo.etsimport deviceInfo from '@ohos.deviceInfo';
export enum PlatformTypeEnum { HARMONYOS = 'HarmonyOS Platform', ANDROID = 'Android Platform', IOS = 'iOS Platform', UNKNOWN = 'Unknown Platform',}
export class PlatformInfo { static getPlatform(): PlatformTypeEnum { let osFullNameInfo: string = deviceInfo.osFullName; let platformName: string = osFullNameInfo.split(' ')[0]; if (platformName.includes("OpenHarmony")) { return PlatformTypeEnum.HARMONYOS; } else if (platformName.includes("Android")) { return PlatformTypeEnum.ANDROID; } else if (platformName.includes('iOS')) { return PlatformTypeEnum.IOS; } else { return PlatformTypeEnum.UNKNOWN; } }}
复制代码

FAQ

Q1:


  上面讲到的都是开源 API 不支持跨平台时的处理策略,对于均不支持跨平台的 HMS API,直接基于上述写法运行起来会直接 crash,以活体检测 API interactiveLiveness为例,运行态会报如下截图错误,该如何解决?



A1:


1.原因分析: 如果应用工程中 import 了 HMS API 并涉及到具体调用,当应用运行起来后还未执行到具体业务逻辑时,方舟虚拟机就会首先遍历寻找实现这些 HMS API 的 hsp,对于鸿蒙应用而言,这些 hsp 包预置到鸿蒙手机系统,因而可以成功获取,但是安卓和 iOS 原生平台未集成这些闭源 hsp,所以会因找不到 hsp 直接 crash。(注:若 import 了 HMS API,但是只存在对象声明,不调用 API 的情况不会初始化寻找对应 hsp)。


2.解决方案: 仍可以基于上面“一码三平台”的架构,只不过需要在 commons 层使用 HMS API 的地方引入动态import的处理策略,以活体检测 API 为例,具体实现参考如下:


// commons/recognize/src/main/ets/Interactive/Interactive.etsimport { PlatformInfo, PlatformTypeEnum } from 'utils';import { InteractiveInterface } from './interface/interactiveInterface';import { InteractiveArkUIX } from './impl/interactiveArkUIX';
export class Interactive { public static getInterface(): InteractiveInterface { let platform: PlatformTypeEnum = PlatformInfo.getPlatform(); if (platform == PlatformTypeEnum.ANDROID || platform == PlatformTypeEnum.IOS) { return InteractiveArkUIX.getInstance(); } else { return InteractiveLocal.getInstance(); }}
复制代码


这里不再赘述 InteractiveArkUIX 的实现,和上文 CameraArkUIX 思想一致,直接看 InteractiveLocal 的实现,最重要的便是在使用 startLivenessDetection 该 API 之前,需要重新动态 import 一次具体的系统接口文件,但正如上述所说,函数入口处的 isSilentMode、actionsNum 和 routerOptions 只是使用 API 中的数据类型声明定义变量,这些仍可以直接使用文件入口处 import 的 interactiveLiveness 能力而不会带来问题:


// commons/recognize/src/main/ets/Interactive//impl/InteractiveLocal.etsimport { InteractiveInterface } from '../interface/interactiveInterface';import { BusinessError } from '@kit.BasicServicesKit';import { hilog } from '@kit.PerformanceAnalysisKit';import interactiveLiveness from '@hms.ai.interactiveLiveness'
export class InteractiveLocal implements InteractiveInterface { public startInteractive(): void { let isSilentMode = "INTERACTIVE_MODE" as interactiveLiveness.DetectionMode; let actionsNum = 3 as interactiveLiveness.ActionsNumber; let routerOptions: interactiveLiveness.InteractiveLivenessConfig = { actionsNum: actionsNum, isSilentMode: isSilentMode }; import('@hms.ai.interactiveLiveness').then((ns) => { try { ns.default.startLivenessDetection(routerOptions).then((DetectState: boolean) => { hilog.info(0x0001, "LivenessCollectionIndex", `Succeeded in jumping.`); }).catch((err: BusinessError) => { hilog.error(0x0001, "LivenessCollectionIndex", `Failed to jump. Code:${err.code},message:${err.message}`); }) } catch (err) { err = err as BusinessError; console.error(`startLivenessDetection failed. Code: ${err.code}, message: ${err.message}`); } return; }) }}
复制代码


3.更优化的解决方案: 上述写法虽然可以解决 crash 的问题,但是不可避免地会在一个 ets 文件中的不同函数中反复多次进行动态 import API,影响开发效率和代码整齐程度,所以可以考虑在 Interactive 类的平台差异化处,直接进行动态 import ets 文件的处理,处理完成后,文件内使用 HMS API 的地方无需再增加动态 import API 的侵入修改。


// commons/recognize/src/main/ets/Interactive/Interactive.etsimport { PlatformInfo, PlatformTypeEnum } from 'utils';import { InteractiveInterface } from './interface/interactiveInterface';import { InteractiveArkUIX } from './impl/interactiveArkUIX';
export class Interactive { public static async getInterface(): Promise<InteractiveInterface|null> { let temp: InteractiveInterface | null = null; let platform: PlatformTypeEnum = PlatformInfo.getPlatform(); if (platform == PlatformTypeEnum.ANDROID || platform == PlatformTypeEnum.IOS) { temp = InteractiveArkUIX.getInstance(); } else { await import('./impl/interactiveLocal').then((ns) => { temp = new ns.InteractiveLocal(); }) } return temp; }}
复制代码


发布于: 刚刚阅读数: 2
用户头像

龙儿筝

关注

还未添加个人签名 2024-10-27 加入

还未添加个人简介

评论

发布
暂无评论
ArkUI-x跨平台Bridge最佳实践_龙儿筝_InfoQ写作社区