This article is based on react NatveSeptember 2018 – revision 5version

In my last article “Thoroughly Understand the Mapping relationship between React Native and Android Native controls”, I have thoroughly analyzed the mapping relationship between RN components and Native controls. In this article, I briefly mentioned some communication principles, so I will explain the communication principles of RN in detail.

PS: Online interpretation of RN communication principle of the relevant articles are many, but the good and bad are intermingled, some code flooding, logic chaos, some too simple, unclear structure, some seriously outdated code, is not the current RN mainstream version of the source. The purpose of this article is to give readers a clear understanding of the latest RN communication principles. Let’s get started!

The principle is briefly

The principle of RN communication is simply that one party registers part of its methods into a mapping table, and the other party looks up and invokes corresponding methods in this mapping table, and jsBridge acts as the bridge between the two.

The source code parsing

According to the order of principle brief, I divide this section into two parts, one is the registration process starting from native (Java), the other is the call process starting from js, and some C++ content in jsBridge is interspersed in the middle.

The registration process

Take a look at an example from the official tutorial:

public class ToastModule extends ReactContextBaseJavaModule { public ToastModule(ReactApplicationContext reactContext) {  super(reactContext); } @Override public StringgetName() {
    return "ToastExample"; } @ReactMethod public void show(String message, Int duration) {/ / that can be invoked by js method Toast. The makeText (getReactApplicationContext (), the message, duration), show (); }}Copy the code

This example is simplified a bit. It simply registers a NativeModule (NativeModule) for js to call and then play Toast.

public class CustomToastPackage implements ReactPackage {

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    returnCollections.emptyList(); } @Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new ToastModule(reactContext)); // added to the ReactPackagereturnmodules; }}Copy the code
// YourActivity.java
mReactInstanceManager = ReactInstanceManager.builder()
      .setApplication(getApplication())
      .setBundleAssetName("index.android.bundle")
      .setJSMainModulePath("index").addPackage(new MainReactPackage()).addPackage(new CustomToastPackage()) // passed into the ReactInstanceManager .setUseDeveloperSupport(BuildConfig.DEBUG) .setInitialLifecycleState(LifecycleState.RESUMED) .build()Copy the code

The above code is from the official tutorial.

As you can see from the code above, the NativeModule is added to the ReactPackage and passed into the ReactInstanceManager. Anyone who has written RN is certainly familiar with ReactInstanceManager. Writing an Activity in RN is bound to instantiate ReactInstanceManager. RN on the Android side does almost all of its communication logic inside it.

Next, start the source code analysis:

// ReactInstanceManager.java

NativeModuleRegistry nativeModuleRegistry = processPackages(reactContext, mPackages, false); CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder() .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault()) .setJSExecutor(jsExecutor) .setregistry (nativeModuleRegistry)// nativeModuleRegistry will be called in CatalystInstanceImpl. SetJSBundleLoader (jsBundleLoader) .setNativeModuleCallExceptionHandler(exceptionHandler); private NativeModuleRegistry processPackages( ReactApplicationContext reactContext, List<ReactPackage> packages, boolean checkAndUpdatePackageMembership) { NativeModuleRegistryBuilder nativeModuleRegistryBuilder = new NativeModuleRegistryBuilder(reactContext, this); .for(ReactPackage reactPackage : Packages) {/ / ReactPackage all incoming NativeModuleRegistry processPackage (ReactPackage nativeModuleRegistryBuilder); }... NativeModuleRegistry nativeModuleRegistry; nativeModuleRegistry = nativeModuleRegistryBuilder.build(); .returnnativeModuleRegistry; } private void processPackage( ReactPackage reactPackage, NativeModuleRegistryBuilder nativeModuleRegistryBuilder) { ... nativeModuleRegistryBuilder.processPackage(reactPackage); . }Copy the code

So that’s part of the code in ReactInstanceManager, and you can see that the ReactPackage is passed into NativeModuleRegistry, and NativeModuleRegistry is a mapping table, All registered NativeModule is stored inside it for external calls. And NativeModuleRegistry is called in the CatalystInstanceImpl.

Take a look at the logic inside CatalystInstanceImpl:

