preface

Although Google launched Flutter on the cross-platform platform, ReactNative has more advantages in terms of current ecology and big factory applications. Although this is a cross-platform framework launched in 2015, the ReactNative team has been optimizing it in recent years. In particular, great adjustments have been made to the Bridge which is often criticized by many people. The code has basically been changed from OC to CPP Bridge. It’s basically seamless with JavaScript, but the other thing is that it also allows you to unify the code on both sides of android and iOS. In my opinion, learning its design idea and design mode is far greater than its future ecology, so based on version 0.61.0, I summarize the source code of ReactNative. This paper starts with the business code of ReactNative and explores its internal principles step by step.

Start with the initialization of ReactNative

According to the ReactNative documentation, when we want a ViewController to be a container for RN, the implementation should look like this:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.bundle? platform=ios"]; / / initialize the rootView RCTRootView * rootView = [[RCTRootView alloc] initWithBundleURL: jsCodeLocation moduleName: @"RNHighScores"
                                             initialProperties:nil
                                                 launchOptions: nil];
    self.view = rootView;
}
Copy the code
  1. Initialize a RootView based on the url of the local JSBundle.
  2. Assign this rootView to the VC View.

In the RCTRootView initialization method, an RCTBridge is also created. This RCTBridge is the Bridge part of the ReactNative framework layer, which is a very important part

// RCTRootView.m

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                       moduleName:(NSString *)moduleName
                initialProperties:(NSDictionary *)initialProperties
                    launchOptions:(NSDictionary *)launchOptions
{
  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
                                            moduleProvider:nil
                                             launchOptions:launchOptions];

  return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}
Copy the code
// RCTBridge.m

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                   moduleProvider:(RCTBridgeModuleListProvider)block
                    launchOptions:(NSDictionary *)launchOptions
{
  return [self initWithDelegate:nil
                      bundleURL:bundleURL
                 moduleProvider:block
                  launchOptions:launchOptions];
}

- (void)setUp { ... Class bridgeClass = self.bridgeClass; . self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self]; [self.batchedBridge start]; } - (Class)bridgeClass {return [RCTCxxBridge class];
}
Copy the code

Inside rctrootView.m, the -start method of the RCTCxxBridge is finally used.

// RCTCxxBridge.mm - (void)start { // 1. Create a resident thread _jsThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoop) object:nil]; [_jsThread start]; dispatch_group_t prepareBridge = dispatch_group_create(); // 2. Load native modules [self registerExtraModules]; (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO]; [self registerExtraLazyModules]; // 3. Create Instance _reactinstance. reset(new Instance); __weak RCTCxxBridge *weakSelf = self; // Prepare ExecutorFactory (shared_ptrfor copy into block)
  std::shared_ptr<JSExecutorFactory> executorFactory;
  if(! self.executorClass) {if ([self.delegate conformsToProtocol:@protocol(RCTCxxBridgeDelegate)]) {
      id<RCTCxxBridgeDelegate> cxxDelegate = (id<RCTCxxBridgeDelegate>) self.delegate;
      executorFactory = [cxxDelegate jsExecutorFactoryForBridge:self];
    }
    if (!executorFactory) {
      executorFactory = std::make_shared<JSCExecutorFactory>(nullptr);
    }
  } else {
    id<RCTJavaScriptExecutor> objcExecutor = [self moduleForClass:self.executorClass];
    executorFactory.reset(new RCTObjcExecutorFactory(objcExecutor, ^(NSError *error) {
      if(error) { [weakSelf handleError:error]; }})); } // 5. Initialize the underlying Bridge dispatch_group_enter(prepareBridge); [self ensureOnJavaScriptThread:^{ [weakSelf _initializeBridge:executorFactory]; dispatch_group_leave(prepareBridge); }];  // 6. Load JS dispatch_group_enter(prepareBridge); __block NSData *sourceCode;
  [self loadSource:^(NSError *error, RCTSource *source) {
    if (error) {
      [weakSelf handleError:error];
    }

    sourceCode = source.data; dispatch_group_leave(prepareBridge); } onProgress:^(RCTLoadingProgress *progressData) { ... }]; Execute JS dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0). ^{ RCTCxxBridge *strongSelf = weakSelf;if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO]; }}); RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
Copy the code

The -start method runs through the entire startup process of ReactNative. The -start method mainly does the following:

  1. Create a JS thread (_jsThread) and open an internal runloop to make it resident. Initialize the underlying Bridge and execute the JS code in this thread.
  2. Load the native modules and initialize all of them to the native RCTBridgeModule used by JS. Each native module is initialized as RCTModuleData and thrown into the array.
  3. Initialize the Instance Instance, which is the top layer of the Bridge at the public layer, the entry point to the public part of the data, which has entered the CPP file. Instance is a wrapper around the underlying Bridge and provides some apis for executing Call JS.
  4. JSExecutorFactory uses the factory pattern to build different Jsexecutors in different scenarios, where the production environment uses JSCExecutorFactory to build JSIExecutor, which is an intermediate part of the underlying Bridge. I’m not going to talk about it.
  5. Initializes the underlying Bridge in the JS thread.
  6. Load the JS code.
  7. Execute the JS code.

The above source code uses a prepareBridge of dispatch_group_T type. Dispatch_group_t and dispatch_group_notify are combined to ensure that asynchronous code synchronization is executed in sequence. The loading of the native module is in the main thread, the initialization of the underlying Bridge is in the JS thread, the loading of JS code can be synchronous or asynchronous, these works are performed asynchronously, dispatch_group_notify can ensure that these works are completed, before the execution of JS code. The process of building the Bridge and loading and executing the JS code is long and will be described below.

Native module loading

First, use an example of official documents to see how native modules are loaded by ReactNative and provided to the JS side. Create a native module in ReactNative. Take the document calendar module as an example:

// CalendarManager.h

#import <React/RCTBridgeModule.h>
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
Copy the code
// CalendarManager.m
#import "CalendarManager.h"
#import <React/RCTLog.h>

@implementation CalendarManager

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}

@end
Copy the code
  1. Create a subclass of NSObject called CalendarManager, and followRCTBridgeModuleThe agreement.
  2. Implemented in the source fileRCT_EXPORT_MODULE().
  3. useRCT_EXPORT_METHODMacro to implement the method that needs to be exported to JS.

This creates a native module that can be called directly by the JS side:

// index.js

import {NativeModules} from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party'.'4 Privet Drive, Surrey');
Copy the code

So this is the end of a simple call, we just know that this call can pass parameters to CalendarManager, but we don’t know what ReactNative is doing behind the scenes, we don’t know why so let’s look at the source code, Let’s look at what RCT_EXPORT_MODULE() does.

This is a nested function of macros, which are layered as follows:

// RCTBridgeModule.h

RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

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);
  });

  RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
            @"%@ does not conform to the RCTBridgeModule protocol",
            moduleClass);

  // Register module
  dispatch_barrier_async(RCTModuleClassesSyncQueue, ^{
    [RCTModuleClasses addObject:moduleClass];
  });
}
Copy the code

Using +moduleName to return the native moduleName, override the + (void)load function. Call RCTRegisterModule() to register the class with the collection of native module classes. After the App starts, all modules actually use the +load method, that is, they call the RCTRegisterModule() method to register. RCTRegisterModule() determines whether the registered module complies with the RCTBridgeModule protocol. If it does, the registered module will be added to the RCTModuleClasses array. There’s actually a reading function that says later.

Let’s see what the second macro, RCT_EXPORT_METHOD, does. RCT_EXPORT_METHOD is also an expansion macro, but it is a bit more complex than the above, expanding as follows:

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); + (const RCTMethodInfo *)__rct_export__390 {static RCTMethodInfo config = {""."addEvent:(NSString *)name location:(NSString *)location",
      NO,
   };
   return &config;
 }
 - (void)addEvent:(NSString *)name location:(NSString *)location
 {
   RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
 }
Copy the code

The RCT_EXPORT_METHOD macro completes – (void), so that – (void) and its parameters restore the JS method defined above. It also becomes a function that concatenates __LINE__ and __COUNTER__ as method names on __rct_export__, which are two C macros representing line numbers and a built-in counter, presumably to generate a unique identifier. This function returns an object of type RCTMethodInfo, containing information about the exported function, including the JS name, the name of the native function, and whether it is a synchronous function.

Let’s dig into the source code and see how CalendarManager native modules are loaded by ReactNative and called by JS.

Go back to step 3 of the -start method and load the native module:

// RCTCxxBridge.mm - (void)start { dispatch_group_t prepareBridge = dispatch_group_create(); // Load manually exported native modules [self registerExtraModules]; / / load automatic export native modules (void) [self _initializeModules: RCTGetModuleClasses () withDispatchGroup: prepareBridge lazilyDiscovered:NO]; // Load the debug mode required native module [self registerExtraLazyModules]; . }Copy the code

We take the module of automatic registration as an example to explore the specific implementation part:

// RCTCxxBridge.mm

- (NSArray<RCTModuleData *> *)_registerModulesForClasses:(NSArray<Class> *)moduleClasses
                                        lazilyDiscovered:(BOOL)lazilyDiscovered
{
  RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
                          @"-[RCTCxxBridge initModulesWithDispatchGroup:] autoexported moduleData", nil);

  NSArray *moduleClassesCopy = [moduleClasses copy];
  NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray arrayWithCapacity:moduleClassesCopy.count];
  for (Class moduleClass in moduleClassesCopy) {
    if (RCTTurboModuleEnabled() && [moduleClass conformsToProtocol:@protocol(RCTTurboModule)]) {
      continue;
    }
    NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);

    // Check for module name collisions
    RCTModuleData *moduleData = _moduleDataByName[moduleName];
    if(moduleData) { ... Omit some code} moduleData = [[RCTModuleData alloc] initWithModuleClass: moduleClass bridge: self]. _moduleDataByName[moduleName] = moduleData; [_moduleClassesByID addObject:moduleClass]; [moduleDataByID addObject:moduleData]; } [_moduleDataByID addObjectsFromArray:moduleDataByID]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

  return moduleDataByID;
}
Copy the code

There are three main tables created:

  1. _moduleDataByID array: Use the RCTGetModuleClasses() to retrieve the modules previously registered in the array and iterate over creating an object of type RCTModuleData to store in the _moduleDataByID array. In effect, RCTModuleData wraps a native module in a layer, and an instance of a native module is just an attribute in RCTModuleData. Not only does this instantiate the native module, but it also does a few other things, which we’ll talk about later
  2. _moduleDataByName dictionary: Key is the +moduleName return value added automatically by the module export macro as mentioned earlier, and defaults to the current class name if not set.
  3. _moduleClassesByID array: Puts all module classes into the array.

At this point all native modules are loaded and initialized by ReactNative. JSBridge is a tool used to interact with native JS modules, whether Cordova framework or WKJavaScriptBridge. ReactNative also interacts with JSBridge, which is just a CPP version of the bridge. The bridge construction starts with -start. The -start function initializes the underlying Bridge in step 5. Let’s delve into the initialization process of the underlying Bridge.

Underlying Bridge construction

-start code for the underlying bridge construction is as follows:

// rctcxxbridge. mm - (void)start {// create JSThread _jsThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoop) object:nil]; _jsThread.name = RCTJSThreadName; _jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;#if RCT_DEBUG
  _jsThread.stackSize *= 2;
#endif[_jsThread start]; dispatch_group_t prepareBridge = dispatch_group_create(); . _reactInstance.reset(new Instance); __weak RCTCxxBridge *weakSelf = self; . dispatch_group_enter(prepareBridge); / / in _jsThread initialize the underlying bridge [self ensureOnJavaScriptThread: ^ {[weakSelf _initializeBridge: executorFactory]; dispatch_group_leave(prepareBridge); }]; . }Copy the code
  1. -startCreates a JS thread_jsThreadRunloop is not enabled for child threads by default. Child threads are released after executing tasks, so a runloop is enabled for child threads to keep them alive. The underlying Bridge is initialized, and js bundles are loaded/run on the JS thread.
  2. in_jsThreadThe underlying bridge is built in the thread.

Moving on _initializeBridge: What has been done:

// RCTCxxBridge.mm

- (void)_initializeBridge:(std::shared_ptr<JSExecutorFactory>)executorFactory
{
  __weak RCTCxxBridge *weakSelf = self;
  
  _jsMessageThread = std::make_shared<RCTMessageThread>([NSRunLoop currentRunLoop], ^(NSError *error) {
    if(error) { [weakSelf handleError:error]; }}); . [self _initializeBridgeLocked:executorFactory]; } - (void)_initializeBridgeLocked:(std::shared_ptr<JSExecutorFactory>)executorFactory { _reactInstance->initializeBridge( std::make_unique<RCTInstanceCallback>(self), executorFactory, _jsMessageThread, [self _buildModuleRegistryUnlocked]); }Copy the code
  1. Create a file named_jsMessageThreadThread, bound_jsThreadThe runloop allows its task to complete without exiting, byRCTCxxBridgeHolds and passes to the underlying bridge.
  2. Initialize the underlying bridge, which is aC++Function, which initializes a function calledNativeToJsBridgeThe instance.
// Instance.cpp

