Yin Liping, iOS engineer of Wedoctor Cloud Service team

Huang Lili, Android engineer of Wedoctor Cloud service team, loves traveling and food.

The popularity of cross-platform frameworks such as React Native, Flutter, and Weex in recent years has allowed programmers to focus on the business itself rather than the differences between platforms. But no matter which solution, from a mobile perspective, the underlying bridge API has the same appeal. From H5, React Native, Weex and Flutter, the Native API has been repeatedly constructed for several rounds, resulting in the lack of standardized definition of API interface and unified management and control. So in this case, we will unify all container Bridge apis, including the definition of the interface and its underlying native code.

outline

This section describes the process of the old solution

In the past, to Bridge a method, we needed to write a set of registration and implementation on Web, RN, Weex, and Flutter respectively. Sometimes, due to different requirements or different developers, the Bridge definition and implementation of the same function were different. This not only wasted the developer’s time and effort. And when the Web needs to be replaced by RN or Weex, the replacement becomes more difficult and risky. Even if the planning was better and the Bridge implementation and interface were unified, it would still be a waste of time for developers to register implementations on all sides.

This section describes the new solution process

In the basic container layer, we have adapted the Bridge layer of each cross-platform container, mainly including module registration, Bridge resolution, invocation and compatibility scheme. The specific implementation will be discussed later.

WYBridge

First, let’s take a look at the underlying WYBridge library, which includes four unified Bridges and implementations. As mentioned above, our previous Bridges were written independently at each end, and the implementation and interface definition were not uniform and very non-standard. Now we only need to add a module to the library, and the upper four ends will automatically register this module, so that the definition and implementation of each end are in WYBridge, to achieve the unification of the lower layer.

Android

The core of WYBridge in Android is the BridgeModule, which provides the interface for all modules.

public interface BridgeModule {
  // Module name
  String getName(a);
}

Copy the code

We also created a BridgeMethod annotation to mark the methods that this module provides to the upper layer (similar to @reactMethod in RN). The provided methods must conform to one of the following two templates:

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

@BridgeMethod
public void xxxx(BridgeJSCallBack callBack){}
 Copy the code

Here is a simple BridgeModle example:

public class XXTestModule extends BaseBridgeModule {

    @Override
    public String getName(a) {
        return "xxtest";
 }   @BridgeMethod  public void getData(JSONObject data, final BridgeJSCallBack callBack){  //do something  } }  Copy the code

Each side parses these modules and registers them with its own platform. At this point we have achieved the underlying unification through WYBridge.

iOS

The core of WYBridge in iOS is the macro definition file, which is provided to upper-level modules through the XX_EXPORT_MODULE(module_name) macro to expose the class as module to JS. The Native method is then exposed to JS using XX_EXPORT_METHOD(js_name).

Module registration

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; } Copy the code

Note: Load registers four platform registerxxModules. If there is no SDK for each platform, declare an empty RegisterXXModule by checking the header file registration

As shown in the code above, behind the XX_EXPORT_MODULE macro are two static methods +(NSString *)moduleName and +(NSString *) LOAD. The moduleName method simply returns the class name of the Native module, and the load method is well-known. The load method calls the RegisterXXModule function to register the module. I registered the four ends of the RegisterXXModule function. The implementation of the RegisterXXModule function refers to RCTRegisterModule(this function is defined in 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];  }); } Copy the code

