I wrote an article ReactNative Android source code analysis before, and analyzed and summarized the communication process between RN and Native on the basis of this article. This paper is based on Android code analysis, iOS implementation principle is similar.

1. Communication framework diagram




Communication frame diagram

First, let’s analyze the roles and functions of each module:

Java layer, which is implemented in ActAndroid

  • ReactContext: Android context subclass, containing an instance of CatalystInstance, used to get NativeModule, JSModule, add various call-back, handle exceptions, etc
  • ReactInstanceManager: Manages instances of CatalystInstance, handles RN Root Views, launches JS pages, and manages life cycles
  • CatalystInstance: a key class for communication, which allows JS to call JS modules and Native Modules to interact with Bridges, invisible to developers

C++ layer, which is implemented in ReactCommon for Android and iOS

  • NativeToJsBridge: bridge between Native and JS, responsible for calling JS Module, calling native (calling JsToNativeBridge), and loading JS code (calling JavaScriptCore).
  • JsToNativeBridge: Method of invoking Native Module
  • JSCExecutor: Loading/executing JS code (calling JavaScriptCore), calling JS modules, calling native, and performing statistics are all core functions

The JS layer, implemented in Libraries, and RN JS-related implementations in this folder

  • MessageQueue: Manage JS call queue, call Native/JS Module methods, perform callback, manage JS Module, etc
  • JavaScriptModule: represents all JSModule implementations, and has corresponding code in the Java layer (both interfaces), using dynamic proxy calls, and unified entry in CatalystInstance

2. C++ and JS communication

The communication between Native and JS is nothing more than the cross-language invocation between Java/OC and JS. Before analyzing the communication between Native and JS, let’s first understand the cross-language invocation between Java/OC and JS. JavaScriptCore is used to execute JS in ReactNative. The key here is how to use JavaScriptCore. Take a look at the Android build script:

  • ReactAndroid/build.gradle
compile 'org.webkit:android-jsc:r174650' task downloadJSCHeaders(type: Download) { def jscAPIBaseURL = 'https://svn.webkit.org/repository/webkit/ ! svn/bc/174650/trunk/Source/JavaScriptCore/API/' def jscHeaderFiles = ['JavaScript.h', 'JSBase.h', 'JSContextRef.h', 'JSObjectRef.h', 'JSRetainPtr.h', 'JSStringRef.h', 'JSValueRef.h', 'WebKitAvailability.h'] def output = new File(downloadsDir, 'jsc') output.mkdirs() src(jscHeaderFiles.collect { headerName -> "$jscAPIBaseURL$headerName" }) onlyIfNewer true overwrite false dest output } // Create Android.mk library module based on so files from mvn + include headers fetched from webkit .org task prepareJSC(dependsOn: downloadJSCHeaders) << { copy { from zipTree(configurations.compile.fileCollection { dep -> dep.name == 'android-jsc' }.  singleFile) from {downloadJSCHeaders.dest} from 'src/main/jni/third-party/jsc/Android.mk' include 'jni/**/*.so', '*.h', 'Android.mk' filesMatching('*.h', { fname -> fname.path = "JavaScriptCore/${fname.path}"}) into "$thirdPartyNdkDir/jsc"; }}Copy the code
  • ReactAndroid/src/main/jni/third-party/jsc/Android.mk
LOCAL_SRC_FILES := jni/$(TARGET_ARCH_ABI)/libjsc.soCopy the code

It can be seen from this that RN does not use webkit of the system. Webkit mainly includes WebCore typesetting engine and JSCore engine. JSCore engine is mainly used here, and typesetting is entrusted to Native.

Set native methods and properties in RN using the following methods:

JSObjectSetProperty(m_context, globalObject, jsPropertyName, valueToInject, 0, NULL);Copy the code

This method is in libjsc.so and is available in jsobjectref. h. This can be implemented in Native Settings and then pulled out in JS, and vice versa.