void Instance::initializeBridge(
    std::unique_ptr<InstanceCallback> callback,
    std::shared_ptr<JSExecutorFactory> jsef,
    std::shared_ptr<MessageQueueThread> jsQueue,
    std::shared_ptr<ModuleRegistry> moduleRegistry) {
  callback_ = std::move(callback);
  moduleRegistry_ = std::move(moduleRegistry);
  jsQueue->runOnQueueSync([this, &jsef, jsQueue]() mutable {
    nativeToJsBridge_ = std::make_unique<NativeToJsBridge>(
        jsef.get(), moduleRegistry_, jsQueue, callback_);

    std::lock_guard<std::mutex> lock(m_syncMutex);
    m_syncReady = true;
    m_syncCV.notify_all();
  });

  CHECK(nativeToJsBridge_);
}
Copy the code

The creation of NativeToJsBridge is performed synchronously in the thread passed above. The creation of nativeToJsBridge_ passes three parameters:

  1. InstanceCallback: Callback to the upper layer after the lower level call.

  2. JSExecutorFactory: The production environment is actually JSIExecutor, as this class will explain later.

  3. MessageQueueThread: _jsMessageThread passed from the upper layer.

  4. ModuleRegistry: It is responsible for two core tasks: generating an intermediate version of the Native module configuration information, which can be further processed and eventually imported to the JS side; and serving as a hub for JS call Native.

ModuleRegistry

ModuleRegistry contains all native Module information. For details, go back to rcTCxxbridge.mm to see the code:

// RCTCxxBridge.mm - (std::shared_ptr<ModuleRegistry>)_buildModuleRegistryUnlocked { ... auto registry = std::make_shared<ModuleRegistry>( createNativeModules(_moduleDataByID, self, _reactInstance), moduleNotFoundCallback); .return registry;
}
Copy the code
//RCTCxxUtils.mm

std::vector<std::unique_ptr<NativeModule>> createNativeModules(NSArray<RCTModuleData *> *modules, RCTBridge *bridge, const std::shared_ptr<Instance> &instance)
{
  std::vector<std::unique_ptr<NativeModule>> nativeModules;
  for (RCTModuleData *moduleData in modules) {
    if ([moduleData.moduleClass isSubclassOfClass:[RCTCxxModule class]]) {
      nativeModules.emplace_back(std::make_unique<CxxNativeModule>(
        instance,
        [moduleData.name UTF8String],
        [moduleData] { return [(RCTCxxModule *)(moduleData.instance) createModule]; },
        std::make_shared<DispatchMessageQueueThread>(moduleData)));
    } else{ nativeModules.emplace_back(std::make_unique<RCTNativeModule>(bridge, moduleData)); }}return nativeModules;
}
Copy the code

_buildModuleRegistryUnlocked is mainly responsible for building and returns a ModuleRegistry instance, _moduleDataByID initialization was the primary module when RCTModuleData array, iterate through group here, ModuleRegistry generates the corresponding CPP version of NativeModule for the RCTModuleData module. In the ModuleRegistry constructor, these NativeModule will be held by ModuleRegistry:

ModuleRegistry::ModuleRegistry(std::vector<std::unique_ptr<NativeModule>> modules, ModuleNotFoundCallback callback)
    : modules_{std::move(modules)}, moduleNotFoundCallback_{callback} {}
Copy the code

This modules_ is crucial because it will be necessary for the JS side to access any configuration information provided by Native in the future. Since NativeModule is installed in ModuleRegistry, let’s look at the NativeModule function. NativeModule defines a set of interfaces for retrieving NativeModule information and calling NativeModule functions:

// NativeModule.h
class NativeModule {
 public:
  virtual ~NativeModule() {// get module information virtual STD ::string getName() = 0; virtual std::vector<MethodDescriptor> getMethods() = 0; virtual folly::dynamic getConstants() = 0; // Call the native function Virtual void invoke(unsigned int reactMethodId, Folly ::dynamic&& params, int callId) = 0; virtual MethodCallResult callSerializableNativeHook(unsigned int reactMethodId, folly::dynamic&& args) = 0; };Copy the code

NativeModule provides an interface but does not implement it. The real implementation is in RCTNativeModule, which is a C++ NativeModule description class that encapsulates the OC version RCTModuleData. Is for the underlying CPP layer Bridge.

Getting module information, let’s take getConstants as an example, as you can see from the name, this is getting module constant information. Let’s dig deeper to see how it gets constant information:

// RCTNativeModule.mm

folly::dynamic RCTNativeModule::getConstants() {... NSDictionary *constants = m_moduleData.exportedConstants; folly::dynamic ret = convertIdToFollyDynamic(constants);return ret;
}
Copy the code

M_moduleData is our OC version of the native module information RCTModuleData, we continue down:

// RCTModuleData.mm

- (NSDictionary<NSString *, id> *)exportedConstants
{
  [self gatherConstants];
  NSDictionary<NSString *, id> *constants = _constantsToExport;
  _constantsToExport = nil; // Not needed anymore
  return constants;
}
Copy the code
- (void)gatherConstants {// If there is a constant export and it is the first exportif(_hasConstantsToExport && ! _constantsToExport) { RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, ([NSString stringWithFormat:@"[RCTModuleData gatherConstants] %@", _moduleClass]), nil); // Make sure the native module exists (void)[self instance]; // Since the entire bridge construction and js/native communication are carried out in jsThread, if you need to obtain constantsToExport from the main thread, you need to switch back to the main threadif (_requiresMainQueueSetup) {
      if(! RCTIsMainQueue()) { RCTLogWarn(@"Required dispatch_sync to load constants for %@. This may lead to deadlocks", _moduleClass); } RCTUnsafeExecuteOnMainQueueSync(^{ self->_constantsToExport = [self->_instance constantsToExport] ? : @ {}; }); }else{ _constantsToExport = [_instance constantsToExport] ? : @ {}; } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); }}Copy the code

Our native Module is wrapped in a layer of RCTModuleData, which in turn is wrapped in a layer of RCTNativeModule. The RCTNativeModule is loaded into ModuleRegistry, which is also held by lower-level JSINativeModules. JSINativeModules, in turn, are held by lower levels of JSIExcutor, CalendarManager < -rCTModuleData < -rctnativemodule < -Moduleregistry < -jsinativemodules < -jsiexcutor, JSIExcutor will inject an object called NativeModuleProxy into JS side when executing JS code. When JS side calls this object, it will call into JSIExcutor of Native side. This gives us a roadmap for how our native module information is provided to the JS side. JSIExcutor NativeModuleProxy JSINativeModules will be discussed in detail later. For those of you who are not familiar with the ReactNative source code, it may be a bit confusing, but don’t worry, I’ll use the function call stack again when I talk about NativeModuleProxy.

