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 | #define RCT_EXPORT_MODULE(js_name) |
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 | RCT_EXTERN void RCTRegisterModule(Class); |
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 | void RCTRegisterModule(Class 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 | Printing description of RCTModuleClasses: |
Register and Initialize Modules
Modules in
RCTModuleClasses
are registered and initialized in start phase inRCTCxxBridge.mm
. In initializeModules:withDispatchGroup:lazilyDiscovered, react native firstly registers all theseautomatically-exported
modules.code here; then store them into array_moduleClassesByID
, a bunch of pointers toClass
objects, and_moduleDataByID
referring to a bunch ofRCTModuleData
object. code here. The back trace for module registering is as follow: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 returnNO
in therequiresMainQueueSetup
method.
If your module does not require access to UIKit, then you should respond to
+ requiresMainQueueSetup
withNO
.
- When the
RNBridge
is initialized. It inits theRCTCxxBridge
andstarts
it as current bridge.RNBridge
in Objc realm basically is a wrapper ofRCTCxxBridge.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,
_moduleDataByIDstores
RCTModuleData`.
1 | // Native modules |
RCTModuleData
Now, you may wondering what is RCTModuleData
? While, it is just the data structure to hold data for react native module.
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 | - (instancetype)initWithModuleClass:(Class)moduleClass |
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 | // RCTBridge |
They will look up the _moduleDataByName
dictionary to find out the target RCTModuleData
and get its instance
. Source code here
Module-related Class
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
firstly get the
methodName
bymethodId
get the execution queue for current module
construct a block, which calls
invokeInner
.invokeInner
callsinvokeWithBridge:module:arguments:
in theRCTModuleMethod.mm
.1
invokeInner(weakBridge, weakModuleData, methodId, std::move(params), callId, isSyncModule ? Sync : Async);
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
9dispatch_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 asnativeModuleProxy
property to theglobal
object in JavaScript runtime context.
1 | void JSIExecutor::initializeRuntime() { |
- In
NativeModule.js
, JavaScript side can get a bunch ofnative modules
from thisnativeModuleProxy
property fromglobal
object.
1 | let NativeModules: {[moduleName: string]: $FlowFixMe, ...} = {}; |
- In the iOS side, the
NativeModuleProxy
class holds a weak pointer forJSINativeModules
, which actually holds a list of registered native modules.
1 | std::weak_ptr<JSINativeModules> weakNativeModules_; |
Export Method
The RCT_EXPORT_METHOD
macro is used to export method in Objective realm, and will be replaced by the following code
1 | #define RCT_REMAP_METHOD(js_name, method) \ |
Basically, what it does is to construct a RCTMethodInfo
structure.
1 | typedef struct 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 | /** |
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 | @protocol RCTBridgeMethod <NSObject> |
So when react native triggers these two getter method? Well, in the RCTUIManager
1 | RCT_EXPORT_METHOD(dispatchViewManagerCommand |
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 | - (void)setUpMethodQueue |
How does JS invoke a method in Native?
I set a breakpoint at [RCTModuleMethod invokeWithBridge:module:arguments:]
.
- in initialization phase,
nativeFlushQueueImmediate
property is set in the global object of the JavaScript execution context.. code reference here.global.nativeFlushQueueImmediate(queue)
is called inenqueueNativeCall
from the Javascript side. code ref. Before calling, it makes sure the last Flush is 5 milliseconds ago; thennativeFlushQueueImmediate
is triggered.
1 | if ( |
call
function in JSCRuntime is invoked when there is a method call from JavaScrip side, then thehost function
is triggered. In this case, the host function isnativeFlushQueueImmediate
in the C++ realm.
1 | JSValueRef call( |
- 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 | runtime_->global().setProperty( |
Then callNativeModules
in JSToNativeBridge
is invoked.
- The callNativeModules in
JSToNativeBridge
firstly parses theJSON
data to get themoduleIds
,methodIds
andparams
, etc. source code here. After constructingMethodCall
structure, which holdsmoduleId
,methodId
,arguments
,callId
,callNativeModules
inModuleRegistry.cpp
is called.
1 | // This is always populated |
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.
It goes to
invoke
in theRCTNativeModule.mm
, which has module info in its private variableRCTModuleData
object.The
invoke
function in theRCTNativeModule.mm
callsinvokeInner
. InsideinvokeInner
, it get theRCTBridgeMethod
object fromRCTModuleData
object usingmethodId
. code
1 | id<RCTBridgeMethod> method = moduleData.methods[methodId] |
Then, it calls invokeWithBridge:module:arguments:
in the RCTModuleMethod.mm
.
1 | id result = [method invokeWithBridge:bridge module:moduleData.instance arguments:objcParams]; |
- The
invokeWithBridge:module:arguments:
uses NSInvocation to send message to the relatedmodule
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 | _selector = NSSelectorFromString(RCTParseMethodSignature(_methodInfo->objcName, &arguments)); |
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.
- 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.
- 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.
- 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.
- 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.
- The actual methods and constants of a Native Module are computed during runtime using reflection.
scan qr code and share this article