3. Native communication with JS

Loading the bundle file

The communication between Native and JS requires the loading of the Bundle file at the completion of Native initialization, and the location of the Bundle file is configurable.

public abstract class ReactNativeHost {

  ...

  /**
   * Returns the name of the main module. Determines the URL used to fetch the JS bundle
   * from the packager server. It is only used when dev support is enabled.
   * This is the first file to be executed once the {@link ReactInstanceManager} is created.
   * e.g. "index.android"
   */
  protected String getJSMainModuleName() {
    return "index.android";
  }

  /**
   * Returns a custom path of the bundle file. This is used in cases the bundle should be loaded
   * from a custom path. By default it is loaded from Android assets, from a path specified
   * by {@link getBundleAssetName}.
   * e.g. "file://sdcard/myapp_cache/index.android.bundle"
   */
  protected @Nullable String getJSBundleFile() {
    return null;
  }

  /**
   * Returns the name of the bundle in assets. If this is null, and no file path is specified for
   * the bundle, the app will only work with {@code getUseDeveloperSupport} enabled and will
   * always try to load the JS bundle from the packager server.
   * e.g. "index.android.bundle"
   */
  protected @Nullable String getBundleAssetName() {
    return "index.android.bundle";
  }

  /**
   * Returns whether dev mode should be enabled. This enables e.g. the dev menu.
   */
  protected abstract boolean getUseDeveloperSupport();

  ...

}Copy the code

These methods in the ReactNativeHost are reloaded as needed in the Application. These methods determine where the Bundle is loaded from. The method annotations are very clear, so let’s look at the flow diagram:




Bundle Loading Flowchart

JSBundleLoader is loaded from where, according to the location of the file, you can see its loadScript method. Finally, it will call CatalystIntance to load

  /* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL);
  /* package */ native void loadScriptFromFile(String fileName, String sourceURL);
  /* package */ native void loadScriptFromOptimizedBundle(String path, String sourceURL, int flags);Copy the code

The last one supports loading optimized bundles, which are not currently used. These methods are all c++ implementations, so let’s focus on the first two

void CatalystInstanceImpl::loadScriptFromAssets(jobject assetManager,
                                                const std::string& assetURL) {
  const int kAssetsLength = 9;  // strlen("assets://");
  auto sourceURL = assetURL.substr(kAssetsLength);

  auto manager = react::extractAssetManager(assetManager);
  auto script = react::loadScriptFromAssets(manager, sourceURL);
  if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) {
    instance_->loadUnbundle(
      folly::make_unique(manager, sourceURL),
      std::move(script),
      sourceURL);
    return;
  } else {
    instance_->loadScriptFromString(std::move(script), sourceURL);
  }
}

void CatalystInstanceImpl::loadScriptFromFile(jni::alias_ref fileName,
                                              const std::string& sourceURL) {
  return instance_->loadScriptFromFile(fileName ? fileName->toStdString() : "",
                                       sourceURL);
}Copy the code

To load from assets is to read the contents of the bundle as a string. Here is an UnBundle, which is RN’s way of packaging. In addition to generating the integrated JS file index.android.bundle, it will also generate individual unintegrated JS files (which will be optimized). Put it all in the js-modules directory, and an identity file called UNBUNDLE will be generated and stored there. UNBUNDLE The first four bytes of a file are fixed as 0xFB0BD1E5 and are used for verification before loading. It should be noted that the js-modules directory will be packaged into the ASSETS folder of APK. Here is how to deal with this situation. A bit special for development mode, the ReactInstanceManager will download the Bundle file from the server before creating it, and then save it. The demo program path is as follows:

/data/user/0/com.awesomeproject/files/ReactNativeDevBundle.js

After the download is complete call CatalystInstance. LoadScriptFromFile (), after passing the cache path, this method is to read the file content, as a string, also call loadScriptFromString