All native modules are registered with ModuleRegistry, so you can execute native module methods as long as JS calls ModuleRegistry. Indeed, to understand the calling process, we need to first understand NativeToJsBridge. NativeToJsBridge is a C++ class, and the Instance mentioned above is a wrapper around NativeToJsBridge. NativeToJsBridge is closer to the bottom than Instance.

NativeToJsBridge

As the name implies, NativeToJsBridge is the summative of Native Call JS, so what is the relationship between it and JS Call Native? In fact, it also holds JSExecutor and JsToNativeBridge inside. Finally, the JsToNativeBridge is passed to JSIExecutor, who triggers JS Call Native. NativeToJsBridge NativeToJsBridge NativeToJsBridge NativeToJsBridge NativeToJsBridge NativeToJsBridge NativeToJsBridge NativeToJsBridge

// NativeToJsBridge.cpp class NativeToJsBridge { public: friend class JsToNativeBridge; // NativeToJsBridge(JSExecutorFactory* JSExecutorFactory, STD ::shared_ptr<ModuleRegistry> registry, std::shared_ptr<MessageQueueThread> jsQueue, std::shared_ptr<InstanceCallback> callback); virtual ~NativeToJsBridge(); / / into the module ID, method ID, the parameters are used to in void callFunction JS side to perform a function (STD: : string && module, STD: : string && method, folly::dynamic&& args); // Call the JS side callback void invokeCallback(double callbackId, Folly ::dynamic&& args); // Start JS application. If bundleRegistry is not empty, Void loadApplication(STD ::unique_ptr<RAMBundleRegistry>) void loadApplication(STD ::unique_ptr<RAMBundleRegistry> bundleRegistry, std::unique_ptr<const JSBigString> startupCode, std::stringsourceURL);
  void loadApplicationSync(
    std::unique_ptr<RAMBundleRegistry> bundleRegistry,
    std::unique_ptr<const JSBigString> startupCode,
    std::string sourceURL);

private:
  std::shared_ptr<JsToNativeBridge> m_delegate;
  std::unique_ptr<JSExecutor> m_executor;
  std::shared_ptr<MessageQueueThread> m_executorMessageQueueThread;
};
Copy the code

Because the example given in module loading does not contain the Native callback to JS, in fact, it can be seen from the above code that if there is a callback, NativeToJsBridge’s invokeCallback method will be triggered to call the callbackId back to JS side. The JS side gets the callbackId and executes the corresponding callback. A brief introduction to its member variables and methods:

Member variables

  1. M_delegate: reference of type JsToNativeBridge, used mainly for JS call Native
  2. M_executor: This class is created from the factory described above. The production environment is JSIExecutor and the debugging environment is RCTObjcExecutor. This is the aggregation of the underlying communication, both JS Call Native and Native Call JS are based on it.
  3. M_executorMessageQueueThread: not initialization NativeToJsBridge himself, but as the initialization parameters passed in, is the outermost layer_jsMessageThread. The NativeToJsBridge is passed in when Instance is initialized, and the INTERACTION between JS side and Native side is processed in this thread.

The main method

  1. void callFunction(std::string&& module, std::string&& method, folly::dynamic&& args); The purpose of this function is to call the JS method with the module ID and method ID and parameters

  2. void invokeCallback(double callbackId, folly::dynamic&& args); The purpose of this function is to trigger a JS callback with the callbackId and arguments. Usually, after JS calls Native method, Native calls back some asynchronous execution results to JS in the form of callback.

  3. void loadApplication( std::unique_ptr bundleRegistry, std::unique_ptr startupCode, std::string sourceURL); This method executes JS code and has a sibling called loadApplicationSync. As the name implies, this sibling is a synchronization function, so it itself executes JS code asynchronously.

The constructor

// NativeToJsBridge.cpp

NativeToJsBridge::NativeToJsBridge(
    JSExecutorFactory *jsExecutorFactory,
    std::shared_ptr<ModuleRegistry> registry,
    std::shared_ptr<MessageQueueThread> jsQueue,
    std::shared_ptr<InstanceCallback> callback)
    : m_destroyed(std::make_shared<bool>(false)),
      m_delegate(std::make_shared<JsToNativeBridge>(registry, callback)),
      m_executor(jsExecutorFactory->createJSExecutor(m_delegate, jsQueue)),
      m_executorMessageQueueThread(std::move(jsQueue)),
      m_inspectable(m_executor->isInspectable()) {}
Copy the code

NativeToJsBridge internally generates and holds JsToNativeBridge by taking the Native information Registry and the external incoming callback as input parameters, which is also the reason why JsToNativeBridge can Call Native. JsExecutorFactory in turn generates and holds an executor from the JsToNativeBridge and an external incoming JS thread. In a production environment, this executor is JSIExecutor. JS Call Native is actually a JSIExecutor that calls the JsToNativeBridge implementation. JSIExecutor is a JSIExecutor that calls the JsToNativeBridge implementation.

JSIExecutor

Through the above process, we understand that JSIExecutor is lower level than NativeToJsBridge. We can understand that Instance is the wrapper of NativeToJsBridge. NativeToJsBridge is a wrapper around JSIExecutor that actually calls callJSFunction: in Instance, which should be called in this order: Instance->NativeToJsBridge->JSIExecutor, JSIExecutor will call the lower level, this will say later. The same goes for JS calls to Native: JSIExecutor->JsToNativeBridge->ModuleRegistry. As mentioned earlier, in the constructor of NativeToJsBridge, jsExecutorFactory creates m_Executor using the JsToNativeBridge instances m_Delegate and jsQueue. We will focus on JSIExecutor in production as an example. In debug mode refer to RCTObjcExecutor, which both inherit from JSExecutor. The JSIExecutor for the production environment is produced by JSCExecutorFactory, as follows:

// JSCExecutorFactory.mm

 return std::make_unique<JSIExecutor>(
      facebook::jsc::makeJSCRuntime(),
      delegate,
      JSIExecutor::defaultTimeoutInvoker,
      std::move(installBindings));
Copy the code

Now that we know where it came from, let’s take a quick look at some of the key properties in JSIExecutor:

// JSIExecutor.h class JSIExecutor : public JSExecutor { ... STD ::shared_ptr<jsi::Runtime> runtime_; std::shared_ptr<ExecutorDelegate> delegate_; JSINativeModules nativeModules_; . };Copy the code
  1. Jsi: : Runtime runtime_ : JavaScriptCore is a JAVASCRIPT implementation based on THE C language version. It is used to create JS context, execute JS, and inject javascript into native objects.
  2. Delegate_ : This delegate initializes the JSIExecutor parameter in the NativeToJsBridge, which is the JsToNativeBridge object responsible for JS Call Native.
  3. NativeModules_ : ModuleRegistry is constructed from external incoming ModuleRegistry. When JS calls Native, ModuleRegistry in JSINativeModules will be used to obtain information about Native modules. This information is transmitted to the JS side through the __fbGenNativeModule function. Thus, it can be seen that the Native module information is not actively imported to the JS side, but is obtained from the JS side to the Native side. The general process is as follows, first from the Native sidegetModuleStart:
