RY 's Blog

Dive into react native module

2020-12-23

Native Modules is one of the key part of React Native which enables JavasScript to call methods in iOS or Android native implementation.

It is quite interesting to explore the source code about native modules in react native repo. In this article, I would like to talk something about how React Native register, initialize native modules and what is behind the method calling from JavaScript side to iOS Objective-C modules.

First of all, letโ€™s take look at RCTBridgeModule` , which is a protocol provides interface needed to register a bridge module.

1
2
3
4
5
6
#define RCT_EXPORT_MODULE(js_name)  
#define RCT_EXPORT_METHOD(method)
+ (NSString *)moduleName;

@optional
+ (BOOL)requiresMainQueueSetup;

Export and Register Modules

RCT_EXPORT_MODULE is in RCTBridgeModule protocol, You can use this macro to export your iOS module.

Place this macro in your class implementation to automatically register your module with the bridge when it loads. The optional js_name argument. It will be used as the JS module name. If omitted, the JS module name will match the Objective-C class name.

For example, RCT_EXPORT_MODULE is used in RNCAsyncStorage.m#L404, react-native-async-storage.

This macro will be replaced by the following code in the preprocessor phase when you are compiling a source file.

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

In Objc world, load method is invoked whenever a class is added to the Objective-C runtime at the very beginning of app launch. At this time, it invokes RCTRegisterModule function to add the class object into the RCTModuleClasses array. RCTModuleClasses is an array containing a list of registered classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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];
});
}

When adding current module class into RCTModuleClasses array, it uses dispatch_barrier_async to dispatch this task to a concurrent queue, RCTModuleClassesSyncQueue. Using dispatch_barrier_async makes sure that
one async block executing at a time in RCTModuleClassesSyncQueue.

As we mentioned just now, RCTModuleClasses is a global array containing a list of registered classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Printing description of RCTModuleClasses:
<__NSArrayM 0x7fe4dc5d6bb0>(
....
RNSVGTSpanManager,
RNSVGUseManager,
RCTBaseTextViewManager,
RCTTextViewManager,
RNLinearTextGradientViewManager,
RCTVirtualTextViewManager,
RNVirtualLinearTextGradientViewManager,
RCTAccessibilityManager,
RCTActionSheetManager,
RCTActivityIndicatorViewManager,
RCTAlertManager,
.....
)

Register and Initialize Modules

  1. Modules in RCTModuleClasses are registered and initialized in start phase in RCTCxxBridge.mm. In initializeModules:withDispatchGroup:lazilyDiscovered, react native firstly registers all these automatically-exported modules.code here; then store them into array _moduleClassesByID, a bunch of pointers to Class objects, and _moduleDataByID referring to a bunch of RCTModuleData object. code here. The back trace for module registering is as follow:

    image-20201001142132537

  2. If the module needs to be set up in main thread. It will be guaranteed running in Main thread. code here and here. It makes sense that react native doc suggests us return NO in the requiresMainQueueSetup method.

If your module does not require access to UIKit, then you should respond to + requiresMainQueueSetup with NO.

  1. When the RNBridge is initialized. It inits the RCTCxxBridge and starts it as current bridge.RNBridge in Objc realm basically is a wrapper of RCTCxxBridge.mm. At that time, RN registers extra modules.

Just as what Lorenzo S. shared in this talk . The initialization of all modules in RCTCxxBridge start phase slows down its launch time. The more native module you have, the longer time it takes to initialize these modules even you wonโ€™t use them on the first page.

Store modules

In the registration phase, in [RCTCXXBridge _registerModulesForClasses:lazilyDiscovered:] , react native stores a list of registered classes into a dictionary _moduleDataByName. The key is the moduleName, the value is the RCTModuleData.

1
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;

Remember, in registering phase, The classes of modules are stored in_moduleClassesByID. While,_moduleDataByIDstoresRCTModuleData`.

1
2
3
// Native modules
NSMutableArray<RCTModuleData *> *_moduleDataByID;
NSMutableArray<Class> *_moduleClassesByID;