Quite simply, the RCTRegisterModule function does three things: 1. Create a global mutable array/dictionary and a queue (in Web/Weex/Fluuter I defined a dictionary) 2. Check whether the exported JS module complies with the RCTBridgeModule protocol. (Because of this check, a null RCTBridgeModule protocol needs to be written in WYBridge. When the load method is called after APP startup, all methods that need to be exposed to JS have been registered in an array/dictionary. At this point, I’m just writing down the classes that need to be exported to JS.

Method registration and implementation

# 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; The \}typedef struct RCTMethodInfo {  const char *const jsName;  const char *const objcName;  const BOOL isSync;  } RCTMethodInfo; Copy the code

As you can see from the sequence of macro calls above, XX_EXPORT_METHOD does three things

  1. Defines an object method for the method implementation that is actually called
  2. Defines a static method named xx_export_method___LINE__. The return value is the registered method name, used in the Web and Weex, that scans all exported nativemodule methods as xx_export_method
  3. Define a static method whose name is of the format+(const RCTMethodInfo *)__rct_export__+js_name+___LINE__+__COUNTER__RN wraps this method into an RCTMethodInfo object, and at run time RN scans all exported Native Module methods starting with __rct_export__.

This is just how module and method are exported. Registration of these modules and methods will be covered in their respective modules.

use

// Module registration
XX_EXPORT_MODULE(moduleName)
// Method registration
XX_EXPORT_METHOD(methodName:success:fail:)
// Method implementation
- (void)methodName:(NSDictionary *)data success:(XXBridgeResolveBlock)success fail:(XXBridgeRejectBlock)fail { . success(resdic);  } Copy the code

XX_EXPORT_METHOD() = XX_EXPORT_METHOD() = XX_EXPORT_METHOD() = XX_EXPORT_METHOD();

Web

Previously, no matter Android or iOS, native provides H5 with a unified method to call the native bridge module. H5 passes in the name of the bridge method and parameters to be called in the form of JSON string. These Bridges are registered during webView initialization, which only contains the method name but not the module name. Native finds the corresponding registered method to call according to the parameters. The whole flow chart is as follows:


H5 has only a method name, but RN and Weex are both module names. Method name “, so in order to be consistent with other ends, the method name of H5 calling the native bridge also needs to add the module name, and the bridge in WYBridge should be registered during initialization. The new flow chart is as follows:

Android

The following is a detailed introduction to the implementation of Android Web bridge WYBridge.

registered

The whole is modeled after weeX and RN registration. Add a new registration method and pass in BridgeModule:

// New registration method
boolean registerHandler(Class<? extends BridgeModule> moduleClass);
Copy the code

At registration time, we convert BridgeModule to a management class that provides three methods: Module instantiation, Module method resolution, and Module method invocation. Call the module instantiation method to create an instance of BridgeModule, which generates the Map table as key-value pairs for both the previous management class and module instances.

parsing

The module method resolution in the previous management class is actually the resolution of the @bridgemethod annotated method in the Module, get the method Invoker, the last method call is implemented in the 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;  }  } } Copy the code

call

It is known that H5 calls are all through a unified method and will eventually reach a processing JS call Native data class. In this processing class, we parse the data passed in by H5. When we call the new Bridge method, we and H5 agree that the method name is passed as “module name.” Method name “, so through parsing, we can get the corresponding module name and method name.

In this way, the instance and its management class of the module in the Map can be found according to the module name, and the Invoker corresponding to the method can be obtained according to the method name in the management class. Finally, we pass the parameters to the Invoker along with the callback, calling the Invoke method of the method inside the Invoker, the WYBridge method invocation.

method.invoke(receiver, params);
Copy the code

Compatible with the plan

Because the original H5 are used to call the method name of the native, now change to “module name. Method name “, the old calling module will no longer apply. However, the H5 team is diverse, there is no unified call base class, and it is not realistic to change one by one from the front end, so it needs to be compatible with the old version. The current solution on Android side is to create a corresponding processing class for the old bridge method, which contains the following 5 interfaces:

// The old method name
String aliasName(a);
// The new module name
String bridgeModuleName(a);
// The new method name
String bridgeMethodName(a); // Input parameter conversion JSONObject mapping(String data); // Callback parameter conversion String backMapping(String data, int code, String msg); Copy the code

These processes are also registered to a Map at webView initialization with the old method name Key. When H5 still calls the old method, find the corresponding module name and method name through this class, and then call, the whole process is as follows:

iOS

The following is a detailed introduction to the implementation of iOS Web bridge WYBridge.

registered

In WYBridge, when the load method is called after APP startup, all the methods that need to be exposed to JS have been registered in the dictionary in the way of class, and the registration method can be cyclically executed.

ModuleName. MethodName is used as a key, and moduleName. MethodName is used as a key, and moduleName. NSInvocation encapsulates a dictionary of block values for method invocation objects, method selectors, parameters, return values, etc.

[self registerApi:newMethod block:^(XXHandlerModel *handlerModel) {
 // get the method name and parameters by passing in the handlerModel
 // 2. Get selector from methodName
 // 3, generate the method signature, set the call object, set the parameters and call the method through the object saved when registering
   }];
 Copy the code

call

We agree with H5 that the method name is passed as “module name. Method name “, we can get the corresponding module name and method name, by getting all instance methods of a class, save all method returns starting with “xx_export_method_” in the dictionary, and return the method name with which the call was made.

- (NSDictionary *)clazzMethodFactory {
.    Get all methods from class_copyMethodList
    //2. Return the method dictionary starting with xx_export_method_

}  Copy the code

Compatible with the plan

Create a new class XXBridgeFactoryMapping to handle old and new method names, input parameters, and callback mappings.

React Native

In RN, we have encapsulated a TS file for each Module, which is used to unify the interface called by the upper RN side. The business code can directly use this method when referencing. For example, the wytest module contains a getData method:

//WYNativeTest.ts
import {NativeModules} from 'react-Native';

