Article first personal blog: ReactNative and iOS communication principle analysis – communication article

Introduction: Originally I wanted to write an article on how to implement JSBridge in iOS with React-Native (hereinafter referred to as RN). For those of you who have read the official documentation, rn communicates with iOS using a module called RCTBridgeModule. I believe that you, like me, cannot know why and why; So I decided to double the source code of RN and explore the communication mechanism between RN and iOS. Results More contents were found with the further analysis. So I wrote ReactNative and iOS native communication principle analysis – initialization and ReactNative and iOS native communication principle analysis -JS load and execute two RN source code analysis articles.

This article will build on the previous two articles to further understand the native communication mechanism between RN and iOS.

Declaration: The version of RN used in this article is 0.63.0.

origin

After executing the JS code, the JSIExecutor executes the flush function. The JS and Native bindings are first bound in the Flush function; After binding, native can call JS functions to realize the communication between native and JS.


// Various JS methods are bound to native
void JSIExecutor::bindBridge() {
  std::call_once(bindFlag_, [this] {
    // Get batchedBridge from __fbBatchedBridge on js side
    Value batchedBridgeValue =
        runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
    if (batchedBridgeValue.isUndefined()) {
      throw JSINativeException(
          "Could not get BatchedBridge, make sure your bundle is packaged correctly");
    }
/ / the batchedBridge callFunctionReturnFlushedQueue_ of callFunctionReturnFlushedQueue and JSIExecutor object binding
    Object batchedBridge = batchedBridgeValue.asObject(*runtime_);
    callFunctionReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
        *runtime_, "callFunctionReturnFlushedQueue");
  / / the batchedBridge invokeCallbackAndReturnFlushedQueue and JSIExecutor invokeCallbackAndReturnFlushedQueue_ in binding;
    invokeCallbackAndReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
        *runtime_, "invokeCallbackAndReturnFlushedQueue");
  // Bind the flushedQueue in batchedBridge to the FlushedQueu_ in JSIExecutor.
    flushedQueue_ =
        batchedBridge.getPropertyAsFunction(*runtime_, "flushedQueue");
  });
}
Copy the code

Ok, so now we know that the JS function is bound to native after the entire JS execution; So how does native execute JS functions? Let’s find out.

Native to JS

If you remember, when native executes JS code, there is a callback function that notifies RCTRootView that javascript is loaded by means of an event inside the function.

// code execution
  - (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync{
  // The js code executes the callback
  dispatch_block_t completion = ^{
    // When the js code is finished, the js execution event queue needs to be refreshed
    [self _flushPendingCalls];

    // Notify the RCTRootView in the main thread; The js code has been executed. When the RCTRootView receives a notification, it hangs and displays it
    dispatch_async(dispatch_get_main_queue(), ^{
      [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification
                                                          object:self->_parentBridge
                                                        userInfo:@{@"bridge" : self}];

      [self ensureOnJavaScriptThread:^{
        // The timer continues
        [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
      }];
    });
  };

  if (sync) {
    // Execute js code synchronously
    [self executeApplicationScriptSync:sourceCode url:self.bundleURL];
    completion();
  } else {
    // Execute js code asynchronously
    [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion];
  }

  [self.devSettings setupHotModuleReloadClientIfApplicableForURL:self.bundleURL];
}
Copy the code

Will listen in RCTRootView RCTJavaScriptDidLoadNotification events; And perform the following methods:

 (void)javaScriptDidLoad:(NSNotification *)notification
{
  // Retrieve the batchedBridge instance from RCTBridge.
  RCTBridge *bridge = notification.userInfo[@"bridge"];
  if (bridge != _contentView.bridge) {
    [self bundleFinishedLoading:bridge];
  }
}

- (void)bundleFinishedLoading:(RCTBridge *)bridge
{
  // ...
  [_contentView removeFromSuperview];
  _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds
                                                    bridge:bridge
                                                  reactTag:self.reactTag
                                           sizeFlexiblity:_sizeFlexibility];
  // Use RCTBridge to call js method to start the page
  [self runApplication:bridge];
  // Display the page
  [self insertSubview:_contentView atIndex:0];
}

- (void)runApplication:(RCTBridge *)bridge { NSString *moduleName = _moduleName ? : @"";
  NSDictionary *appParameters = @{
    @"rootTag" : _contentView.reactTag,
    @"initialProps" : _appProperties ?: @{},
  };
   / / call RCTCxxBridge enqueueJSCall: method: the args: completion: method
  [bridge enqueueJSCall:@"AppRegistry" method:@"runApplication" args:@[ moduleName, appParameters ] completion:NULL];
}

Copy the code

For bridge enqueueJSCall, rn the Instance – > NativeToJsBridge – > JSIExecutor the invocation chain call JSIExecutor: : callFunction method, Method calls within the JSIExecutor callFunctionReturnFlushedQueue_ method.

In bindBridge callFunctionReturnFlushedQueue_ is through the way of the runtime to native callFunctionReturnFlushedQueue_ pointed to the js The callFunctionReturnFlushedQueue function.

Native to moduleId, methodId, arguements as parameters to the execution of JS side callFunctionReturnFlushedQueue function returns a queue; This queue is what JS needs to execute on the native side; Finally, the native side gives callNativeModules to execute the corresponding method.

The js side uses the callFunction to obtain the specified module and method. Use apply to execute the corresponding method.


// RCTxxBridge.mm

- (void)enqueueJSCall:(NSString *)module
               method:(NSString *)method
                 args:(NSArray *)args
           completion:(dispatch_block_t)completion{
    if (strongSelf->_reactInstance) {
        // Instance.calljsfunction is called
      strongSelf->_reactInstance->callJSFunction(
          [moduleUTF8String], [method UTF8String], convertIdToFollyDynamic(args ? : @ [])); }}]; }// Instance.cpp