void Instance::loadScriptFromString(std::unique_ptr string,
                                    std::string sourceURL) {
  callback_->incrementPendingJSCalls();
  SystraceSection s("reactbridge_xplat_loadScriptFromString",
                    "sourceURL", sourceURL);
  // TODO mhorowitz: ReactMarker around loadApplicationScript
  nativeToJsBridge_->loadApplicationScript(std::move(string), std::move(sourceURL));
}
--------------------------------------
void NativeToJsBridge::loadApplicationScript(std::unique_ptr script,
                                             std::string sourceURL) {
  // TODO(t11144533): Add assert that we are on the correct thread
  m_mainExecutor->loadApplicationScript(std::move(script), std::move(sourceURL));
}Copy the code

For details about loadApplicationScript, see ReactNative Android source code. Here the JS code has already been executed.

How to call JS

How to call JS in Native is as follows:

ReactContext. GetJSModule (JSModule class name. The class). The method name (params);Copy the code

ReactContext calls the CatalystInstance method of the same name

  @Override
  public  T getJSModule(Class jsInterface) {
    return getJSModule(mMainExecutorToken, jsInterface);
  }

  @Override
  public  T getJSModule(ExecutorToken executorToken, Class jsInterface) {
    return Assertions.assertNotNull(mJSModuleRegistry)
        .getJavaScriptModule(this, executorToken, jsInterface);
  }Copy the code

The mMainExecutorToken was created in initializeBridge and is described as web workers, JS multithreaded, using tokens to distinguish threads. In this case, use the mMainExecutorToken. Take a look at the implementation of CatalystInstance:

public synchronized T getJavaScriptModule( CatalystInstance instance, ExecutorToken executorToken, Class moduleInterface) { HashMap, JavaScriptModule> instancesForContext = mModuleInstances.get(executorToken); if (instancesForContext == null) { instancesForContext = new HashMap<>(); mModuleInstances.put(executorToken, instancesForContext); } JavaScriptModule module = instancesForContext.get(moduleInterface); if (module ! = null) { return (T) module; } JavaScriptModuleRegistration registration = Assertions.assertNotNull( mModuleRegistrations.get(moduleInterface), "JS module " + moduleInterface.getSimpleName() + " hasn't been registered!" ); JavaScriptModule interfaceProxy = (JavaScriptModule) Proxy.newProxyInstance( moduleInterface.getClassLoader(), new Class[]{moduleInterface}, new JavaScriptModuleInvocationHandler(executorToken, instance, registration)); instancesForContext.put(moduleInterface, interfaceProxy); return (T) interfaceProxy; }Copy the code

All javascriptModules are collected into the JavaScriptModule Map (mModuleRegistrations) when CatalystInstance is created. MModuleInstances are proxy objects that cache JS Modules that have been called. If they have been called, they are returned directly from the Map. Otherwise, they are created and cached. The dynamic proxy mode is used here, where a proxy object of interface is created and the invoke() method of the InvocationHandler is invoked when its methods are called.

@Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { ExecutorToken executorToken = mExecutorToken.get(); if (executorToken == null) { FLog.w(ReactConstants.TAG, "Dropping JS call, ExecutorToken went away..." ); return null; } NativeArray jsArgs = args ! = null ? Arguments.fromJavaArgs(args) : new WritableNativeArray(); mCatalystInstance.callFunction( executorToken, mModuleRegistration.getName(), method.getName(), jsArgs ); return null; }Copy the code

This call flow has been analyzed in the ReactNative Android source code analysis. This will go to the JS MessageQueue. CallFunctionReturnFlushedQueue ().

JS receives calls and processes