Public class CatalystInstanceImpl implements CatalystInstance {static {// Implements CatalystInstance. StaticInit (); } private CatalystInstanceImpl( final ReactQueueConfigurationSpec reactQueueConfigurationSpec, final JavaScriptExecutor jsExecutor, final NativeModuleRegistry nativeModuleRegistry, final JSBundleLoader jsBundleLoader, NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) { ... mNativeModuleRegistry = nativeModuleRegistry; // Pass the native module registry to jsBridge initializeBridge(new BridgeCallback(this), jsExecutor mReactQueueConfiguration.getJSQueueThread(), mNativeModulesQueueThread, mNativeModuleRegistry.getJavaModules(this), mNativeModuleRegistry.getCxxModules()); . } private native void initializeBridge(ReactCallback callback, JavaScriptExecutor jsExecutor, MessageQueueThread jsQueue, MessageQueueThread moduleQueue, Collection<JavaModuleWrapper> javaModules, Collection<ModuleHolder> cxxModules); . }Copy the code

The CatalystInstanceImpl is already associated with jsBridge (or ReactBridge), which uses the C++ function initializeBridge to upload the native module mapping table to jsBridge.

Take a look at the implementation of CatalystInstanceImpl in C++ :

// CatalystInstanceImpl.cpp void CatalystInstanceImpl::initializeBridge( jni::alias_ref<ReactCallback::javaobject> callback, JavaScriptExecutorHolder* jseh, jni::alias_ref<JavaMessageQueueThread::javaobject> jsQueue, jni::alias_ref<JavaMessageQueueThread::javaobject> nativeModulesQueue, jni::alias_ref<jni::JCollection<JavaModuleWrapper::javaobject>::javaobject> javaModules, jni::alias_ref<jni::JCollection<ModuleHolder::javaobject>::javaobject> cxxModules) { ... ModuleRegistry_ = STD ::make_shared<ModuleRegistry>(buildNativeModuleList( std::weak_ptr<Instance>(instance_), javaModules, cxxModules, moduleMessageQueue_)); . }Copy the code

Next is ModuleRegistry:

// ModuleRegistry.cpp ModuleRegistry::ModuleRegistry(std::vector<std::unique_ptr<NativeModule>> modules, ModuleNotFoundCallback callback) : modules_{std::move(modules)}, moduleNotFoundCallback_{callback} {} ... void ModuleRegistry::callNativeMethod(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) { ... Modules_ [moduleId]->invoke(methodId, STD ::move(params), callId); } MethodCallResult ModuleRegistry::callSerializableNativeHook(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params) { ... // The native module registry is called at 2return modules_[moduleId]->callSerializableNativeHook(methodId, std::move(params));
}
Copy the code

ModuleRegistry is the location of the native module mapping table in C++. ModuleRegistry exposes the function callNativeMethod for js calls.

This completes the analysis of the native module registration process.

Call the process

Let’s start by looking at the official documentation for how to call a native module:

import {NativeModules} from 'react-native';

NativeModules.ToastExample.show('Awesome', 1);
Copy the code

This invokes the native Toast. It mainly calls NativeModules. Js, ToastExample is the string returned by the getName method of ToastModule, and show is the method annotated by ReactMethod in ToastModule.

ReactMethod ReactMethod

functiongenModule( config: ? ModuleConfig, moduleID: number, ): ? {name: string, module? : Object} { const [moduleName, constants, methods, promiseMethods, syncMethods] = config; . // Get native method module[methodName] = genMethod(moduleID, methodID, methodType); .return {name: moduleName, module};
}

function genMethod(moduleID: number, methodID: number, type: MethodType) {
let fn = null;
  if (type= = ='promise') {// Call fn = asynchronouslyfunction(... args: Array<any>) {return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall(
          moduleID,
          methodID,
          args,
          data => resolve(data),
          errorData => reject(createErrorFromErrorData(errorData)),
        );
      });
    };
  } else if (type= = ='sync') {// call fn = synchronouslyfunction(... args: Array<any>) {returnglobal.nativeCallSyncHook(moduleID, methodID, args); }; }}let NativeModules: {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
} else if(! Global.nativeextensions) {// Initialize jsBridge const bridgeConfig = global.__fbbatchedBridgeconfig; . // Call native module const info = genModule(config, moduleID);if(! info) {return;
  }

  if(info.module) { NativeModules[info.name] = info.module; }... } module.exports = NativeModules;Copy the code

NativeModules obtain NativeModules through genModule and invoke methods of NativeModules through genMethod.

Native methods can be called synchronously or asynchronously.

Synchronous calls, for example, it calls the global nativeCallSyncHook, namely JSIExecutor. CPP registration method of c + + :

// jsiexecutor.cpp // registers the nativeCallSyncHook method for js to call runtime_->global().setProperty(*runtime_,"nativeCallSyncHook",
    Function::createFromHostFunction(
        *runtime_,
        PropNameID::forAscii(*runtime_, "nativeCallSyncHook"),
        1,
        [this](
            jsi::Runtime&,
            const jsi::Value&,
            const jsi::Value* args,
            size_t count) { returnnativeCallSyncHook(args, count); })); Value JSIExecutor::nativeCallSyncHook(const Value* args, size_t count) { ... // Call the delegate, ModuleRegistry, CallSerializableNativeHook function MethodCallResult result = delegate_ - > callSerializableNativeHook (* this, static_cast<unsigned int>(args[0].getNumber()), static_cast<unsigned int>(args[1].getNumber()), dynamicFromValue(*runtime_, args[2]));if(! result.hasValue()) {return Value::undefined();
  }
  return valueFromDynamic(*runtime_, result.value());
}
Copy the code

In jsiExecutor.cpp, this is done through a delegate, which ultimately calls Moduleregistry.cpp

// ModuleRegistry.cpp MethodCallResult ModuleRegistry::callSerializableNativeHook(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params) { ... // where the native module is calledreturn modules_[moduleId]->callSerializableNativeHook(methodId, std::move(params));
}
Copy the code

Finally, we come to ModuleRegistry, the end of the registration process, and the call is complete.