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

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"近几年随着 React Native、Flutter、Weex 等跨平台框架的流行,使得程序员可以尽量关注于业务本身,而非平台间的差异。但是不管哪一种方案,从移动端的角度看,都对底层桥接 API 有着共同的诉求。从 H5 到 React Native,再到 Weex 以及后面的 Flutter,原生进行了多轮的 API 重复建设,造成了缺少 API 接口的标准化定义,以及实现的统一管控的现状。所以针对这一情况,我们将统一所有容器 Bridge API,包括接口的定义,以及其底层原生代码。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"方案概况"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"旧方案流程介绍"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9f\/9fc36e25b11a618b5f91f0d86c9f490f.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以前需要桥接一个方法时,我们需要在 Web、RN、Weex、Flutter 各自写一套注册和实现,有时候往往因为需求时间的不同,或者开发人员的不同,导致相同功能的 Bridge 定义和实现也不一样,这不仅仅浪费了开发人员的时间精力,而且当 Web 需要换成 RN 或者 Weex 时,增加了替换的难度及风险。就算规划的好一点,统一了 Bridge 的实现和接口,但那时还是需要开发人员对各端都做注册实现,也是很浪费时间。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"新方案流程介绍"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5e\/5e06c292a4d4c7cc5725a4847fe2f86c.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们在基础容器层对各跨平台容器的 Bridge 层做了适配,主要是模块注册、Bridge 解析、调用以及兼容方案,具体实现后文会讲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"WYBridge"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我们先介绍下底层的 WYBridge 库,它包括了 4 端统一的桥接及实现。就像前面说的,我们以前的 bridge 都是各端写各自的,实现及接口定义都不统一,极不规范。而现在我们只需要在这个库中增加一个 module,上层 4 端会自动注册这个模块,这样各端的定义和实现都是用的 WYBridge 中的,实现了下层的统一。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Android"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Android 中的 WYBridge 的核心为 BridgeModule,提供给上层的模块都会继承该接口,里面提供了 4 端都会用到的方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public interface BridgeModule {\n  \/\/模块名称\n  String getName();\n}\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同时我们新建了 BridgeMethod 注解,标记为该模块提供给上层的方法(类似于 RN 中的@ReactMethod)。提供的方法必须符合下面两种模板中的一种:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@BridgeMethod\npublic void xxxx(JSONObject data, BridgeJSCallBack callBack){}\n\n@BridgeMethod\npublic void xxxx(BridgeJSCallBack callBack){}\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面就是一个简单的 BridgeModle 例子:"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"public class XXTestModule extends BaseBridgeModule {\n\n @Override\n public String getName() {\n return \"xxtest\";\n }\n \n @BridgeMethod\n public void getData(JSONObject data, final BridgeJSCallBack callBack){\n \/\/do something\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"各端通过解析这些模块,将其注册到自己的平台中。至此我们通过WYBridge实现了底层的统一。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"iOS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS 中的 WYBridge 的核心为宏定义文件,提供给上层的模块通过 "},{"type":"codeinline","content":[{"type":"text","text":"XX_EXPORT_MODULE(module_name)"}]},{"type":"text","text":"宏将该类以 module 的方式暴露给 JS,然后使用 "},{"type":"codeinline","content":[{"type":"text","text":"XX_EXPORT_METHOD(js_name)"}]},{"type":"text","text":"将 Native 方法暴露给 JS。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"模块注册"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"define XX_EXPORT_MODULE(module_name) \\\n    XX_EXTERN void XXRegisterWebModule(Class); \\\n    XX_EXTERN void XXRegisterWeexModule(Class); \\\n    XX_EXTERN void RCTRegisterModule(Class); \\\n    XX_EXTERN void XXRegisterFlutterModule(Class); \\\n    + (void)load {\\\n        XXRegisterWebModule(self);\\\n        XXRegisterWeexModule(self);\\\n        RCTRegisterModule(self);\\\n        XXRegisterFlutterModule(self);\\\n    }\\\n    + (NSString *)moduleName { return @# module_name; }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"注:load 里面会注册四个平台的RegisterXXModule,若没有对应平台的SDK,可以通过判断头文件注册声明一个空的RegisterXXModule"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上代码所示,XX_EXPORT_MODULE 宏背后是两个静态方法"},{"type":"codeinline","content":[{"type":"text","text":"+(NSString *)moduleName"}]},{"type":"text","text":" 和"},{"type":"codeinline","content":[{"type":"text","text":"+(NSString *)load。moduleName"}]},{"type":"text","text":" 方法简单的返回了 Native 模块的类名,load 方法是大家耳熟能详的的,load 方法调用 RegisterXXModule 函数注册了模块,我这里注册了 4 端,RegisterXXModule 函数的实现是参考 RCTRegisterModule(该函数定义在 RCTBridge.m 中)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void RCTRegisterModule(Class);\nvoid RCTRegisterModule(Class moduleClass)\n{\n  static dispatch_once_t onceToken;\n  dispatch_once(&onceToken, ^{\n    RCTModuleClasses = [NSMutableArray new];\n    RCTModuleClassesSyncQueue = dispatch_queue_create(\"com.facebook.react.ModuleClassesSyncQueue\", DISPATCH_QUEUE_CONCURRENT);\n  });\n...\n  \/\/ Register module\n  dispatch_barrier_async(RCTModuleClassesSyncQueue, ^{\n    [RCTModuleClasses addObject:moduleClass];\n  });\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很简单,RCTRegisterModule 函数只做了 3 件事: 1.创建一个全局的可变数组\/字典和一个队列(在 Web\/Weex\/Fluuter 我定义的是一个字典) 2.检查导出给 JS 模块是否遵守了 RCTBridgeModule 协议(由于该检查,需要在 WYBridge 写一个 RCTBridgeModule 的空协议,为了躲过检查,其他端可不作检查) 3.把要导出的类添加到全局的可变数组\/字段中进行记录 在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法都已经被注册到一个数组\/字典中。到此为止,只是把需要导出给 JS 的类记录下来."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"方法注册及实现"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"# define XX_EXPORT_METHOD(method) \\\nXX_EXPORT_METHOD_INTERNAL(@selector(method),xx_export_method_)\\\nRCT_REMAP_METHOD(, method)\n# define XX_EXPORT_METHOD_INTERNAL(method, token) \\\n + (NSString *)XX_CONCAT_WRAPPER(token, __LINE__) { \\\n return NSStringFromSelector(method); \\\n }\n# define RCT_REMAP_METHOD(js_name, method) \\\n _RCT_EXTERN_REMAP_METHOD(js_name, method, NO)\n# define _RCT_EXTERN_REMAP_METHOD(js_name, method, is_blocking_synchronous_method) \\\n+ (const RCTMethodInfo *)XX_CONCAT_WRAPPER(__rct_export__, XX_CONCAT_WRAPPER(js_name, XX_CONCAT_WRAPPER(__LINE__, __COUNTER__))) { \\\nstatic RCTMethodInfo config = {# js_name, # method, is_blocking_synchronous_method}; \\\n return &config; \\}\ntypedef struct RCTMethodInfo {\n const char *const jsName;\n const char *const objcName;\n const BOOL isSync;\n } RCTMethodInfo;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过上面一系列的宏调用不难看出,XX_EXPORT_METHOD 做了 3 件事"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"定义一个对象方法,用于真正调用的方法实现"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"定义了一个静态方法,该方法名为 xx_export_method___LINE__,返回值是注册方法名,用于 Web 和 Weex,会扫描所有导出的 nativemodule 中以 xx_export_method 的方法"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"定义一个静态方法,该方法名的格式是 "},{"type":"codeinline","content":[{"type":"text","text":"+(const RCTMethodInfo *)__rct_export__+js_name+___LINE__+__COUNTER__"}]},{"type":"text","text":",用于 RN 这个方法包装成了一个 RCTMethodInfo 对象,在运行时 RN 会扫描所有导出的 Native module 中以__rct_export__开头的方法。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上只是说了 module 和 method 是如何导出的,这些模块和方法的注册将会在各自模块中介绍。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"使用"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"\/\/模块注册\nXX_EXPORT_MODULE(moduleName)\n\/\/方法注册\nXX_EXPORT_METHOD(methodName:success:fail:)\n\/\/方法实现\n- (void)methodName:(NSDictionary *)data success:(XXBridgeResolveBlock)success\nfail:(XXBridgeRejectBlock)fail {\n     ...\n     success(resdic);\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"注:由于RN里会对XX_EXPORT_METHOD()中的参数做解析,然后取的方法名做跳转,而Web\/WEEX中直接取的方法名,所以方法注册的时候还没有统一,可以优化。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Web"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前不管是 Android 还是 iOS,都是由原生提供给 H5 一个统一的方法用于调用原生桥接模块,H5 把需要调用的桥接方法名以及参数以 json 字符串的方式传入。这些桥接在 webview 初始化的时候进行注册,注册时只包含方法名,没有模块名,native根据参数找到对应注册的方法进行调用,整个流程图如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/8c\/8ca9f8d919d456f9f82ce63bc9457f1a.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面说到 H5 只有方法名,但是 RN 和 Weex 都是以\"模块名.方法名\"的方式进行调用,所以为了和其他端统一,H5 调用原生桥接的方法名也需要加上模块名,并且初始化时,需将 WYBridge 中的桥接进行注册。新的流程图如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7f\/7ff9f965618131df4263a23a1be9ebab.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Android"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面具体介绍一下 Android Web 桥接 WYBridge 的实现。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"注册"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整体仿照weex和RN的注册。新增新的注册方式,将BridgeModule传入:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"\/\/新的注册方式\nboolean registerHandler(Class extends BridgeModule> moduleClass);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注册的时候,我们会将 BridgeModule 转化成一个管理类,该类提供了 3 个方法:module实例化、module方法解析和module方法的调用。调用模块实例化方法,创建 BridgeModule 实例,将前面的管理类和 module 实例都以键值对的方式生成 Map 表。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面管理类中module方法解析,实际上就是解析 module 中被@BridgeMethod 注解的方法, 获取到方法的 Invoker,最后的方法调用就是在这个Invoker里面实现的。"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"for (Method method : mClazz.getMethods()) {\n  for (Annotation anno : method.getDeclaredAnnotations()) {\n    if (anno instanceof BridgeMethod) {\n      String name = method.getName();\n      methodMap.put(name, new Invoker(method));\n      ...\n      break;\n    }\n  }\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"调用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"已知 H5 调用都是通过统一的方法,最终会到一个处理 JS 调用 Native 数据类。在这个处理类中,我们将 H5 传入的数据进行解析。调用新的 bridge 方法时,我们和 H5 约定方法名传入为“模块名.方法名”,所以通过解析,我们可以得到对应的模块名和方法名。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样根据模块名找到存在上面 Map 中该模块的实例和其管理类,又在管理类中根据方法名可以获取前面解析得到的该方法对应的Invoker。最后我们将传入的参数和回调一同传入Invoker中,调用里面method的invoke方法,即 WYBridge 中方法的调用。"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"method.invoke(receiver, params);\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"兼容方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因为原来 H5 都是用过方法名的方式调用原生,现改成“模块名.方法名”,旧的调用模块将不再适用。但是 H5 团队繁多,没有统一的调用基类,由前端一处一处的改动是不现实的,所以需要做到兼容老的版本。 目前 Android 这边的处理方案是,对于旧的桥接方法,新建一个对应的处理类,里面包含了如下 5 个接口:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"\/\/旧的方法名\nString aliasName();\n\/\/新的模块名\nString bridgeModuleName();\n\/\/新的方法名\nString bridgeMethodName();\n\/\/入参转换\nJSONObject mapping(String data);\n\/\/回调参数转换\nString backMapping(String data, int code, String msg);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 webView 初始化的时候也将这些处理注册到一个 Map,以旧的方法名为 Key。当 H5 还是调用老的方法时,通过这个类找到对应新的模块名和方法名,从而进行调用,整个流程如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b4\/b4133e4e9a51aa16051075b709f075cb.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"iOS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面具体介绍一下 iOS Web 桥接 WYBridge 的实现。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"注册"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WYBridge 中,在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法以 class 的方式 都已经被注册到字典保存着,循环执行注册方法即可."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以前是维护一份以 methodName 为 key,原生代码实现的 block 为 value 的字典,现是以 moduleName.methodName 为 key, 通过 NSInvocation 封装了方法调用对象、方法选择器、参数、返回值等的 block 为 value 的字典."}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"[self registerApi:newMethod block:^(XXHandlerModel *handlerModel) {\n \/\/ 1、通过传入handlerModel获取 方法名、参数\n \/\/ 2、通过 methodName 获取 selector\n \/\/ 3、通过注册的时候保存的对象,生成该方法签名,设置调用对象,设置参数然后调用方法\n   }];"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"调用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们和 H5 约定方法名传入为“模块名.方法名”,我们可以得到对应的模块名和方法名,通过获取一个类的所有实例方法,将所有以“xx_export_method_”开头的方法返回保存在字典中,返回的是调用用的方法名."}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"- (NSDictionary *)clazzMethodFactory {\n    ...\n    \/\/1、通过class_copyMethodList获取所有方法\n    \/\/2、返回以xx_export_method_开头方法字典\n\n}"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"兼容方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS 这边的处理方案同 Android,新建一个对应的处理类 XXBridgeFactoryMapping,处理新老方法名字、入参、回调映射。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"React Native"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RN 中我们给每一个 Module 都封装了一个 ts 文件,用来统一上层 RN 端调用的接口,业务代码引用的时候直接用该方法就行。举个例子,如 wytest 模块,里面包含了一个 getData 方法:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/WYNativeTest.ts\nimport {NativeModules} from 'react-Native';\n\nlet WYTestModule = NativeModules.wytest;\n\nexport var WYNativeTest = {\n    getData(): Promise {\n    return WYTestModule.getData().then((value: any) => {\n        return value;\n    })\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 Native 层,Android 和 iOS 都有自己的一套桥接库,在应用初始化的时候进行注册。以前这些桥接实现都是写在各自库里面,现在需要接入 WYBridge 实现底层统一,各端的方案各有不同,下面具体讲解。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Android"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"旧的 RN 桥接先是继承 ReactPackage 创建一个原生模块包用来添加原生模块,后通过继承 ReactContextBaseJavaModule 创建原生模块,复写 getName 方法设置模块名,对 public 方法添加@ReactMethod 注解表示 RN 端可调用该方法。然后在应用初始化时添加该原生模块包,这样在 RN 创建 ReactContext 时,会解析该包,从而解析里面的原生模块,创建 JavaScript Module 注册表,方便 JS 层调用。整个流程如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/92\/92eb6128e7df1c6cec1ab793b8445a81.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上图可知整个流程对我们接入 WYBridge 最主要的是 JavaModuleWrapper,里面解析了模块的名称以及@ReactMethod 注解的方法,所以我们需要修改里面的解析用来支持 WYBridge,但是 JavaModuleWrapper 又是在 NativeModuleRegistry 里面 new 的,而 NativeModuleRegistry 则是在创建 reactContext 中创建的,中间过程无法介入修改。幸好 CatalystInstanceImpl 提供了 extendNativeModules 方法,通过修改 NativeModuleRegistry 注册 Modules。故新的流程图如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f7\/f7fae71adc8fa46ecd776bf05181ad9d.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面具体介绍一下 Android RN 桥接 WYBridge 的实现。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"注册"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注册由原来从 RN 初始化时注册改为初始化完成时注册。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"reactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {\n    @Override\n    public void onReactContextInitialized(ReactContext context) {\n        \/\/注册 modules\n    }\n});\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注册时会解析RN的Package,Package就是WYBridge中所有module的集合。将 BridgeModule以moduleName为key生成 Map 表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"将生成的 Map 传入到注册类中,这个注册类继承NativeModuleRegistry。最后调用 CatalystInstanceImpl 中的 extendNativeModules 方法进行注册。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/extendNativeModules 注册 module\npublic void extendNativeModules(NativeModuleRegistry modules) {\n    ...\n    Collection javaModules = modules.getJavaModules(this);\n    ...\n    this.jniExtendNativeModules(javaModules, cxxModules);\n}\n\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在extendNativeModules中可以看到调用了getJavaModules方法,在里面将module转化成RN的模块解析类JavaModuleWrapper。我们重写里面的方法解析以及 invoke 方法,原来 RN 在 JavaModuleWrapper 里解析@ReactMethod,现在我们则解析 BridgeModule 中的@BridgeMethod 注解。最后将注解的方法生成对应的MethodWrapper类,该类继承NativeModule.NativeMethod。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"调用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RN 端调用原生,实际上调用的是 JavaModuleWrapper 的 invoke 方法。根据传入的methodId,找到前面生成的对应MethodWrapper类。将参数以及回调传入,调用method的invoke方法,即 WYBridge 中方法的调用。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"method.invoke(receiver, params);\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"兼容方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因为 RN 项目不分团队,且上层有封装,即使改变底层实现,也可以统一上层的调用。所以并未做兼容方案,直接对原来代码进行修改。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"现存问题"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因为 RN 之前桥接是在初始化时注册的,现在是在初始化完成时注册,所以当显示 RN 页面时,可能存在该桥接还未注册完成的时候。目前没有特别好的方法避免,只能在显示 RN 页面时做了一些延迟。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"iOS"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"注册"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WYBridge 中,在 APP 启动后调用 load 方法时,调用 RCTRegisterModule"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解析调用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于 WYBridge 注册方式回调与其 SDK 规则一致,所以不用适配"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Weex"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Weex 和 RN 相似,也是在初始化的时候注册桥接。原先 Native 层同样在 Android 和 iOS 都有自己的一套桥接库,实现都是写在各自库里面,现在需要接入 WYBridge 实现底层统一,各端的方案也各有不同,下面具体讲解。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Android"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"旧的 Weex 注册桥接时,桥接类必须继承 WXModule,方法的解析在 TypeModuleFactory 中实现。注册时调用 registerModule 设置 moduleName 和对应的 Module,对 public 方法添加@JSMethod 注解表示 Weex 可调用该方法。整个注册和调用流程如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/fd\/fdd9581cda0b28669b15d62a10ef0ad7.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上图可知方法的解析是通过 TypeModuleFactory,我们只要修改里面的解析方法,就可以实现 Weex 识别 WYBridge 的方法。而 WXSDKEngine.registerModule 中可以传入 ModuleFactory 进行注册,而 TypeModuleFactory 就是继承该类的,故我们可以重写 ModuleFactory 将其传入,进行注册。下面是整个新的流程:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/30\/30293c5e6c83200b336f8e5236e2b890.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面具体介绍一下 Android Weex 桥接 WYBridge 的实现。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"注册"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注册由原来调用 "},{"type":"codeinline","content":[{"type":"text","text":"registerModule(String moduleName, Class extends WXModule> moduleClass)"}]},{"type":"text","text":"改为调用 "},{"type":"codeinline","content":[{"type":"text","text":"registerModule(String moduleName, ModuleFactory factory, boolean global)"}]},{"type":"text","text":",首先创建一个类继承 ModuleFactory,将 Module 进行封装传入注册。后面建立 JS 和 Native 的映射表之类就和原来的 Weex 一致。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在这个新建的类中重写里面的解析过程。原来 Weex 在 TypeModuleFactory 里解析@JSMethod,现在我们则解析 BridgeModule 中的@BridgeMethod 注解,然后返回改方法的对应的 Invoker。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面的Web过程就参考这里weex的解析过程。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"调用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Weex 端调用原生,就是找到对应的ModuleFactory,在该类中我们根据传入的方法名找到前面生成的Invoker,在这里处理对应方法参数以及回调,具体参考 Weex 中 NativeInvokeHelper。最后调用Invoker 的 invoke 方法,即 WYBridge 中方法的调用。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"兼容方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于 Weex 层没有像 RN 那样团队单一,且上层有 JS 封装统一文件,所以需要对老的调用方式进行兼容。 其原理和 Web 一样,即在旧方法调用时有一个依赖关系,可对应新方法的 moduleName、methodName、参数的映射和回调参数的映射,具体的过程就不再过多的说明了,可参考 Web 的兼容过程。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"iOS"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"注册"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WYBridge 中,在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法以 Class 的方式 都已经被注册到 WYBridgeGetModuleClassesDic 保存着,循环执行注册方法即可。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解析调用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解析过程同以前 Weex 调用一致"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"兼容"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其实原理和 Web 一样,即在旧方法调用时有一个依赖关系,可对应新方法的 moduleName、methodName、参数的映射和回调参数的映射, Hook Weex 的 WXJSCoreBridge 的 registerCallNativeModule 中调用的模块名和方法名 Hook Weex 的 WXBridgeMethod 的 invocationWithTarget:selector: 的方法 处理参数回调映射。 整体流程图如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3a\/3aa45b9e53970d8d99a0c25b83c5752f.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Flutter"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter 就比较简单,我们新建了一个 wrapper_bridge 的插件,所有的调用都通过这个通道来桥接。在 Flutter 层,我们给每个 Module 都新建了一个 dart 文件,并对里面的方法进行包装,方法调用的名称规定为“module 名称.方法名称”,这样业务调用时则会更加的清晰,举个例子,如 wytest 模块,里面包含了一个 getData 方法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/wytest.dart\nimport 'dart:async';\n\nimport 'package:flutter\/services.dart';\n\nclass WYTest {\n  static const MethodChannel _channel =\n  const MethodChannel('bridge');\n  \n  static const moduleName = 'wytest.';\n\n  static Future get getData async {\n    final Map data = await _channel.invokeMethod(moduleName+'getData');\n    return data;\n  }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在业务代码中则可以引入这个文件,调用里面的方法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import 'package:bridge\/wytest.dart';\n\nWYTest.getData.then((value){\n  print(value.toString());\n});\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter 层代码统一,对于 Native 层提供的 module 方法没有注册流程,只是在调用的时候,根据调用方法名称进行区分,所以中间并不需要修改。Flutter 使用平台通道和原生端进行通讯,下面是官网的一张图:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/eb\/ebbd1c9b4c440880458720638ffa54c7.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Android"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"已知 Flutter 调用原生方式是通过 MethodChannel 中 invokeMethod 方法名调用的,最终所有的方法都在原生的 onMethodCall 方法中做处理:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@Override\npublic void onMethodCall(MethodCall call, final MethodChannel.Result result) {}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MethodCall 中保存了调用的方法名称以及参数,MethodChannel.Result 则是回调。既然我们已经规定了方法调用名称格式为“module 名.方法名”,所以我们可以通过解析名称从而调用对应的 WYBridge 方法。 在初始化的时候,我们将 WYBridge 里面的 module 遍历生成 Map。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后在方法调用时,根据传过来的方法名解析出 module 名称和实际调用的方法名称,通过 module 名称找到对应的模块。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"知道了方法名以及实际调用的 moudle,我们就可以 invoke 方法。接下来就和前面的那些一样,传入参数和回调,调用invoke方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"invoke 中我们看到,只要是 module 中有的方法都是可以被调用,这样可能会存在安全漏洞,所以我们规定允许被调用的函数必须有@BridgeMethod 注解,故调用的时候需要进行解析,如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/获取调用的方法\nMethod m = moduleClazz.getMethod(methodName, new Class[]{...});\nfor (Annotation anno : me.getDeclaredAnnotations()) {\n    \/\/判断是@BridgeMethod 注解的才执行\n    if(anno instanceof BridgeMethod) {\n        m.invoke(...);\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样我们就实现了 Android Flutter 桥接 WYBridge 底层的统一。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"iOS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"与其他三端一样,在 APP 启动后调用 load 方法时,所有需要暴露给 JS 的方法以 Class 的方式 都已经被注册到 WYBridgeGetModuleClassesDic 保存着. 方法的调用在原生 Flutter 插件中的 handleMethodCall 方法中做处理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"获取模块名.方法名通过解析找到对应的方法通过获取一个类的所有实例方法,将所有以“xx_export_method_”开头的方法返回保存在字典中,返回的是调用用的方法名.然后通过注册的时候保存的对象,生成该方法签名,设置调用对象,设置参数然后调用方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"总结及展望"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前 WYBridge 已经在微医和微医生项目中替换了部分 Bridge 投入使用,使用过程中未发现明显 bug,接下来我们将进一步将所有 Bridge 都替换完成,统一所有容器 Bridge API。 这次的方案涉及的都只是桥接方法的调用,对于属性等桥接并未涉及。而且 Android 因为 RN 和 Weex 都涉及到源码,iOS 在 Weex 中也有涉及,所以在升级时这几部分解析也需要跟着升级,并不是特别友好。 通过这次方案研究,进一步衍生开来,对于 RN、Weex、Flutter 一些原生组件桥接也可以得到统一,这是未来我们所要研究的,希望感兴趣的小伙伴和我们一起交流分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"头图:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:殷利萍,黄丽丽"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/jq_mavgLnt8wPNemTbpoag"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:如何做到四端统一桥接?微医跨平台桥接标准化方案了解一下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"来源:微医大前端技术 - 微信公众号 [ID:wed_fed]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章