To explain why will go to callFunctionReturnFlushedQueue first.

  1. The generated bundle.js will place the MessageQueue object in a global property
    Object.defineProperty(global,"__fbBatchedBridge",{configurable:! 0,value:BatchedBridge})Copy the code

    MessageQueue: BatchedBridge MessageQueue: BatchedBridge MessageQueue

    const BatchedBridge = new MessageQueue(
    () => global.__fbBatchedBridgeConfig,
    serializeNativeParams
    );Copy the code
  2. The following method is executed when the bundle file is loaded from above

    void JSCExecutor::bindBridge() throw(JSException) {
       auto global = Object::getGlobalObject(m_context);
       auto batchedBridgeValue = global.getProperty("__fbBatchedBridge");
       if (batchedBridgeValue.isUndefined()) {
         throwJSExecutionException("Could not get BatchedBridge, make sure your bundle is packaged   correctly");
       }
    
       auto batchedBridge = batchedBridgeValue.asObject();
       m_callFunctionReturnFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnFlushedQueue"). asObject();
       m_invokeCallbackAndReturnFlushedQueueJS = batchedBridge.getProperty(  "invokeCallbackAndReturnFlushedQueue").asObject();
       m_flushedQueueJS = batchedBridge.getProperty("flushedQueue").asObject();
    }Copy the code

    MessageQueue’s three methods will be stored as objects in c++, which will be directly used when we call JS methods.

    void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) { try { auto result = m_callFunctionReturnFlushedQueueJS->callAsFunction({ Value(m_context, String::createExpectingAscii(moduleId)), Value(m_context, String::createExpectingAscii(methodId)), Value::fromDynamic(m_context, std::move(arguments)) }); auto calls = Value(m_context, result).toJSONString(); m_delegate->callNativeModules(*this, std::move(calls), true); } catch (...) { std::throw_with_nested(std::runtime_error("Error calling function: " + moduleId + ":" + methodId)); }}Copy the code
    Value Object::callAsFunction(JSObjectRef thisObj, int nArgs, const JSValueRef args[]) const { JSValueRef exn; JSValueRef result = JSObjectCallAsFunction(m_context, m_obj, thisObj, nArgs, args, &exn); if (! result) { std::string exceptionText = Value(m_context, exn).toString().str(); throwJSExecutionException("Exception calling object as function: %s", exceptionText.c_str()); } return Value(m_context, result); }Copy the code

    JavaScriptCore’s JSObjectCallAsFunction is used to call JS. The next easy to do, directly analyze the JS code.

At this method mainly call __callFunction callFunctionReturnFlushedQueue, take a look at its implementation:

__callFunction(module: string, method: string, args: any) { ... const moduleMethods = this._callableModules[module]; . const result = moduleMethods[method].apply(moduleMethods, args); Systrace.endEvent(); return result; }Copy the code

The method is taken from _callableModules, so where does it get its value from? If YOU look at this file, it turns out there’s a method to add to it

  registerCallableModule(name, methods) {
    this._callableModules[name] = methods;
  }Copy the code

In other words, all JS modules need to put the methods available for Native invocation in this Module in order to be able to execute. Take appregistry. js for example, and see how it was added to the file

var AppRegistry = { registerConfig: function(config: Array) {... }, registerComponent: function(appKey: string, getComponentFunc: ComponentProvider): string {... }, registerRunnable: function(appKey: string, func: Function): string {... }, getAppKeys: function(): Array {... }, runApplication: function(appKey: string, appParameters: any): void {... }, unmountApplicationComponentAtRootTag: function(rootTag : number) {... }}; BatchedBridge.registerCallableModule( 'AppRegistry', AppRegistry );Copy the code

At this point Native JS calls are done. To summarize the process:

  1. MessageQueue puts methods called by Native into JavaScriptCore
  2. The JS Module puts callable methods into a pair of columns in MessageQueue
  3. Native gets the JS call entry from JavaScriptCore and passes Module Name, Method Name, and Parameters
  4. Execute the JS Module method

4. Communication between JS and Native

JS handles the list of Native Modules