RCTModuleData

Now, you may wondering what is RCTModuleData? While, it is just the data structure to hold data for react native module.

image-20201215174307790

RCTBridgeModuleProvider in RCTModuleData is for initializing an instance of RCTBridgeModule. Then, RCTModuleData retain this instance. In initialization phase, RCTModuleData creates an instance for the module class by RCTBridgeModuleProvider . What RCTBridgeModuleProvider does is to provide an instance using[moduleClass new]. Code ref.

1
2
3
4
5
6
7
- (instancetype)initWithModuleClass:(Class)moduleClass
bridge:(RCTBridge *)bridge
{
return [self initWithModuleClass:moduleClass
moduleProvider:^id<RCTBridgeModule>{ return [moduleClass new]; }
bridge:bridge];
}

Get Module by name or class

RCTBridge bridge is a simple wrapper for the RCTCxxBridge , to make the methods in RCTCxxBridge.mm available for other Objective-C objects. You can access to these two methods to get the module from Objective-C objects.

1
2
3
// RCTBridge 
- (id)moduleForName:(NSString *)moduleName
- (id)moduleForClass:(Class)moduleClass

They will look up the _moduleDataByName dictionary to find out the target RCTModuleData and get its instance. Source code here

You might be interested in the relationship between these classes.

RCTNativeModule and ModuleRegistry

In RCTNativeModule.mm, RCTNativeModule, this c++ class inherits from the base class, NativeModule, which holds the info for modules.
The constructor method in RCTNativeModule takes in two parameters, RCTBridge object and RCTModuleData object.

Invoke method in RCTNativeModule

The invoke method in RCTNativeModule

  1. firstly get the methodName by methodId

  2. get the execution queue for current module

  3. construct a block, which calls invokeInner. invokeInner calls invokeWithBridge:module:arguments: in the RCTModuleMethod.mm.

    1
    invokeInner(weakBridge, weakModuleData, methodId, std::move(params), callId, isSyncModule ? Sync : Async);

image-20201029152216662

  1. if current module is executed in JSThread, then block for invoking the method is executed synchronously. But if methods in current module should be executed in other threads, react native will dispatch the previous block into the related queue.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    dispatch_queue_t queue = m_moduleData.methodQueue;
    const bool isSyncModule = queue == RCTJSThread;
    if (isSyncModule) {
    block();
    BridgeNativeModulePerfLogger::syncMethodCallReturnConversionEnd(moduleName, methodName);
    } else if (queue) {
    BridgeNativeModulePerfLogger::asyncMethodCallDispatch(moduleName, methodName);
    dispatch_async(queue, block);
    }

NativeModuleProxy

  • In the Runtime initialization phase, RCTxxBridge.start->Instance.initializeBridge->NativeToJSBridge.initializeRuntime->JSIExecutor.initializeRuntime,

    an NativeModuleProxy object is created and set as nativeModuleProxy property to the global object in JavaScript runtime context.

1
2
3
4
5
6
7
8
9
void JSIExecutor::initializeRuntime() {
...
runtime_->global().setProperty(
*runtime_,
"nativeModuleProxy",
Object::createFromHostObject(
*runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));
...
}
  • In NativeModule.js, JavaScript side can get a bunch of native modules from this nativeModuleProxy property from global object.
