如何做到四端统一桥接?微医跨平台桥接标准化方案了解一下

近几年随着 React Native、Flutter、Weex 等跨平台框架的流行,使得程序员可以尽量关注于业务本身,而非平台间的差异。但是不管哪一种方案,从移动端的角度看,都对底层桥接 API 有着共同的诉求。从 H5 到 React Native,再到 Weex 以及后面的 Flutter,原生进行了多轮的 API 重复建设,造成了缺少 API 接口的标准化定义,以及实现的统一管控的现状。所以针对这一情况,我们将统一所有容器 Bridge API,包括接口的定义,以及其底层原生代码。

方案概况

旧方案流程介绍

以前需要桥接一个方法时,我们需要在 Web、RN、Weex、Flutter 各自写一套注册和实现,有时候往往因为需求时间的不同,或者开发人员的不同,导致相同功能的 Bridge 定义和实现也不一样,这不仅仅浪费了开发人员的时间精力,而且当 Web 需要换成 RN 或者 Weex 时,增加了替换的难度及风险。就算规划的好一点,统一了 Bridge 的实现和接口,但那时还是需要开发人员对各端都做注册实现,也是很浪费时间。

新方案流程介绍

我们在基础容器层对各跨平台容器的 Bridge 层做了适配,主要是模块注册、Bridge 解析、调用以及兼容方案,具体实现后文会讲。

WYBridge

首先我们先介绍下底层的 WYBridge 库,它包括了 4 端统一的桥接及实现。就像前面说的,我们以前的 bridge 都是各端写各自的,实现及接口定义都不统一,极不规范。而现在我们只需要在这个库中增加一个 module,上层 4 端会自动注册这个模块,这样各端的定义和实现都是用的 WYBridge 中的,实现了下层的统一。

Android

Android 中的 WYBridge 的核心为 BridgeModule,提供给上层的模块都会继承该接口,里面提供了 4 端都会用到的方法。