// JSINativeModules.cpp Value JSINativeModules::getModule(Runtime& rt, const PropNameID& name) { ... Auto Module = createModule(rt, moduleName); . auto result = m_objects.emplace(std::move(moduleName), std::move(*module)).first;return Value(rt, result->second);
}

Copy the code

GetModule calls createModule internally. What does the createModule method do?

// JSINativeModules.cpp

folly::Optional<Object> JSINativeModules::createModule(
    Runtime& rt,
    const std::string& name) {
  ...
  if(! __fbgennativemoduleJS = 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)); .return module;
}
Copy the code
  1. Get JS side global function__fbGenNativeModule, to call this function can be called to the JS side, the details of this function will be described later.
  2. According to the module name, goModuleRegistryTo retrieve module information,ModuleRegistryAs defined above, this is where all the acoustic module information is stored.
  3. call__fbGenNativeModuleThis global function also passes in the native module information retrieved from ModuleRegistry.

So where is getModule called?

NativeModuleProxy

NativeModuleProxy is the only entry to getModule:

// JSIExecutor.cpp class JSIExecutor::NativeModuleProxy : public jsi::HostObject { public: NativeModuleProxy(JSIExecutor &executor) : Executor_ (executor) {} // The NativeModuleProxy get method is used to obtain native Module information. Value get(Runtime &rt, const PropNameID &name) override {returnexecutor_.nativeModules_.getModule(rt, name); }};Copy the code

So where is the NativeModuleProxy CPP class used? Global search NativeModuleProxy, you will find only one place to use NativeModuleProxy, JSIExecutor loadApplicationScript method, source code:

// JSIExecutor.cpp 

void JSIExecutor::loadApplicationScript(
    std::unique_ptr<const JSBigString> script,
    std::string sourceURL) {

  runtime_->global().setProperty(
      *runtime_,
      "nativeModuleProxy",
      Object::createFromHostObject(
          *runtime_, std::make_shared<NativeModuleProxy>(*this)));

  // ...
  
}
Copy the code

Runtime is a JSCRuntime object. A global global object is obtained by calling rumtime_->global(). We then set an object named nativeModuleProxy to the Global object using the setProperty method. The global object on the JS side can access the nativeModuleProxy on the native side by using the name “nativeModuleProxy”. In essence, global.nativeModuleProxy on JS side is nativeModuleProxy on native side. In other words, our NativeModules on the JS side correspond to the nativeModuleProxy on the native side. We might as well look at the following JS side of the source code:

letNativeModules: {[moduleName: string]: Object, ... } = {};if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
} else if(! global.nativeExtensions) { const bridgeConfig = global.__fbBatchedBridgeConfig; invariant( bridgeConfig,'__fbBatchedBridgeConfig is not set, cannot invoke native modules',); . }Copy the code

When we write ReactNative code using NativeModules is native side nativeModuleProxy object, the article at the beginning of example NativeModules. CalendarManager, In fact, they will first go to the nativeModuleProxy on the Native side, then to JSINativeModules, and then call the getModule method to obtain the information of CalendarManager through the module name of CalendarManager. Then Call the global JS function __fbGenNativeModule, passing the native information to the JS side.

Export native module information

Below, we go through the process of CalendarManager exporting constants in the form of function call stack, which can also be understood as the process of JS side obtaining the information of Native side. The method exporting is the same:

  • NativeModules – CalendarManager (JS)
  • Nativemoduleproxy-get (Enter Native side)
    • JSINativeModules-getModule
    • JSINativeModules-createModule
      • ModuleRegistry-getConfig
        • RCTNativeModule-getConstants
          • RCTModuleData-exportedConstants
            • CalendarManager-constantsToExport (Enter custom CalendarManager)

We can also analyze what JS side did after getting the configuration information of Native side:

/ / the react - native/Libraries/BatchedBridge/NativeModules js / / generate native module informationfunctiongenModule( config: ? ModuleConfig, moduleID: number, ): ? {name: string, module? : Object} { const [moduleName, constants, methods, promiseMethods, syncMethods] = config; const module = {}; ForEach ((methodName, methodName)) methodID) => { const isPromise = promiseMethods && arrayContains(promiseMethods, methodID); const isSync = syncMethods && arrayContains(syncMethods, methodID); const methodType = isPromise ?'promise' : isSync ? 'sync' : 'async'; // Generate JS function module[methodName] = genMethod(moduleID, methodID, methodType); }); // Add native modules Export constants Object.assign(module, constants); // Add native modules export constants object. assign(module, constants);if (module.getConstants == null) {
    module.getConstants = () => constants || Object.freeze({});
  } else{... }return{name: moduleName, module}; // Export genModule to global so native can call global.__fbgennativemodule = genModule; // Export genModule to global so native can call global.__fbgennativemodule = genModule; // Generate the function genMethod(moduleID: number, methodID: number,type: MethodType) {
  let fn = null;
  if (type= = ='promise') {
    fn = function(... args: Array<any>) {returnThe new Promise ((resolve, reject) = > {/ / function team BatchedBridge. EnqueueNativeCall (moduleID, methodID, args. data => resolve(data), errorData => reject(createErrorFromErrorData(errorData)), ); }); }; }else if (type= = ='sync') {
    fn = function(... args: Array<any>) {return global.nativeCallSyncHook(moduleID, methodID, args);
    };
  } else {
    fn = function(... args: Array<any>) { ... BatchedBridge.enqueueNativeCall(...) ; }; } fn.type =type;
  return fn;
}
Copy the code

ModuleName, module} {name: moduleName, module} returns to the native end. The native end takes the module and constructs it as a JS object for JS to use:

Value moduleInfo = m_genNativeModuleJS->call( rt, valueFromDynamic(rt, result->config), static_cast<double>(result->index)); CHECK(! moduleInfo.isNull()) <<"Module returned from genNativeModule is null";

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

At this point, JSIExecutor initialization is complete, and even after JSBridge is built, Native Call JS will eventually reach JS through Instance, NativeToJSBridge, and JSIExecutor.

To load the JS

By now, all the native modules have been loaded, and all the information of the native modules has been exported to the JS side for use. The NativeToJsBridge and JSIExecutor construction of the Bridge from RCTCxxBridge to _reactInstance to instance have also been completed. However, the last step, how JS calls to native and how native calls back to JS, has not been expanded. Let’s go back to RCTCxxBrige’s -start method:

- (void)start {// Load js bundle asynchronously [self loadSource:^(NSError *error, RCTSource *source) {
    if (error) {
      [weakSelf handleError:error];
    }

    sourceCode = source.data; dispatch_group_leave(prepareBridge); } onProgress:^(RCTLoadingProgress *progressData) { }]; } // Execute JS dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ RCTCxxBridge *strongSelf = weakSelf;if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO]; }});Copy the code
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress
{
  ...
    __weak RCTCxxBridge *weakSelf = self;
    [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:^(NSError *error, RCTSource *source) {
      if (error) {
        [weakSelf handleError:error];
        return;
      }
      onSourceLoad(error, source); }]; } + (void)loadBundleAtURL:(NSURL *)scriptURL onProgress:(RCTSourceLoadProgressBlock)onProgress OnComplete :(RCTSourceLoadBlock)onComplete {// try to load int64_t synchronouslysourceLength;
  NSError *error;
  NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL
                                          runtimeBCVersion:JSNoBytecodeFileFormatVersion
                                              sourceLength:&sourceLength
                                                     error:&error];
  if (data) {
    onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength));
    return; }... // Synchronous loading failed Asynchronous loadingif (isCannotLoadSyncError) {
    attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete);
  } else {
    onComplete(error, nil);
  }
}

static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoadProgressBlock onProgress, RCTSourceLoadBlock onComplete)
{
  scriptURL = sanitizeURL(scriptURL);

  if(scripturl.fileurl) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); ^{ NSError *error = nil; NSData *source = [NSData dataWithContentsOfFile:scriptURL.path
                                              options:NSDataReadingMappedIfSafe
                                                error:&error];
      onComplete(error, RCTSourceCreate(scriptURL, source, source.length));
    });
    return; }}Copy the code

In fact, the loading of THE JS code and the creation of the underlying Bridge are executed concurrently, due to the dispatch_group, only after they are both executed and the native module is initialized.

Perform JS

Finally, asynchronous execution of JS code, in fact, the JS bundle is passed through layers, and finally delivered to JavaScriptCore to execute.

/ RCTCxxBridge.mm - (void)start { ... // Wait until the native module instance is created, the underlying Bridge is initialized, and the JS bundle is loaded. Dispatch_group_notify (prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ RCTCxxBridge *strongSelf = weakSelf;if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO]; }}); }Copy the code

ExecuteSourceCode executeSourceCode code is as follows:

- (void)executeSourceCode:(NSData *)sourceSync :(BOOL)sync {dispatch_block_t completion = ^{// execute temporary Native call JS [self _flushPendingCalls]; dispatch_async(dispatch_get_main_queue(), ^ {/ / main thread send notification [[NSNotificationCenter defaultCenter] postNotificationName: RCTJavaScriptDidLoadNotification object:self->_parentBridge userInfo:@{@"bridge": self}]; // Enable the scheduled task. Used to drive the JS ultimately end timing task [self ensureOnJavaScriptThread: ^ {[self - > _displayLink addToRunLoop: [NSRunLoop currentRunLoop]];}]. }); }; // Select JS execution mode according to sync (synchronous, asynchronous)if (sync) {
    [self executeApplicationScriptSync:sourceCode url:self.bundleURL];
    completion();
  } else {
    [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion]; }}Copy the code

The code above does two main things:

  1. Build the callback, which is executed after JS is finished.
  2. Execute the JS code.
- (void)enqueueApplicationScript:(NSData *)script url:(NSURL *)url onComplete:(dispatch_block_t)onComplete { // The bottom into the JS thread execution JS bundle [self executeApplicationScript: script url: url async: YES];if(onComplete) { _jsMessageThread->runOnQueue(onComplete); }}Copy the code

Since the underlying JS bundle is executed in the JS thread, _jsMessageThread->runOnQueue(onComplete) ensures that the JS bundle is executed first and the onComplete callback is executed later.

- (void)executeApplicationScript:(NSData *)script
                             url:(NSURL *)url
                           async:(BOOL)async
{
  [self _tryAndHandleError:^{
    NSString *sourceUrlStr = deriveSourceURL(url); . Self ->_reactInstance->loadScriptFromString(STD ::make_unique<NSDataBigString>(script))sourceUrlStr.UTF8String, ! async); }]; }Copy the code
// Instance.cpp
void Instance::loadScriptFromString(std::unique_ptr<const JSBigString> string,
                                    std::string sourceURL,
                                    bool loadSynchronously) {
    ...
    loadApplication(nullptr, std::move(string), std::move(sourceURL));
}

void Instance::loadApplication(std::unique_ptr<RAMBundleRegistry> bundleRegistry,
                               std::unique_ptr<const JSBigString> string,
                               std::string sourceURL) {
  nativeToJsBridge_->loadApplication(std::move(bundleRegistry), std::move(string),
                                     std::move(sourceURL));
}
Copy the code
// NativeToJsBridge.cpp void NativeToJsBridge::loadApplication( std::unique_ptr<RAMBundleRegistry> bundleRegistry, std::unique_ptr<const JSBigString> startupScript, STD ::string startupScriptSourceURL) {// Execute JS bundle runOnExecutorQueue([...  (JSExecutor* executor) mutable { ... executor->loadApplicationScript(std::move(*startupScript), std::move(startupScriptSourceURL)); }); }Copy the code

LoadApplication and loadApplicationSync are implemented in the same way. They both call the loadApplicationScript method of the m_Executor member variable. Difference between loadApplication to put the code in the m_executorMessageQueueThread execution, and loadApplicationSync execution in the current thread.

// JSIexecutor.cpp

void JSIExecutor::loadApplicationScript(
    std::unique_ptr<const JSBigString> script,
    std::string sourceURL) {

  runtime_->global().setProperty(
      *runtime_,
      "nativeModuleProxy",
      Object::createFromHostObject(
          *runtime_, std::make_shared<NativeModuleProxy>(*this)));

  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) {
            callNativeModules(args[0], false);
            return Value::undefined();
          }));

  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); })); EvaluateJavaScript (JSEvaluateScript runtime_->evaluateJavaScript( std::make_unique<BigStringBuffer>(std::move(script)),sourceURL);
      
  flush();
}
Copy the code
  1. NativeModuleProxy object is set to the Global object using the setProperty method. JS NativeModules correspond to native nativeModuleProxy.
  2. Injected into the global nativeFlushQueueImmediate nativeCallSyncHook two methods.
  3. callruntime_->evaluateJavaScriptMethod, which is finally called to JavaScriptCoreJSEvaluateScriptThe SEvaluateScript function executes JS code in a JS environment.
  4. The JS script is executed successfullyflushOperation. The primary role of the flush function is to execute method calls to Native cached in the JS side queue.