let WYTestModule = NativeModules.wytest;

export var WYNativeTest = {  getData(): Promise<any> {  return WYTestModule.getData().then((value: any) = > {  return value;  })  } } Copy the code

The Native layer, Android and iOS have their own set of bridge libraries, which are registered when the application is initialized. In the past, these bridge implementations were written in their own libraries, but now they need to access WYBridge to achieve the underlying unification. The schemes at each end are different, and the details are explained below.

Android

Old RN bridge first inherited ReactPackage packages to create a primary module add native modules, after through inheritance ReactContextBaseJavaModule create native modules, autotype getName method set up the module name, Annotating a public method with @reactMethod means that the RN side can call the method. The native Module package is then added during application initialization, so that when RN creates the ReactContext, the package will be parsed, and the native modules in it will be parsed, creating a JavaScript Module registry for easy JS layer invocation. The whole process is as follows:

The JavaModuleWrapper, which interprets the module name and @reactMethod annotation, is the most important thing for us to access WYBridge. Therefore, we need to modify the resolution to support WYBridge. However, JavaModuleWrapper is new in NativeModuleRegistry, which is created in the reactContext and cannot be modified. Fortunately, CatalystInstanceImpl provides extendNativeModules methods to register Modules by modifying NativeModuleRegistry. So the new flow chart is as follows:

The following is a detailed introduction to the implementation of Android RN bridge WYBridge.

registered

Registration was changed from registration at initialization of RN to registration at completion of initialization.

reactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
    @Override
    public void onReactContextInitialized(ReactContext context) {
        / / register modules
    }
});  Copy the code

RN’s Package, which is the collection of all modules in WYBridge, is parsed during registration. The BridgeModule uses moduleName as the key to generate a Map table.

Pass the generated Map into the registry class, which inherits NativeModuleRegistry. Finally, the extendNativeModules method in CatalystInstanceImpl is called for registration.

/ / extendNativeModules registration module
public void extendNativeModules(NativeModuleRegistry modules) {
.    Collection<JavaModuleWrapper> javaModules = modules.getJavaModules(this);
. this.jniExtendNativeModules(javaModules, cxxModules); }  Copy the code

parsing

In extendNativeModules you can see that the getJavaModules method is called, where module is converted to RN’s module resolution class JavaModuleWrapper. Instead of resolving @reactMethod in JavaModuleWrapper, RN now resolves @bridgemethod in BridgeModule. Finally the annotation method to generate the corresponding MethodWrapper class, the class inheritance NativeModule. NativeMethod.

call

The RN side calls native, which actually calls the JavaModuleWrapper invoke method. Based on the methodId passed in, find the corresponding MethodWrapper class generated earlier. Call method’s Invoke method, the WYBridge method, by passing in the parameters and the callback.

method.invoke(receiver, params);
Copy the code

Compatible with the plan

Because RN projects are teamless and have encapsulation at the top, it is possible to unify calls at the top even if the underlying implementation is changed. So there is no compatibility scheme, directly modify the original code.

Existing problems

Because RN Bridges were previously registered at initialization, they are now registered when initialization is complete, so when an RN page is displayed, there may be times when the bridge has not been registered. There is no particularly good way to avoid this, except for some delay in displaying RN pages.

iOS

registered

In WYBridge, RCTRegisterModule is called when the load method is called after APP startup

Resolution calls

Since the WYBridge registration callback is consistent with the SDK rules, no adaptation is required

Weex

Weex is similar to RN in that it registers Bridges during initialization. The original Native layer also has its own set of bridge libraries in Android and iOS, and the implementation is written in their own libraries. Now it is necessary to access WYBridge to realize the unification of the bottom layer, and the schemes of each end are also different. The details are explained below.

Android

When the old Weex bridge was registered, the bridge class had to inherit from WXModule, and the method resolution was implemented in TypeModuleFactory. Call registerModule at registration time to set moduleName and the corresponding Module. Add @jsMethod annotation to public method to indicate Weex can call this method. The whole registration and invocation process is as follows:

The TypeModuleFactory method is used to resolve the Weex WYBridge. If you modify the TypeModuleFactory method, Weex can recognize WYBridge. And WXSDKEngine. RegisterModule can pass in the ModuleFactory register, and TypeModuleFactory is the inheritance of the class, so we can rewrite ModuleFactory its incoming, to register. Here’s the whole new process:

The following is a detailed introduction to the implementation of Android Weex bridge WYBridge.

registered

RegisterModule (String moduleName, Class
moduleClass) to call registerModule(String moduleName, ModuleFactory Factory, Boolean global) First create a class that inherits the ModuleFactory, passing in the Module wrapper for registration. After the establishment of JS and Native mapping table and the same as the original Weex.