public interface BridgeModule {  //模块名称  String getName();}

复制代码

同时我们新建了 BridgeMethod 注解,标记为该模块提供给上层的方法(类似于 RN 中的 @ReactMethod)。提供的方法必须符合下面两种模板中的一种:

@BridgeMethodpublic void xxxx(JSONObject data, BridgeJSCallBack callBack){}
@BridgeMethodpublic void xxxx(BridgeJSCallBack callBack){}

复制代码

下面就是一个简单的 BridgeModle 例子:

public class XXTestModule extends BaseBridgeModule {
    @Override    public String getName() {        return "xxtest";    }        @BridgeMethod    public void getData(JSONObject data, final BridgeJSCallBack callBack){        //do something    }}

复制代码

各端通过解析这些模块,将其注册到自己的平台中。至此我们通过 WYBridge 实现了底层的统一。

iOS

iOS 中的 WYBridge 的核心为宏定义文件,提供给上层的模块通过 XX_EXPORT_MODULE(module_name)宏将该类以 module 的方式暴露给 JS,然后使用 XX_EXPORT_METHOD(js_name)将 Native 方法暴露给 JS。

模块注册

define XX_EXPORT_MODULE(module_name) \    XX_EXTERN void XXRegisterWebModule(Class); \    XX_EXTERN void XXRegisterWeexModule(Class); \    XX_EXTERN void RCTRegisterModule(Class); \    XX_EXTERN void XXRegisterFlutterModule(Class); \    + (void)load {\        XXRegisterWebModule(self);\        XXRegisterWeexModule(self);\        RCTRegisterModule(self);\        XXRegisterFlutterModule(self);\    }\    + (NSString *)moduleName { return @# module_name; }

复制代码

注:load 里面会注册四个平台的RegisterXXModule,若没有对应平台的SDK,可以通过判断头文件注册声明一个空的RegisterXXModule

如上代码所示,XX_EXPORT_MODULE 宏背后是两个静态方法+(NSString *)moduleName 和+(NSString *)load。moduleName 方法简单的返回了 Native 模块的类名,load 方法是大家耳熟能详的的,load 方法调用 RegisterXXModule 函数注册了模块,我这里注册了 4 端,RegisterXXModule 函数的实现是参考 RCTRegisterModule(该函数定义在 RCTBridge.m 中)

void RCTRegisterModule(Class);void RCTRegisterModule(Class moduleClass){  static dispatch_once_t onceToken;  dispatch_once(&onceToken, ^{    RCTModuleClasses = [NSMutableArray new];    RCTModuleClassesSyncQueue = dispatch_queue_create("com.facebook.react.ModuleClassesSyncQueue", DISPATCH_QUEUE_CONCURRENT);  });...  // Register module  dispatch_barrier_async(RCTModuleClassesSyncQueue, ^{    [RCTModuleClasses addObject:moduleClass];  });}

复制代码

很简单,RCTRegisterModule 函数只做了 3 件事: 1.创建一个全局的可变数组/字典和一个队列(在 Web/Weex/Fluuter 我定义的是一个字典) 2.检查导出给 JS 模块是否遵守了 RCTBridgeModule 协议(由于该检查,需要在 WYBridge 写一个 RCTBridgeModule 的空协议,为了躲过检查,其他端可不作检查) 3.把要导出的类添加到全局的可变数组/字段中进行记录 在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法都已经被注册到一个数组/字典中。到此为止,只是把需要导出给 JS 的类记录下来.

方法注册及实现

# define XX_EXPORT_METHOD(method) \XX_EXPORT_METHOD_INTERNAL(@selector(method),xx_export_method_)\RCT_REMAP_METHOD(, method)# define XX_EXPORT_METHOD_INTERNAL(method, token) \   + (NSString *)XX_CONCAT_WRAPPER(token, __LINE__) { \    return NSStringFromSelector(method); \    }# define RCT_REMAP_METHOD(js_name, method) \ _RCT_EXTERN_REMAP_METHOD(js_name, method, NO)# define _RCT_EXTERN_REMAP_METHOD(js_name, method, is_blocking_synchronous_method) \+ (const RCTMethodInfo *)XX_CONCAT_WRAPPER(__rct_export__, XX_CONCAT_WRAPPER(js_name, XX_CONCAT_WRAPPER(__LINE__, __COUNTER__))) { \static RCTMethodInfo config = {# js_name, # method, is_blocking_synchronous_method}; \   return &config; \}typedef struct RCTMethodInfo {      const char *const jsName;      const char *const objcName;      const BOOL isSync; } RCTMethodInfo;

复制代码

通过上面一系列的宏调用不难看出,XX_EXPORT_METHOD 做了 3 件事

  1. 定义一个对象方法,用于真正调用的方法实现
  2. 定义了一个静态方法,该方法名为 xx_export_method___LINE__,返回值是注册方法名,用于 Web 和 Weex,会扫描所有导出的 nativemodule 中以 xx_export_method 的方法
  3. 定义一个静态方法,该方法名的格式是 +(const RCTMethodInfo *)__rct_export__+js_name+___LINE__+__COUNTER__,用于 RN 这个方法包装成了一个 RCTMethodInfo 对象,在运行时 RN 会扫描所有导出的 Native module 中以__rct_export__开头的方法。

以上只是说了 module 和 method 是如何导出的,这些模块和方法的注册将会在各自模块中介绍。

使用

//模块注册XX_EXPORT_MODULE(moduleName)//方法注册XX_EXPORT_METHOD(methodName:success:fail:)//方法实现- (void)methodName:(NSDictionary *)data success:(XXBridgeResolveBlock)successfail:(XXBridgeRejectBlock)fail {     ...     success(resdic); }

复制代码

注:由于RN里会对XX_EXPORT_METHOD()中的参数做解析,然后取的方法名做跳转,而Web/WEEX中直接取的方法名,所以方法注册的时候还没有统一,可以优化。

Web

之前不管是 Android 还是 iOS,都是由原生提供给 H5 一个统一的方法用于调用原生桥接模块,H5 把需要调用的桥接方法名以及参数以 json 字符串的方式传入。这些桥接在 webview 初始化的时候进行注册,注册时只包含方法名,没有模块名,native 根据参数找到对应注册的方法进行调用,整个流程图如下:

前面说到 H5 只有方法名,但是 RN 和 Weex 都是以"模块名.方法名"的方式进行调用,所以为了和其他端统一,H5 调用原生桥接的方法名也需要加上模块名,并且初始化时,需将 WYBridge 中的桥接进行注册。新的流程图如下:

Android

下面具体介绍一下 Android Web 桥接 WYBridge 的实现。

注册

整体仿照 weex 和 RN 的注册。新增新的注册方式,将 BridgeModule 传入:

//新的注册方式boolean registerHandler(Class<? extends BridgeModule> moduleClass);

复制代码

注册的时候,我们会将 BridgeModule 转化成一个管理类,该类提供了 3 个方法:module 实例化、module 方法解析和 module 方法的调用。调用模块实例化方法,创建 BridgeModule 实例,将前面的管理类和 module 实例都以键值对的方式生成 Map 表。

解析

前面管理类中 module 方法解析,实际上就是解析 module 中被 @BridgeMethod 注解的方法, 获取到方法的 Invoker,最后的方法调用就是在这个 Invoker 里面实现的。

for (Method method : mClazz.getMethods()) {  for (Annotation anno : method.getDeclaredAnnotations()) {    if (anno instanceof BridgeMethod) {      String name = method.getName();      methodMap.put(name, new Invoker(method));      ...      break;    }  }}

复制代码

调用

已知 H5 调用都是通过统一的方法,最终会到一个处理 JS 调用 Native 数据类。在这个处理类中,我们将 H5 传入的数据进行解析。调用新的 bridge 方法时,我们和 H5 约定方法名传入为“模块名.方法名”,所以通过解析,我们可以得到对应的模块名和方法名。

这样根据模块名找到存在上面 Map 中该模块的实例和其管理类,又在管理类中根据方法名可以获取前面解析得到的该方法对应的 Invoker。最后我们将传入的参数和回调一同传入 Invoker 中,调用里面 method 的 invoke 方法,即 WYBridge 中方法的调用。

method.invoke(receiver, params);

复制代码

兼容方案

因为原来 H5 都是用过方法名的方式调用原生,现改成“模块名.方法名”,旧的调用模块将不再适用。但是 H5 团队繁多,没有统一的调用基类,由前端一处一处的改动是不现实的,所以需要做到兼容老的版本。 目前 Android 这边的处理方案是,对于旧的桥接方法,新建一个对应的处理类,里面包含了如下 5 个接口:

//旧的方法名String aliasName();//新的模块名String bridgeModuleName();//新的方法名String bridgeMethodName();//入参转换JSONObject mapping(String data);//回调参数转换String backMapping(String data, int code, String msg);

复制代码

在 webView 初始化的时候也将这些处理注册到一个 Map,以旧的方法名为 Key。当 H5 还是调用老的方法时,通过这个类找到对应新的模块名和方法名,从而进行调用,整个流程如下:

iOS

下面具体介绍一下 iOS Web 桥接 WYBridge 的实现。

注册

WYBridge 中,在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法以 class 的方式 都已经被注册到字典保存着,循环执行注册方法即可.

以前是维护一份以 methodName 为 key,原生代码实现的 block 为 value 的字典,现是以 moduleName.methodName 为 key, 通过 NSInvocation 封装了方法调用对象、方法选择器、参数、返回值等的 block 为 value 的字典.

[self registerApi:newMethod block:^(XXHandlerModel *handlerModel) { // 1、通过传入handlerModel获取 方法名、参数 // 2、通过 methodName 获取 selector // 3、通过注册的时候保存的对象,生成该方法签名,设置调用对象,设置参数然后调用方法   }];

复制代码

调用

我们和 H5 约定方法名传入为“模块名.方法名”,我们可以得到对应的模块名和方法名,通过获取一个类的所有实例方法,将所有以“xx_export_method_”开头的方法返回保存在字典中,返回的是调用用的方法名.

- (NSDictionary *)clazzMethodFactory {    ...    //1、通过class_copyMethodList获取所有方法    //2、返回以xx_export_method_开头方法字典
}

复制代码

兼容方案

iOS 这边的处理方案同 Android,新建一个对应的处理类 XXBridgeFactoryMapping,处理新老方法名字、入参、回调映射。

React Native

RN 中我们给每一个 Module 都封装了一个 ts 文件,用来统一上层 RN 端调用的接口,业务代码引用的时候直接用该方法就行。举个例子,如 wytest 模块,里面包含了一个 getData 方法:

//WYNativeTest.tsimport {NativeModules} from 'react-Native';
let WYTestModule = NativeModules.wytest;
export var WYNativeTest = {    getData(): Promise<any> {    return WYTestModule.getData().then((value: any) => {        return value;    })    }}

复制代码

而 Native 层,Android 和 iOS 都有自己的一套桥接库,在应用初始化的时候进行注册。以前这些桥接实现都是写在各自库里面,现在需要接入 WYBridge 实现底层统一,各端的方案各有不同,下面具体讲解。

Android

旧的 RN 桥接先是继承 ReactPackage 创建一个原生模块包用来添加原生模块,后通过继承 ReactContextBaseJavaModule 创建原生模块,复写 getName 方法设置模块名,对 public 方法添加 @ReactMethod 注解表示 RN 端可调用该方法。然后在应用初始化时添加该原生模块包,这样在 RN 创建 ReactContext 时,会解析该包,从而解析里面的原生模块,创建 JavaScript Module 注册表,方便 JS 层调用。整个流程如下:

由上图可知整个流程对我们接入 WYBridge 最主要的是 JavaModuleWrapper,里面解析了模块的名称以及 @ReactMethod 注解的方法,所以我们需要修改里面的解析用来支持 WYBridge,但是 JavaModuleWrapper 又是在 NativeModuleRegistry 里面 new 的,而 NativeModuleRegistry 则是在创建 reactContext 中创建的,中间过程无法介入修改。幸好 CatalystInstanceImpl 提供了 extendNativeModules 方法,通过修改 NativeModuleRegistry 注册 Modules。故新的流程图如下:

下面具体介绍一下 Android RN 桥接 WYBridge 的实现。

注册

注册由原来从 RN 初始化时注册改为初始化完成时注册。

reactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {    @Override    public void onReactContextInitialized(ReactContext context) {        //注册 modules    }});

复制代码

注册时会解析 RN 的 Package,Package 就是 WYBridge 中所有 module 的集合。将 BridgeModule 以 moduleName 为 key 生成 Map 表。

将生成的 Map 传入到注册类中,这个注册类继承 NativeModuleRegistry。最后调用 CatalystInstanceImpl 中的 extendNativeModules 方法进行注册。

//extendNativeModules 注册 modulepublic void extendNativeModules(NativeModuleRegistry modules) {    ...    Collection<JavaModuleWrapper> javaModules = modules.getJavaModules(this);    ...    this.jniExtendNativeModules(javaModules, cxxModules);}

复制代码

解析

在 extendNativeModules 中可以看到调用了 getJavaModules 方法,在里面将 module 转化成 RN 的模块解析类 JavaModuleWrapper。我们重写里面的方法解析以及 invoke 方法,原来 RN 在 JavaModuleWrapper 里解析 @ReactMethod,现在我们则解析 BridgeModule 中的 @BridgeMethod 注解。最后将注解的方法生成对应的 MethodWrapper 类,该类继承 NativeModule.NativeMethod。

调用

RN 端调用原生,实际上调用的是 JavaModuleWrapper 的 invoke 方法。根据传入的 methodId,找到前面生成的对应 MethodWrapper 类。将参数以及回调传入,调用 method 的 invoke 方法,即 WYBridge 中方法的调用。

method.invoke(receiver, params);

复制代码

兼容方案

因为 RN 项目不分团队,且上层有封装,即使改变底层实现,也可以统一上层的调用。所以并未做兼容方案,直接对原来代码进行修改。

现存问题

因为 RN 之前桥接是在初始化时注册的,现在是在初始化完成时注册,所以当显示 RN 页面时,可能存在该桥接还未注册完成的时候。目前没有特别好的方法避免,只能在显示 RN 页面时做了一些延迟。

iOS

注册

WYBridge 中,在 APP 启动后调用 load 方法时,调用 RCTRegisterModule

解析调用

由于 WYBridge 注册方式回调与其 SDK 规则一致,所以不用适配

Weex

Weex 和 RN 相似,也是在初始化的时候注册桥接。原先 Native 层同样在 Android 和 iOS 都有自己的一套桥接库,实现都是写在各自库里面,现在需要接入 WYBridge 实现底层统一,各端的方案也各有不同,下面具体讲解。

Android

旧的 Weex 注册桥接时,桥接类必须继承 WXModule,方法的解析在 TypeModuleFactory 中实现。注册时调用 registerModule 设置 moduleName 和对应的 Module,对 public 方法添加 @JSMethod 注解表示 Weex 可调用该方法。整个注册和调用流程如下:

上图可知方法的解析是通过 TypeModuleFactory,我们只要修改里面的解析方法,就可以实现 Weex 识别 WYBridge 的方法。而 WXSDKEngine.registerModule 中可以传入 ModuleFactory 进行注册,而 TypeModuleFactory 就是继承该类的,故我们可以重写 ModuleFactory 将其传入,进行注册。下面是整个新的流程:

下面具体介绍一下 Android Weex 桥接 WYBridge 的实现。

注册

注册由原来调用 registerModule(String moduleName, Class<? extends WXModule> moduleClass)改为调用 registerModule(String moduleName, ModuleFactory factory, boolean global),首先创建一个类继承 ModuleFactory,将 Module 进行封装传入注册。后面建立 JS 和 Native 的映射表之类就和原来的 Weex 一致。

解析

在这个新建的类中重写里面的解析过程。原来 Weex 在 TypeModuleFactory 里解析 @JSMethod,现在我们则解析 BridgeModule 中的 @BridgeMethod 注解,然后返回改方法的对应的 Invoker。

前面的 Web 过程就参考这里 weex 的解析过程。

调用

Weex 端调用原生,就是找到对应的 ModuleFactory,在该类中我们根据传入的方法名找到前面生成的 Invoker,在这里处理对应方法参数以及回调,具体参考 Weex 中 NativeInvokeHelper。最后调用 Invoker 的 invoke 方法,即 WYBridge 中方法的调用。

兼容方案

由于 Weex 层没有像 RN 那样团队单一,且上层有 JS 封装统一文件,所以需要对老的调用方式进行兼容。 其原理和 Web 一样,即在旧方法调用时有一个依赖关系,可对应新方法的 moduleName、methodName、参数的映射和回调参数的映射,具体的过程就不再过多的说明了,可参考 Web 的兼容过程。

iOS

注册

WYBridge 中,在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法以 Class 的方式 都已经被注册到 WYBridgeGetModuleClassesDic 保存着,循环执行注册方法即可。

解析调用

解析过程同以前 Weex 调用一致

兼容

其实原理和 Web 一样,即在旧方法调用时有一个依赖关系,可对应新方法的 moduleName、methodName、参数的映射和回调参数的映射, Hook Weex 的 WXJSCoreBridge 的 registerCallNativeModule 中调用的模块名和方法名 Hook Weex 的 WXBridgeMethod 的 invocationWithTarget:selector: 的方法 处理参数回调映射。 整体流程图如下:

Flutter

Flutter 就比较简单,我们新建了一个 wrapper_bridge 的插件,所有的调用都通过这个通道来桥接。在 Flutter 层,我们给每个 Module 都新建了一个 dart 文件,并对里面的方法进行包装,方法调用的名称规定为“module 名称.方法名称”,这样业务调用时则会更加的清晰,举个例子,如 wytest 模块,里面包含了一个 getData 方法:

//wytest.dartimport 'dart:async';
import 'package:flutter/services.dart';
class WYTest {  static const MethodChannel _channel =  const MethodChannel('bridge');    static const moduleName = 'wytest.';
  static Future<Map> get getData async {    final Map data = await _channel.invokeMethod(moduleName+'getData');    return data;  }}

复制代码

在业务代码中则可以引入这个文件,调用里面的方法:

import 'package:bridge/wytest.dart';

WYTest.getData.then((value){
  print(value.toString());
});

Flutter 层代码统一,对于 Native 层提供的 module 方法没有注册流程,只是在调用的时候,根据调用方法名称进行区分,所以中间并不需要修改。Flutter 使用平台通道和原生端进行通讯,下面是官网的一张图:

Android

已知 Flutter 调用原生方式是通过 MethodChannel 中 invokeMethod 方法名调用的,最终所有的方法都在原生的 onMethodCall 方法中做处理:

@Overridepublic void onMethodCall(MethodCall call, final MethodChannel.Result result) {}

MethodCall 中保存了调用的方法名称以及参数,MethodChannel.Result 则是回调。既然我们已经规定了方法调用名称格式为“module 名.方法名”,所以我们可以通过解析名称从而调用对应的 WYBridge 方法。 在初始化的时候,我们将 WYBridge 里面的 module 遍历生成 Map。

然后在方法调用时,根据传过来的方法名解析出 module 名称和实际调用的方法名称,通过 module 名称找到对应的模块。

知道了方法名以及实际调用的 moudle,我们就可以 invoke 方法。接下来就和前面的那些一样,传入参数和回调,调用 invoke 方法。

invoke 中我们看到,只要是 module 中有的方法都是可以被调用,这样可能会存在安全漏洞,所以我们规定允许被调用的函数必须有 @BridgeMethod 注解,故调用的时候需要进行解析,如下:

//获取调用的方法
Method m = moduleClazz.getMethod(methodName, new Class[]{...});
for (Annotation anno : me.getDeclaredAnnotations()) {
    //判断是@BridgeMethod 注解的才执行
    if(anno instanceof BridgeMethod) {
        m.invoke(...);
    }
}

这样我们就实现了 Android Flutter 桥接 WYBridge 底层的统一。

iOS

与其他三端一样,在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法以 Class 的方式 都已经被注册到 WYBridgeGetModuleClassesDic 保存着. 方法的调用在原生 Flutter 插件中的 handleMethodCall 方法中做处理

获取模块名.方法名通过解析找到对应的方法通过获取一个类的所有实例方法,将所有以“xx_export_method_”开头的方法返回保存在字典中,返回的是调用用的方法名.然后通过注册的时候保存的对象,生成该方法签名,设置调用对象,设置参数然后调用方法

总结及展望

目前 WYBridge 已经在微医和微医生项目中替换了部分 Bridge 投入使用,使用过程中未发现明显 bug,接下来我们将进一步将所有 Bridge 都替换完成,统一所有容器 Bridge API。 这次的方案涉及的都只是桥接方法的调用,对于属性等桥接并未涉及。而且 Android 因为 RN 和 Weex 都涉及到源码,iOS 在 Weex 中也有涉及,所以在升级时这几部分解析也需要跟着升级,并不是特别友好。 通过这次方案研究,进一步衍生开来,对于 RN、Weex、Flutter 一些原生组件桥接也可以得到统一,这是未来我们所要研究的,希望感兴趣的小伙伴和我们一起交流分享。

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/gjc55AGU5frJf2SYiiTP
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券