void JSIExecutor::flush() {// If JSIExecutor's flushedQueue_ function is not empty, the queue of methods to be called is obtained through the flushedQueue_ function, and callNativeModules are executedif (flushedQueue_) {
    callNativeModules(flushedQueue_->call(*runtime_), true);
    return; } / / in"__fbBatchedBridge"I'm going to take the corresponding value in global which is batchedBridge, Value batchedBridge = runtime_->global().getProperty(*runtime_,).batchedBridge = runtime_->global().getProperty(*runtime_,)"__fbBatchedBridge");
  if(! BatchedBridge. IsUndefined ()) {/ / bind batchedBridgebindBridge();
    callNativeModules(flushedQueue_->call(*runtime_), true);
  } else if(delegate_) {// If you do not get the batchedBridge object defined on the JS side, execute the callNativeModules method directly, i.ebindOperation. callNativeModules(nullptr,true); }}Copy the code

BatchedBridge: a batch bridge, ReactNative handles JS and Native communication not one call at a time, but saves the call message to a queue and flush it when appropriate.

flush()

FlushedQueue_ flushedQueue_ flushedQueue_ flushedQueue_ flushedQueue_ flushedQueue_ flushedQueue_ flushedQueue_ flushedQueue_ Will direct execution callNativeModules call native module method, if not, go to get batchedBridge JS side, what is batchedBridge, JS side a batchedBridge. JS, as follows:

// BatchedBridge.js

const MessageQueue = require('./MessageQueue');

const BatchedBridge: MessageQueue = new MessageQueue();

Object.defineProperty(global, '__fbBatchedBridge', {
  configurable: true,
  value: BatchedBridge,
});

module.exports = BatchedBridge;
Copy the code

BatchedBridge object is a MessageQueue on the JS side. After initialization, a global attribute called __fbBatchedBridge is defined. And set the BatchedBridge object as the value of this property, waiting to be used by JSIExecutor.

__fbBatchedBridge __fbBatchedBridge __fbBatchedBridge __fbBatchedBridge __fbBatchedBridge CallNativeModules directly pass null. If there are callNativeModules, it means that there are calls on the JS side, and the method of binding JSBridge on the Native side is required to take the JS call queue over for execution. Since there may be JS calls to Native during JS loading, flush is performed in advance to execute all of these calls.

bindBridge()

Another important place to flush() is in bindBridge:

// JSIExecutor.cpp

void JSIExecutor::bindBridge() {
  std::call_once(bindFlag_, [this] {
    SystraceSection s("JSIExecutor::bindBridge (once)");
    Value batchedBridgeValue =
        runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
    if (batchedBridgeValue.isUndefined()) {
      throw JSINativeException(
          "Could not get BatchedBridge, make sure your bundle is packaged correctly");
    }

    Object batchedBridge = batchedBridgeValue.asObject(*runtime_);
    callFunctionReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
        *runtime_, "callFunctionReturnFlushedQueue");
    invokeCallbackAndReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
        *runtime_, "invokeCallbackAndReturnFlushedQueue");
    flushedQueue_ =
        batchedBridge.getPropertyAsFunction(*runtime_, "flushedQueue");
    callFunctionReturnResultAndFlushedQueue_ =
        batchedBridge.getPropertyAsFunction(
            *runtime_, "callFunctionReturnResultAndFlushedQueue");
  });
}
Copy the code

BindBridge binds four methods:

  • callFunctionReturnFlushedQueue
  • invokeCallbackAndReturnFlushedQueue
  • flushedQueue
  • callFunctionReturnResultAndFlushedQueue

These methods are all triggered by Native and return the JS side’s flushedQueue to the Native side for processing. In the case of callFunctionReturnFlushedQueue, The Native side callFunctionReturnFlushedQueue_ pointer pointing to the side of JS callFunctionReturnFlushedQueue method, When calling callFunctionReturnFlushedQueue_ will call directly to the side of JS, lateral JS method are defined as follows:

// 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();
  }
Copy the code
// MessageQueue.js

 flushedQueue(): null | [Array<number>, Array<number>, Array<any>, number] {
    this.__guard(() => {
      this.__callImmediates();
    });

    const queue = this._queue;
    this._queue = [[], [], [], this._callID];
    return queue[0].length ? queue : null;
  }
Copy the code

JS will call the JS service based on moduleId and methodId, then execute the flushedQueue function to empty the queue, and return it to Native. Once the Native side is in the queue, the JSIExecutor->callNativeModules->ModuleRegistry will continue. At this point, the entire JS execution process is complete, and completion() is executed, and the code returns to rcTCxxbridge.mm executeSourceCode:sync:

// RCTCxxBridge.mm

dispatch_block_t completion = ^{
    ...
    dispatch_async(dispatch_get_main_queue(), ^{
      [[NSNotificationCenter defaultCenter]
       postNotificationName:RCTJavaScriptDidLoadNotification
       object:self->_parentBridge userInfo:@{@"bridge": self}]; . }); };Copy the code

Bridge, will then send out a notification, called RCTJavaScriptDidLoadNotification this notice where implementation, can the global search we found that we returned to the top, RCTRootView.

// RCTRootView.m - (void)javaScriptDidLoad:(NSNotification *)notification { ... [self bundleFinishedLoading:bridge]; . } - (void)bundleFinishedLoading:(RCTBridge *)bridge { ... [self runApplication:bridge]; . } - (void)runApplication:(RCTBridge *)bridge { NSString *moduleName = _moduleName ? : @"";
  NSDictionary *appParameters = @{
    @"rootTag": _contentView.reactTag,
    @"initialProps": _appProperties ?: @{},
  };

  RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}
Copy the code

RunApplication calls enqueueJSCall, AppRegistry is the name of the component, runApplication is the method of the component, and args is the parameter passed. Then go RCTCxxBridge – > Instance – > NativeToJsBridge – > JSIExecutor – > callFunctionReturnFlushedQueue_ this big long list, CallFunctionReturnFlushedQueue_ is actually JS callFunctionReturnFlushedQueue method of the lateral side of JS starts executing entrance rendering interface, to return flushedQueue to Native, Native then traverses queue according to moduleId and methodId to execute Native module methods. At this point, the entire interaction process ends. The loading of the native module, the construction of the underlying Bridge, the loading and execution of JS code are all analyzed. Based on the above analysis, the complete interaction process between JS and Native is summarized.

Native Call JS

The process from Native To JS is relatively simple. The above basic analysis is almost in ten. The following is a detailed review of this process.

// RCTRootView.m
- (void)runApplication:(RCTBridge *)bridge
{
... 
  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}
Copy the code
// RCTCxxBridge.mm