parsing

Rewrite the parsing in this new class. Weex parses @jsMethod in the TypeModuleFactory. Now we parse @bridgemethod in the BridgeModule and return the Invoker corresponding to the modified method.

The previous Web procedures refer to the weeX parsing process here.

call

In this class, we find the previously generated Invoker based on the passed method name. In this class, we process the corresponding method parameters and callback. For details, see NativeInvokeHelper in Weex. Finally, the Invoke method of Invoker, the invocation of the method in WYBridge, is called.

Compatible with the plan

Since the Weex layer is not as monolithic as RN and has JS wrapped uniform files on top, it needs to be compatible with older invocation methods. The principle is the same as that of the Web, that is, there is a dependency relationship when the old method is called, which can correspond to the moduleName, methodName, parameter mapping and callback parameter mapping of the new method. The specific process is not too much explained, but can refer to the Compatibility process of the Web.

iOS

registered

WYBridge, call the load method, after the APP start all exposed to the JS is needed in the form of a Class has been registered with the WYBridgeGetModuleClassesDic preserved, circulation can perform registration method.

Resolution calls

The parsing process is the same as the previous Weex call

Compatible with

It works the same way on the Web, where there is a dependency on the old method being called that corresponds to the moduleName, methodName, parameter mapping, and callback parameter mapping of the new method. Hook Weex’s WXBridgeMethod registerCallNativeModule InvocationWithTarget: selector: callback method processing parameter mapping. The overall flow chart is as follows:

Flutter

For Flutter, we created a plugin for wrapper_bridge. All calls to Flutter are bridged through this channel. In the Flutter layer, we create a dart file for each Module and wrap the methods inside it with a method call named “Module name. The wytest module, for example, contains a getData method:

//wytest.dart
import '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;  } } Copy the code

In business code, you can import this file and call its methods:

import 'package:bridge/wytest.dart';

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

The codes of the Flutter layer are unified. There is no registration process for module methods provided by the Native layer. They are only distinguished according to the name of the calling method when they are called, so there is no need to modify them. The Flutter uses a platform channel to communicate with its native endpoint. Here is an image from the Flutter website:

Android

Given that the native method of Flutter calls is called by the invokeMethod method name in MethodChannel, all methods are handled in the native onMethodCall method:

@Override
public void onMethodCall(MethodCall call, final MethodChannel.Result result) {}
Copy the code

MethodCall holds the name of the called method and its parameters, and methodChannel. Result is the callback. Now that we have specified that the method call name format is “Module name”. Method name “, so we can call the corresponding WYBridge method by resolving the name. During initialization, we iterate through the modules in the WYBridge to generate a Map.

Then, when the method is called, the module name and the actual method name are resolved according to the passed method name, and the corresponding module is found by the module name.

Given the name of the method and the actual moudle to call, we can invoke the method. Next, just like the previous ones, the invoke method is called by passing in parameters and a callback.

In invoke, we can see that all methods in the Module can be called, which may cause security vulnerabilities. Therefore, we stipulate that the function allowed to be called must have @bridgemethod annotation, so it needs to be parsed when calling, as follows:

// Get the method called
Method m = moduleClazz.getMethod(methodName, newClass[]{... });for (Annotation anno : me.getDeclaredAnnotations()) {
    // The judgment is executed only with @bridgemethod annotations
    if(anno instanceof BridgeMethod) {
m.invoke(...) ; } } Copy the code

In this way we achieve the unity of Android Flutter bridging WYBridge underlying layer.

iOS

Like other three-terminal, call the load method, after the APP start all exposed to the JS is needed in the form of a Class has been registered with the WYBridgeGetModuleClassesDic preserved. Method calls are handled in the handleMethodCall method of the native Flutter plugin

By retrieving all instance methods of a class, save all method returns starting with “xx_export_method_” in the dictionary, and return the name of the method used for the call. Then the method signature is generated, the call object is set, the parameters are set, and the method is called from the object saved at registration time

Summary and Outlook

At present, WYBridge has replaced some Bridges in wedoctor and WeDoctor projects, and no obvious bugs have been found in the process of use. Next, we will further replace all Bridges and unify all container Bridge APIS. This scheme only involves the invocation of the bridge method, and does not involve the bridging of attributes. And Android because RN and Weex are involved in the source code, iOS in Weex also involved, so in the upgrade of these parts of the resolution also need to follow the upgrade, is not particularly friendly. Through this scheme research, it can be further derived to unify the bridge of some native components of RN, Weex and Flutter. This is what we will study in the future. We hope that interested partners can share with us.