void Instance::callJSFunction(
    std::string &&module,
    std::string &&method,
    folly::dynamic &&params) {
  callback_->incrementPendingJSCalls();
  // Call the callFunction of NativeToJsBridge
  nativeToJsBridge_->callFunction(
      std::move(module), std::move(method), std::move(params));
}

// NativeToJsBridge.cpp
void NativeToJsBridge::callFunction(
    std::string &&module,
    std::string &&method,
    folly::dynamic &&arguments) {
  runOnExecutorQueue([this.module = std::move(module),
                      method = std::move(method),
                      arguments = std::move(arguments),
                      systraceCookie](JSExecutor *executor) {
    // The callFunction in JSIExecutor is called
    executor->callFunction(module, method, arguments);
  });
}

// JSIExecutor.cpp

void JSIExecutor::callFunction(
    const std::string &moduleId,
    const std::string &methodId,
    const folly::dynamic &arguments) {
/ / if not put callFunctionReturnFlushedQueue_ and js function callFunctionReturnFlushedQueue function of binding, binding first
  if(! callFunctionReturnFlushedQueue_) { bindBridge(); } Value ret = Value::undefined();try {
    scopedTimeoutInvoker_(
        [&] {
        / / call callFunctionReturnFlushedQueue_ incoming JS moduleId, methodId, arguements parameters, JS may be returned to the queue
          ret = callFunctionReturnFlushedQueue_->call(
              *runtime_,
              moduleId,
              methodId,
              valueFromDynamic(*runtime_, arguments));
        },
        std::move(errorProducer));
  } catch (...) {

  }
// Execute native modules
  callNativeModules(ret, true);
}

// MessageQueue.js

 callFunctionReturnFlushedQueue(
    module: string,
    method: string,
    args: any[],
  ): null | [Array<number>, Array<number>, Array<any>, number] {
    this.__guard(() => {
      this.__callFunction(module, method, args);
    });

    return this.flushedQueue();
  }

    __callFunction(module: string, method: string, args: any[]): void {
    this._lastFlush = Date.now();
    this._eventLoopStartTime = this._lastFlush;
    const moduleMethods = this.getCallableModule(module);
    moduleMethods[method].apply(moduleMethods, args);
  }
Copy the code

But just now we talked about callFunctionReturnFlushedQueue_ and js side callFunctionReturnFlushedQueue function of binding, It is also have binding invokeCallbackAndReturnFlushedQueue and flushedQueue. Here is not to do too much explanation, interested students can go to check invokeCallbackAndReturnFlushedQueue and flushedQueue; Its implementation principle and callFunctionReturnFlushedQueue is similar.

Please see the flowchart at the end!

JS to Native

What, in the front native to js said start AppRegistry. RunApplication; The pages are all up; Why not talk about JS to native? To be honest, I’m not being lazy, but I want to deeply understand JS calling native after you know the overall process of RN initialization, the process of Loading and executing Jsbundle of RN, and the three big bases of native calling JS.

Js to native may be more convoluting, let’s first take a look at the whole process:

RN’s official documentation tells us that you can use NativeModules to communicate with iOS; So let’s first look at how we use NativeModules on the JS side.

import { NativeModules } from "react-native";
// Get your native Module on iOS :ReactJSBridge
const JSBridge = NativeModules.ReactJSBridge;
// Call the corresponding method on the corresponding Module
JSBridge.callWithCallback();
Copy the code

NativeModules == global. NativeModuleProxy == native nativeModuleProxy; NativeModules == native nativeModuleProxy; We talked about earlier in the Rn initialization phase where the initializeRuntime of JSIExecutor is called when the NativeToJsBridge initializes; Initialize some Bridges between JS and native.