1
2
3
let NativeModules: {[moduleName: string]: $FlowFixMe, ...} = {};
if (global.nativeModuleProxy) {
NativeModules = global.nativeModuleProxy;
  • In the iOS side, the NativeModuleProxy class holds a weak pointer for JSINativeModules , which actually holds a list of registered native modules.
1
std::weak_ptr<JSINativeModules> weakNativeModules_; 

image-20201217183117953

Export Method

The RCT_EXPORT_METHOD macro is used to export method in Objective realm, and will be replaced by the following code

1
2
3
4
5
6
7
8
#define RCT_REMAP_METHOD(js_name, method)       \
_RCT_EXTERN_REMAP_METHOD(js_name, method, NO) \

+(const RCTMethodInfo *)RCT_CONCAT(__rct_export__, RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__)))
{
static RCTMethodInfo config = {#js_name, #method, is_blocking_synchronous_method};
return &config;
}

Basically, what it does is to construct a RCTMethodInfo structure.

1
2
3
4
5
typedef struct RCTMethodInfo {
const char *const jsName;
const char *const objcName;
const BOOL isSync;
} RCTMethodInfo;

The calculateMethods extracts exported methods and get the IMP of these methods. IMP actually is a c function which takes in class and selector as its parameters. And then, RCTMethodInfo and moduleClass are used to construct RCTModuleMethod, which confirms to RCTBridgeMethod.

RCTBridgeMethod

The RCTModuleData also contains information about exported methods.

1
2
3
4
5
6
7
8
9
10
11
/**
* Returns the module methods. Note that this will gather the methods the first
* time it is called and then memoize the results.
*/
@property (nonatomic, copy, readonly) NSArray<id<RCTBridgeMethod>> *methods;

/**
* Returns a map of the module methods. Note that this will gather the methods the first
* time it is called and then memoize the results.
*/
@property (nonatomic, copy, readonly) NSDictionary<NSString *, id<RCTBridgeMethod>> *methodsByName;

The getter methods for these two will generate a list of RCTBridgeMethod objects. RCTBridgeMethod is a protocol. Any class confirms to this protocol has to implement JSMethodName, functionType and invokeWithBridge: module:arguments: function.

1
2
3
4
5
6
7
8
@protocol RCTBridgeMethod <NSObject>

@property (nonatomic, readonly) const char *JSMethodName;
@property (nonatomic, readonly) RCTFunctionType functionType;

- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments;

@end

So when react native triggers these two getter method? Well, in the RCTUIManager

1
2
3
4
RCT_EXPORT_METHOD(dispatchViewManagerCommand
: (nonnull NSNumber *)reactTag commandID
: (id /*(NSString or NSNumber) */)commandID commandArgs
: (NSArray<id> *)commandArgs)

Beside, RCTModuleData also sets up and holds the thread methodQueue for the native module. This dispatch_queue_t object is also retained in RCTBridgeModule.h, which is used to call all exported methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)setUpMethodQueue
{
if (_instance && !_methodQueue && _bridge.valid) {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData setUpMethodQueue]", nil);
BOOL implementsMethodQueue = [_instance respondsToSelector:@selector(methodQueue)];
if (implementsMethodQueue && _bridge.valid) {
_methodQueue = _instance.methodQueue;
}
if (!_methodQueue && _bridge.valid) {
// Create new queue (store queueName, as it isn't retained by dispatch_queue)
_queueName = [NSString stringWithFormat:@"com.facebook.react.%@Queue", self.name];
_methodQueue = dispatch_queue_create(_queueName.UTF8String, DISPATCH_QUEUE_SERIAL);

// assign it to the module
if (implementsMethodQueue) {
@try {
[(id)_instance setValue:_methodQueue forKey:@"methodQueue"];
} @catch (NSException *exception) {
RCTLogError(
@"%@ is returning nil for its methodQueue, which is not "
"permitted. You must either return a pre-initialized "
"queue, or @synthesize the methodQueue to let the bridge "
"create a queue for you.",
self.name);
}
}
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
}

How does JS invoke a method in Native?

I set a breakpoint at [RCTModuleMethod invokeWithBridge:module:arguments:].

image-20201217171417579

  1. in initialization phase, nativeFlushQueueImmediate property is set in the global object of the JavaScript execution context.. code reference here. global.nativeFlushQueueImmediate(queue) is called in enqueueNativeCall from the Javascript side. code ref. Before calling, it makes sure the last Flush is 5 milliseconds ago; then nativeFlushQueueImmediate is triggered.
1
2
3
4
5
6
7
8
9
10
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);
}
  1. call function in JSCRuntime is invoked when there is a method call from JavaScrip side, then the host function is triggered. In this case, the host function is nativeFlushQueueImmediate in the C++ realm.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JSValueRef call(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
HostFunctionMetadata *metadata =
static_cast<HostFunctionMetadata *>(JSObjectGetPrivate(function));
JSCRuntime &rt = *(metadata->runtime);
...
// ๐ŸŒŸ๐ŸŒŸ
metadata->hostFunction_(rt, thisVal, args, argumentCount));
...
}
  1. See the following C++ lambda is used to create the host function nativeFlushQueueImmediate. In side this C++ lambda, we can see it calls callNativeModules.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}
)
);