In the ReactNative Android source code analysis, the initialization process of Native is analyzed, and the processing of Native modules is summarized here.

  1. All Native modules are placed in a list when initializing CatalystInstance and are stored in both C++(ModuleRegistry) and Java(Java)
  2. In JavaScriptCore, the global attribute __fbBatchedBridgeConfig is set to the Module Name list

So the problem comes, in JS can only take the name of Native Module, how to call its method? Let’s analyze this problem. During JSCExecutor initialization, several c++ methods are registered with JavaScriptCore for JS to call, including methods to get details about the Native Module

void JSCExecutor::initOnJSVMThread() throw(JSException) { .... installNativeHook<&JSCExecutor::nativeRequireModuleConfig>("nativeRequireModuleConfig"); installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate"); . } JSValueRef JSCExecutor::nativeRequireModuleConfig( size_t argumentCount, const JSValueRef arguments[]) { if (argumentCount ! = 1) { throw std::invalid_argument("Got wrong number of args"); } std::string moduleName = Value(m_context, arguments[0]).toString().str(); folly::dynamic config = m_delegate->getModuleConfig(moduleName); return Value::fromDynamic(m_context, config); }Copy the code

From nativeRequireModuleConfig into arguments and returns the result to be able to see it is for JS invoked, for Native Module details information, m_delegate – > getModuleConfig implementation analysis below.

Next, analyze how JS processes Native Modules. The entry is handled in messagequeue.js.

class MessageQueue { constructor(configProvider: () => Config, serializeNativeParams: boolean) { .... lazyProperty(this, 'RemoteModules', () => { const {remoteModuleConfig} = configProvider(); const modulesConfig = this._genModulesConfig(remoteModuleConfig); const modules = this._genModules(modulesConfig) ... return modules; }); }... function lazyProperty(target: Object, name: string, f: () => any) { Object.defineProperty(target, name, { configurable: true, enumerable: true, get() { const value = f(); Object.defineProperty(target, name, { configurable: true, enumerable: true, writeable: true, value: value, }); return value; }}); }Copy the code

It defines a RemoteModules property in its constructor, using a lazy loading mechanism that assigns values to it only when it is actually used. All Modle lists are returned with the Module ID added, but nothing else.

Where are the RemoteModules used and how to obtain other information about the Module? There is a long way to go

const BatchedBridge = require('BatchedBridge'); const RemoteModules = BatchedBridge.RemoteModules; . /** * Define lazy getters for each module. * These will return the module if already loaded, or load it if not. */ const NativeModules = {}; Object.keys(RemoteModules).forEach((moduleName) => { Object.defineProperty(NativeModules, moduleName, { configurable: true, enumerable: true, get: () => { let module = RemoteModules[moduleName]; if (module && typeof module.moduleID === 'number' && global.nativeRequireModuleConfig) { // The old bridge (still used by iOS) will send the config as // a JSON string that needs parsing, so we set config according // to the type of response we got. const rawConfig = global.nativeRequireModuleConfig(moduleName); const config = typeof rawConfig === 'string' ? JSON.parse(rawConfig) : rawConfig; module = config && BatchedBridge.processModuleConfig(config, module.moduleID); RemoteModules[moduleName] = module; } Object.defineProperty(NativeModules, moduleName, { configurable: true, enumerable: true, value: module, }); return module; }}); }); module.exports = NativeModules;Copy the code

This iterates through all the module names in RemoteModules, each defining an object that is not assigned until it is used. See at the time of assignment will invoke the c + + nativeRequireModuleConfig, namely to obtain detailed information of each Module. To get the details, call m_delegate->getModuleConfig(moduleName), m_delegate is the JsToNativeBridge object, GetModuleConfig direct call ModuleRegistry: : getConfig (name)

folly::dynamic ModuleRegistry::getConfig(const std::string& name) { SystraceSection s("getConfig", "module", name); auto it = modulesByName_.find(name); if (it == modulesByName_.end()) { return nullptr; } CHECK(it->second < modules_.size()); NativeModule* module = modules_[it->second].get(); // string name, [object constants,] array methodNames (methodId is index), [array asyncMethodIds] folly::dynamic config = folly::dynamic::array(name); { SystraceSection s("getConstants"); folly::dynamic constants = module->getConstants(); if (constants.isObject() && constants.size() > 0) { config.push_back(std::move(constants)); } } { SystraceSection s("getMethods"); std::vector methods = module->getMethods(); folly::dynamic methodNames = folly::dynamic::array; folly::dynamic asyncMethodIds = folly::dynamic::array; folly::dynamic syncHookIds = folly::dynamic::array; for (auto& descriptor : methods) { methodNames.push_back(std::move(descriptor.name)); if (descriptor.type == "remoteAsync") { asyncMethodIds.push_back(methodNames.size() - 1); } else if (descriptor.type == "syncHook") { syncHookIds.push_back(methodNames.size() - 1); } } if (! methodNames.empty()) { config.push_back(std::move(methodNames)); config.push_back(std::move(asyncMethodIds)); if (! syncHookIds.empty()) { config.push_back(std::move(syncHookIds)); } } } if (config.size() == 1) { // no constants or methods return nullptr; } else { return config; }}Copy the code

Two data structures need to be explained here. Modules_ is an array of all Native module objects, and modulesByName_ is a Map. The key value is the module name, and the value is the index value of the module in Modules_. This method returns an array of the format

[
  "Module Name",
  [Object Constants],
  [Method Name Array],
  [Async Method Ids],
  [Sync Hook Ids]
]Copy the code

The first three are easy to understand, to explain what the last two mean, asyncMethod literally means asynchronous method, so the method parameters are Promise. Methods of the syncHook class, which are not currently encountered, can be called directly from the JS thread, while other methods are thrown into the background thread queue and wait to be called. Let’s focus on the implementation of getConstants and getMethods for NativeModule.




NativeModule class diagram

JavaNativeModule is a common Java module. NewJavaNativeModule is a c++ cross-platform module. It is not used yet, so we will examine JavaNativeModule.

  std::vector getMethods() override {
    static auto getMDMethod =
      wrapper_->getClass()->getMethod::javaobject()>(
        "getMethodDescriptors");

    std::vector ret;
    auto descs = getMDMethod(wrapper_);
    for (const auto& desc : *descs) {
      static auto nameField =
        JMethodDescriptor::javaClassStatic()->getField("name");
      static auto typeField =
        JMethodDescriptor::javaClassStatic()->getField("type");

      ret.emplace_back(
        desc->getFieldValue(nameField)->toStdString(),
        desc->getFieldValue(typeField)->toStdString()
      );
    }
    return ret;
  }

  folly::dynamic getConstants() override {
    static auto constantsMethod =
      wrapper_->getClass()->getMethod("getConstants");
    auto constants = constantsMethod(wrapper_);
    if (!constants) {
      return nullptr;
    } else {
      // See JavaModuleWrapper#getConstants for the other side of this hack.
      return cthis(constants)->array[0];
    }
  }Copy the code

Wrapper_ here refers to JavaModuleWrapper and wrapper_ – > getClass () refers to the Java classes: “Lcom/facebook/react/cxxbridge/JavaModuleWrapper;” That is, getMethodDescriptors and getConstants using reflection to call JavaModuleWrapper. GetContants is simply calling the getConstants method of a Java concrete module and assembling the returned map into a struct return of WritableNativeMap that RN will accept. See the implementation of getMethodDescriptors

@DoNotStrip
public class MethodDescriptor {
  @DoNotStrip
  Method method;
  @DoNotStrip
  String signature;
  @DoNotStrip
  String name;
  @DoNotStrip
  String type;
}
@DoNotStrip
public List getMethodDescriptors() {
  ArrayList descs = new ArrayList<>();
  for (Map.Entry entry :
         mModule.getMethods().entrySet()) {
    MethodDescriptor md = new MethodDescriptor();
    md.name = entry.getKey();
    md.type = entry.getValue().getType();
    BaseJavaModule.JavaMethod method = (BaseJavaModule.JavaMethod) entry.getValue();
    mMethods.add(method);
    descs.add(md);
  }
  return descs;
}Copy the code

Mmodule.getmethods () is a method in BaseJavaModule that uses reflection to find the current module. Methods must be annotated with @reactMethod to collect. And then finally encapsulate that information into a MethodDescriptor class.

Here we have analyzed how JS gets the detailed information of Native module.

How to call Native

Here is a demonstration of how to call Native Module in JS. Assume a scenario where the user clicks a TextView and then plays a Toast prompt. Take the code of demo project as an example:

class AwesomeProject extends Component { render() { return ( Welcome to React Native! To get started, edit index.android.js Double tap R on your keyboard to reload,{'\n'} Shake or press menu button for dev menu ); } } function onClick(){ var ToastAndroid = require('ToastAndroid') ToastAndroid.show('Click TextView... ', ToastAndroid.SHORT); }Copy the code

Take a look at the ToastAndroid implementation

var RCTToastAndroid = require('NativeModules').ToastAndroid; . var ToastAndroid = { ... show: function ( message: string, duration: number ): void { RCTToastAndroid.show(message, duration); },... };Copy the code

Here rcttoastAndroid.show () is called, and RCTToastAndroid is taken from NativeModules. In the previous analysis of how JS collects Native modules, modules attribute will be generated. When calling Native method, the function in it will be executed. Let’s see how this function is generated

_genMethod(module, method, type) { let fn = null; const self = this; if (type === MethodTypes.remoteAsync) { ... } else if (type === MethodTypes.syncHook) { ... } else { fn = function(... args) { const lastArg = args.length > 0 ? args[args.length - 1] : null; const secondLastArg = args.length > 1 ? args[args.length - 2] : null; const hasSuccCB = typeof lastArg === 'function'; const hasErrorCB = typeof secondLastArg === 'function'; hasErrorCB && invariant( hasSuccCB, 'Cannot have a non-function arg after a function arg.' ); const numCBs = hasSuccCB + hasErrorCB; const onSucc = hasSuccCB ? lastArg : null; const onFail = hasErrorCB ? secondLastArg : null; args = args.slice(0, args.length - numCBs); return self.__nativeCall(module, method, args, onFail, onSucc); }; } fn.type = type; return fn; }Copy the code

Just prepare the arguments and __nativeCall.

__nativeCall(module, method, params, onFail, onSucc) { if (onFail || onSucc) { ... onFail && params.push(this._callbackID); this._callbacks[this._callbackID++] = onFail; onSucc && params.push(this._callbackID); this._callbacks[this._callbackID++] = onSucc; } var preparedParams = this._serializeNativeParams ? JSON.stringify(params) : params; . this._callID++; this._queue[MODULE_IDS].push(module); this._queue[METHOD_IDS].push(method); this._queue[PARAMS].push(preparedParams); const now = new Date().getTime(); if (global.nativeFlushQueueImmediate && now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) { global.nativeFlushQueueImmediate(this._queue); this._queue = [[], [], [], this._callID]; this._lastFlush = now; } Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length); . }Copy the code

The module name, method name, and the parameters of the call save on the array, if the last call and the call thinking of more than 5 ms is called c + + nativeFlushQueueImmediate method, if less than 5 ms is returned directly.

Native receives calls and processes them

There are two situations in which Native receives JS calls:

  • Two calls for more than 5 ms, enter nativeFlushQueueImmediate
  • JSCExecutor:: Flush () returns a previously stored call to JSCExecutor:: Flush ()

So let’s do the first case

JSValueRef JSCExecutor::nativeFlushQueueImmediate( size_t argumentCount, const JSValueRef arguments[]) { if (argumentCount ! = 1) { throw std::invalid_argument("Got wrong number of args"); } std::string resStr = Value(m_context, arguments[0]).toJSONString(); flushQueueImmediate(std::move(resStr)); return JSValueMakeUndefined(m_context); } void JSCExecutor::flushQueueImmediate(std::string queueJSON) { m_delegate->callNativeModules(*this, std::move(queueJSON), false); }Copy the code

Let’s do the second case

void JSCExecutor::flush() { auto result = m_flushedQueueJS->callAsFunction({}); try { auto calls = Value(m_context, result).toJSONString(); m_delegate->callNativeModules(*this, std::move(calls), true); } catch (...) { std::string message = "Error in flush()"; try { message += ":" + Value(m_context, result).toString().str(); } catch (...) { // ignored } std::throw_with_nested(std::runtime_error(message)); }}Copy the code

The result is the same, the JS calls into a Json string, and then call JsToNativeBridge. CallNativeModules (). This Json string is an array containing four elements in the following format:




void callNativeModules( JSExecutor& executor, std::string callJSON, bool isEndOfBatch) override { ExecutorToken token = m_nativeToJs->getTokenForExecutor(executor); m_nativeQueue->runOnQueue([this, token, callJSON=std::move(callJSON), isEndOfBatch] { // An exception anywhere in here stops processing of the batch. This // was the behavior of the Android bridge, and since exception handling // terminates the whole bridge, there's not much point in continuing. for (auto& call : react::parseMethodCalls(callJSON)) { m_registry->callNativeMethod( token, call.moduleId, call.methodId, std::move(call.arguments), call.callId); } if (isEndOfBatch) { m_callback->onBatchComplete(); m_callback->decrementPendingJSCalls(); }}); }Copy the code

Here the Native module methods are called based on the ModuleId and MethodId. M_registry is a ModuleRegistry for c++, and I’ll start by explaining how it is created. In CatalystInstance. InitializeBridge () when passing a Java layer ModuleRegistryHolder, also in c + + objects also have a same name, The Native Module list is saved at creation time and a c++ ModuleRegistry is created to pass in the Native Module list.

void ModuleRegistry::callNativeMethod(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) { if (moduleId >= modules_.size()) { throw std::runtime_error( folly::to("moduleId ", moduleId, " out of range [0..", modules_.size(), ")")); } #ifdef WITH_FBSYSTRACE if (callId ! = -1) { fbsystrace_end_async_flow(TRACE_TAG_REACT_APPS, "native", callId);  } #endif modules_[moduleId]->invoke(token, methodId, std::move(params)); }Copy the code

ModuleId is the index of the module in the list, and modules_ is of type

std::vector> modules_;Copy the code

The NativeModule for c++ is created based on the Java layer’s ModuleRegistryHolder. Take a look at its Invoke method

  void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override {
    static auto invokeMethod =
      wrapper_->getClass()->getMethod("invoke");
    invokeMethod(wrapper_, JExecutorToken::extractJavaPartFromToken(token).get(), static_cast(reactMethodId),
                 ReadableNativeArray::newObjectCxxArgs(std::move(params)).get());
  }Copy the code

The invoke method of JavaModuleWrapper is invoked and methodId is passed along with the parameter.

/* package */ class JavaModuleWrapper { ... private final ArrayList mMethods; . @DoNotStrip public void invoke(ExecutorToken token, int methodId, ReadableNativeArray parameters) { if (mMethods == null || methodId >= mMethods.size()) { return; } mMethods.get(methodId).invoke(mCatalystInstance, token, parameters); }}Copy the code

In JavaModuleWrapper there is a List of all the JS methods that can be called in this module. MethodId is the index of the method and the module methodId obtained in MessageQueue. JavaMethod’s invoke is a reflection that invokes the associated method. At this point, the process of JS invoking Native is completed.

Welcome to comment and exchange more