let NativeModules: { [moduleName: string]: Object. } = {};if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
}
Copy the code
// NativeToJsBridge.cpp
void NativeToJsBridge::initializeRuntime() {
  runOnExecutorQueue(
      [](JSExecutor *executor) mutable { executor->initializeRuntime(); });
}
// JSIExecutor.cpp
void JSIExecutor::initializeRuntime() {
  SystraceSection s("JSIExecutor::initializeRuntime");
  runtime_->global().setProperty(
      *runtime_,
      "nativeModuleProxy",
      Object::createFromHostObject(
          *runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));
}

Copy the code

NativeModuleProxy::get (NativeModuleProxy::get) is triggered when you call NativeModules. And a synchronous invocation JSINativeModules: : getModule and JSINativeModules: : createModule method; In JSINativeModules: : createModule method will use js the __fbGenNativeModule for Module information. Check the __fbGenNativeModule function on the JS side and find **__fbGenNativeModule== genModule method on the JS side **

// JSIExecutor.cpp NativeModuleProxy
  Value get(Runtime &rt, const PropNameID &name) override {
    if (name.utf8(rt) == "name") {
      return jsi::String::createFromAscii(rt, "NativeModules");
    }

    auto nativeModules = weakNativeModules_.lock();
    if(! nativeModules) {return nullptr;
    }

    return nativeModules->getModule(rt, name);
  }
// JSINativeModules.cpp
Value JSINativeModules::getModule(Runtime &rt, const PropNameID &name) {
  if(! m_moduleRegistry) {return nullptr;
  }

  std::string moduleName = name.utf8(rt);

  const auto it = m_objects.find(moduleName);
  if(it ! = m_objects.end()) {return Value(rt, it->second);
  }

  auto module = createModule(rt, moduleName);
  if (!module.hasValue()) {

    return nullptr;
  }

  auto result =
      m_objects.emplace(std::move(moduleName), std::move(*module)).first;
  return Value(rt, result->second);
}

folly::Optional<Object> JSINativeModules::createModule(
    Runtime &rt,
    const std::string &name) {

  if(! m_genNativeModuleJS) { m_genNativeModuleJS = rt.global().getPropertyAsFunction(rt,"__fbGenNativeModule");
  }

  auto result = m_moduleRegistry->getConfig(name);

  Value moduleInfo = m_genNativeModuleJS->call(
      rt,
      valueFromDynamic(rt, result->config),
      static_cast<double>(result->index));

  folly::Optional<Object> module( moduleInfo.asObject(rt).getPropertyAsObject(rt, "module"));


  return module;
}
Copy the code

The JS getModule function uses the Module information (moduleName,moduleInfo) passed by native Module. The current need to perform the function of BatchedBridge into the queue. The enqueueNativeCall. When native calls any JS method, the queue is returned to native, and then native executes the method to be called in this queue.

If native does not call JS late, JS sets a time threshold, which is 5ms. If there is no Native Call JS after 5ms. Then JS will actively trigger the refresh of the queue, that is, immediately let the native side execute a series of methods cached in the queue.

// NativeModules.js
function genModule(config: ? ModuleConfig, moduleID: number): ?{
  name: string,
  module? :Object. } {const [moduleName, constants, methods, promiseMethods, syncMethods] = config;

  if(! constants && ! methods) {// Module contents will be filled in lazily later
    return { name: moduleName };
  }

  const module = {};
  methods &&
    methods.forEach((methodName, methodID) = > {
      const isPromise =
        promiseMethods && arrayContains(promiseMethods, methodID);
      const isSync = syncMethods && arrayContains(syncMethods, methodID);
      const methodType = isPromise ? "promise" : isSync ? "sync" : "async";
      // The genMethod will queue the current Method
      module[methodName] = genMethod(moduleID, methodID, methodType);
    });

  Object.assign(module, constants);

  return { name: moduleName, module };
}

// export this method as a global so we can call it from native
global.__fbGenNativeModule = genModule;