- (void)enqueueJSCall:(NSString *)module method:(NSString *)method args:(NSArray *)args completion:(dispatch_block_t)completion
{
  ...
  __weak __typeof(self) weakSelf = self;
  [self _runAfterLoad:^(){
    RCTProfileEndFlowEvent();
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if(! strongSelf) {return;
    }

    if (strongSelf->_reactInstance) {
      strongSelf->_reactInstance->callJSFunction([module UTF8String], [method UTF8String],
                                             convertIdToFollyDynamic(args ?: @[]));
   ...
}
Copy the code

_runAfterLoad is used to check whether the JS bundle has been loaded. If it has been loaded, call the block directly, and then call the Instance callJSFunction. Otherwise, the block will be stored until the js bundle is loaded. In fact, this is a knowledge point, if you encounter such a scenario, your call needs to wait for another asynchronous task to complete, if the knowledge is simple to execute in order, perhaps GCD can be done, but such instructions sent in batches, how to deal with? You can completely refer to ReactNative’s textbook handling of this area. Without further ado, let’s move on:

// Instance.cpp

void Instance::callJSFunction(
    std::string &&module,
    std::string &&method,
    folly::dynamic &&params) {
  callback_->incrementPendingJSCalls();
  nativeToJsBridge_->callFunction(
      std::move(module), std::move(method), std::move(params));
}
Copy the code

Instance continues as a transfer of NativeToJsBridge:

// NativeToJsBridge.cpp void NativeToJsBridge::callFunction(module, method, arguments) { runOnExecutorQueue([...] (JSExecutor* executor) { executor->callFunction(module, method, arguments); }); }Copy the code
// JSIExecutor.cpp

void JSIExecutor::callFunction(
    const std::string &moduleId,
    const std::string &methodId,
    const folly::dynamic &arguments) {
  SystraceSection s(
      "JSIExecutor::callFunction"."moduleId", moduleId, "methodId", methodId);
  if(! callFunctionReturnFlushedQueue_) {bindBridge();
  }

  // Construct the error message producer in case this times out.
  // This is executed on a background thread, so it must capture its parameters
  // by value.
  auto errorProducer = [=] {
    std::stringstream ss;
    ss << "moduleID: " << moduleId << " methodID: " << methodId
       << " arguments: " << folly::toJson(arguments);
    return ss.str();
  };

  Value ret = Value::undefined();
  try {
    scopedTimeoutInvoker_(
        [&] {
          ret = callFunctionReturnFlushedQueue_->call(
              *runtime_,
              moduleId,
              methodId,
              valueFromDynamic(*runtime_, arguments));
        },
        std::move(errorProducer));
  } catch (...) {
    std::throw_with_nested(
        std::runtime_error("Error calling " + moduleId + "." + methodId));
  }

  callNativeModules(ret, true);
}
Copy the code

Will perform to callFunctionReturnFlushedQueue_ callFunctionReturnFlushedQueue_ has been analyzed above, Call callFunctionReturnFlushedQueue_ equivalent to a direct call to the MessageQueue. Js callFunctionReturnFlushedQueue function. CallFunctionReturnFlushedQueue page has been analyzed above, JS may be according To the parameters of the direct call To the side of the JS business module, execute the business code, at this point, Native To JS, the entire process has been completed Next, I will analyze the process of JS To Native.

JS Call Native

Through the above analysis, have been already very clear, JS side in performing NativeModules. CalendarManager, JS side will be to the Native side get all the information of export RCTCalendarManager module, The enqueueNative() method of messagequeue.js is then called and both the call and the callback are stored in the MessageQueue queue, not executed immediately. This is a means for ReactNative to optimize Bridge interaction, which will be discussed later. In the Native side call callFunctionReturnFlushedQueue, JS code executes a flushedQueue processed business, the whole process of flushedQueue have analyzed above, confused the classmates can scroll up. After executing the flushedQueue on the JS side, all the call instructions stored in the queue will be returned to the Native side. After receiving the MessageQueue, the Native side will go through the long process mentioned above and finally reach the Native business side. After processing the service, if the callback is needed, Native will continue to execute enqueueCallback:, and finally to JS side. JS side will fetch the previously stored callback callback, and then callback to JS business code, and the whole process ends. Here is no paste source code, because the above basic analysis about. Finally, we will analyze why JS calls are stored in MessageQueue instead of being called directly. This starts with the JS flushQueue.

flushedQueue

JS bridge allows JS and Native to interact directly, but also brings performance loss. Especially for frameworks like ReactNative, JS interacts with Native very frequently. It can be imagined that the scrolling of scrollView and the implementation of animation will bring very large performance overhead. FlushedQueue doesn’t solve this problem perfectly, but it’s optimized to the hilt. JS Call Native does not Call the Native method directly, but puts the Call message to the queue first:

// MessageQueue.js 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); this._queue[MODULE_IDS].push(moduleID); this._queue[METHOD_IDS].push(methodID); this._queue[PARAMS].push(params); const now = Date.now();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); }}Copy the code

The module name and method name pushed by the _queue variable are ids, not real types. In fact, there is a mapping table on the Native side. The ID from the JS side is mapped to the specific class and method, and the message is sent via the NSInvocation assembly parameter. As for why ID is passed, the purpose is to reduce the amount of communication data and reduce the cost of communication. In addition, the JS side does not actively send these data to the Native side after storing them in the _queue. Instead, the Native side comes to fetch them at an appropriate time, which is the binding method in bindBridge(). Now, some of you might ask, when is this time? In our daily development, code execution and message delivery are usually performed only when the event responds, which may be a screen click event, or a timer event, or a system event, etc. The same is true for ReactNative, whose UI pages are all Native implementations. When Native triggers an event, it will Call JS, or it may actively invoke JS, which may update UI, or simply start an event such as button clicking. After the business logic is processed, the JS side will execute flushQueue and return the message queue to the Native side. The main purpose of this is to reduce communication costs and improve bridge performance, which is almost the same as the design of Cordova framework. Reducing the size of the data and reducing the frequency of interactions is almost optimization to the limit.

If Native never calls the JS side, will the message in the queue never be executed? MIN_TIME_BETWEEN_FLUSHES_MS constant if you’ve noticed, Every time enqueueNativeCall will take the last time to empty the queue (flushQueue invokeCallback or executive nativeFlushQueueImmediate will reset the time) and is now, If JS call Native batch call time interval > = 5 milliseconds, then perform a nativeFlushQueueImmediate (), this function is why didn’t say before, I am also chasing the JS source side to find this function. This is also a global function, and it is the only method that JS side actively calls Native side. Other JS calls Native side are passively waiting for Native to fetch. I then searched globally for the following and found that this method was injected into the JS side while JSIExecutor was loadApplicationScript. The JS side called this method, The Native side performs callNativeModules() to distribute messages to individual modules.

So far, the core code of ReactNative has almost gone through. We will continue to analyze the UI rendering level of ReactNative and analyze how it dynamically modifys our UI components.