Then callNativeModules in JSToNativeBridge is invoked.

  1. The callNativeModules in JSToNativeBridge firstly parses the JSON data to get the moduleIds, methodIds and params, etc. source code here. After constructing MethodCall structure, which holds moduleId, methodId, arguments, callId, callNativeModules in ModuleRegistry.cpp is called.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // This is always populated
std::vector<std::unique_ptr<NativeModule>> modules_;

void ModuleRegistry::callNativeMethod(
unsigned int moduleId,
unsigned int methodId,
folly::dynamic &&params,
int callId) {
if (moduleId >= modules_.size()) {
throw std::runtime_error(folly::to<std::string>(
"moduleId ", moduleId, " out of range [0..", modules_.size(), ")"));
}
modules_[moduleId]->invoke(methodId, std::move(params), callId);
}

moduleId is used to look up the NativeModule object in a list of modules. NativeModule list is set up in CxxBridge initialization phases, which basically is a vector holding module information in C++ realm, transformed from _moduleDataByID array in Objc realm. As we know _moduleDataByID is a list of RCTModuleData holding registered RCTBridgeModule and its instance.

  1. It goes to invoke in the RCTNativeModule.mm, which has module info in its private variable RCTModuleData object.

  2. Theinvoke function in the RCTNativeModule.mm calls invokeInner. Inside invokeInner, it get the RCTBridgeMethod object from RCTModuleData object using methodId . code

1
id<RCTBridgeMethod> method = moduleData.methods[methodId]

Then, it calls invokeWithBridge:module:arguments: in the RCTModuleMethod.mm.

image-20201029152216662

1
id result = [method invokeWithBridge:bridge module:moduleData.instance arguments:objcParams];

code ref

image-20201029162107074

  1. The invokeWithBridge:module:arguments: uses NSInvocation to send message to the related module object.

An NSInvocation object contains all the elements of an Objective-C message: a target, a selector, arguments, and the return value.

โ€‹
7.1. parse MethodSignature to init NSInvocation.

1
2
3
4
5
6
_selector = NSSelectorFromString(RCTParseMethodSignature(_methodInfo->objcName, &arguments));
....
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
...
// set selector
invocation.selector = _selector;

7.2, trigger the method in the module

1
[_invocation invokeWithTarget:module];

In a nutshell, when JavaScript side calls a method in a specific native module, the indices of the module and method are passed to Objc through JSCRuntime. These information is serialized as JSON object. By looking up the table which holds the information about registered modules and methods, React Native can invoke the specific method in a specific module in the Objective-C realm. There are some restrictions in this workflow.

image-20201001143632128

  1. Native Modules are specified in a package and are eagerly initialized. The startup time of React Native increases with the number of Native Modules, even if some of those Native Modules are never used.
  2. There is no simple way to check if the Native Modules that JavaScript calls are actually included in the native app. With over the air updates, there is no easy way to check if a newer version of JavaScript calls the right method with the correct set of arguments in Native Module.
  3. Native Modules are always singleton and their lifecycle is typically tied to the lifetime of the bridge. This issue is compounded in brownfield apps where the React Native bridge may start and shut down multiple times.
  4. During the startup process, Native Modules are typically specified in multiple packages. We then iterate over the list multiple times before we finally give the bridge a list of Native Modules. This does not need to happen at runtime.
  5. The actual methods and constants of a Native Module are computed during runtime using reflection.

scan qr code and share this article