function genMethod(moduleID: number, methodID: number, type: MethodType) {
  let fn = null;
  // If it is a PROMISE type, it needs to be queued
  if (type === "promise") {
    fn = function promiseMethodWrapper(. args:Array<any>) {
      // In case we reject, capture a useful stack trace here.
      const enqueueingFrameError: ExtendedError = new Error(a);return new Promise((resolve, reject) = > {
        BatchedBridge.enqueueNativeCall(
          moduleID,
          methodID,
          args,
          (data) = > resolve(data),
          (errorData) = >
            reject(updateErrorWithErrorData(errorData, enqueueingFrameError))
        );
      });
    };
  } else {
    fn = function nonPromiseMethodWrapper(. args:Array<any>) {
      const lastArg = args.length > 0 ? args[args.length - 1] : null;
      const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
      const hasSuccessCallback = typeof lastArg === "function";
      const hasErrorCallback = typeof secondLastArg === "function";
      const onSuccess = hasSuccessCallback ? lastArg : null;
      const onFail = hasErrorCallback ? secondLastArg : null;
      const callbackCount = hasSuccessCallback + hasErrorCallback;
      args = args.slice(0, args.length - callbackCount);
      if (type === "sync") {
        return BatchedBridge.callNativeSyncHook(
          moduleID,
          methodID,
          args,
          onFail,
          onSuccess
        );
      } else {
          // Also remember to queue fearBatchedBridge.enqueueNativeCall( moduleID, methodID, args, onFail, onSuccess ); }}; } fn.type = type;return fn;
}

// MessageQueue.js
// Time threshold
const MIN_TIME_BETWEEN_FLUSHES_MS = 5;

enqueueNativeCall(
    moduleID: number,
    methodID: number,
    params: any[],
    onFail: ?Function,
    onSucc: ?Function.) {
    this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
    // Queue module,methodName, and parameter
    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);
    this._queue[PARAMS].push(params);

    const now = Date.now();
    // If native does not call JS late, JS specifies a time threshold, which is 5ms, if there is no native Call JS after 5ms. Then JS will actively trigger the refresh of the queue, that is, immediately let the native side execute a series of methods cached in the queue.
    if (
      global.nativeFlushQueueImmediate &&
      now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
    ) {
      const queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      global.nativeFlushQueueImmediate(queue);
    }
    this.__spy({
        type: TO_NATIVE,
        module: moduleID + ' '.method: methodID,
        args: params,
      });
  }
Copy the code

In JS side use nativeFlushQueueImmediate immediately call will trigger a native callNativeModules method, and implements a native method.

// JSIExecutor.cpp
void JSIExecutor::initializeRuntime() {

  runtime_->global().setProperty(
      *runtime_,
      "nativeFlushQueueImmediate",
      Function::createFromHostFunction(
          *runtime_,
          PropNameID::forAscii(*runtime_, "nativeFlushQueueImmediate"),
          1[this](
              jsi::Runtime &,
              const jsi::Value &,
              const jsi::Value *args,
              size_t count) {
            if(count ! =1) {
              throw std::invalid_argument(
                  "nativeFlushQueueImmediate arg count must be 1");
            }
            callNativeModules(args[0].false);
            return Value::undefined();
          }));
}
Copy the code

At this point, the explanation of JS to native is finished; Now let’s make a simple summary of js calling native.

  1. Js to native, NativeModules(NativeModules == globally. nativeModuleProxy == Native nativeModuleProxy) is used to call native GetModule ->createModule on the js side until __fbGenNativeModule is called on the JS side.

  2. The getModule function on the JS side returns the information about the current Module to the native side, and inserts the current moduleId and methodId into the queue by Params. Whether the time interval by comparing the two requests > 5 ms, will use nativeFlushQueueImmediate immediately call native modules.

A question? Why doesn’t JS call native directly instead of by stuffing it into a queue

Js trigger native is actually a very frequent process, you can imagine scrollView scrolling, animation implementation and so on, will bring very large performance overhead; If not cached immediately, the overall performance of RN will degrade; Therefore, the RN side makes use of the queue to cache native Modules calls; To achieve the purpose of performance optimization.

conclusion

Now that we have learned the Native to JS and JS to Native processes, let’s take a holistic look at how JS and Native interact.

Native to JS

  1. Native completes js code will send a RCTJavaScriptDidLoadNotification time to RCTRootView;

  2. RCTRootView after receiving time will use the batchedBridge – > enqueueJSCall to perform AppRegistry. RunApplication functions; Launch the RN page.

  3. Will perform enqueueJSCall process along the Instance – > NativeToJsBridge – > JSIExecutor the invocation chain call JSIExecutor: : callFunction method, Method calls within the JSIExecutor callFunctionReturnFlushedQueue_ method.

  4. CallFunctionReturnFlushedQueue_ callFunctionReturnFlushedQueue methods having and JS side has been binding, so in performing this JS function performed when callFunction method, Use js’s apply function to make a call to module.methodName.

JS to Native

  1. Js to native, NativeModules(NativeModules == globally. nativeModuleProxy == Native nativeModuleProxy) is used to call native GetModule ->createModule on the js side until __fbGenNativeModule is called on the JS side.

  2. The getModule function on the JS side returns the information about the current Module to the native side, and inserts the current moduleId and methodId into the queue by Params. Whether the time interval by comparing the two requests > 5 ms, will use nativeFlushQueueImmediate immediately call native modules.

ReactNative and iOS native communication principles parsing series

  • ReactNative and iOS native communication principles Analysis – initialization
  • ReactNative and iOS native communication principle analysis -JS loading and execution
  • ReactNative and iOS native communication principle analysis – communication

If you find this article useful, welcome to star