This paper analyzes the JS to Native communication mechanism in ReactNative step by step by analyzing the source code.
This post is also published on my personal blog
Overview
This paper analyzes the invocation process of JS to Native in detail, including: initialization of ReactNative, registration of Native Module, JS acquisition of Native Module information, AND JS invocation of Native Module.
The source code analyzed in this paper is based on ReactNative 0.47. For the convenience of narration, ReactNative is abbreviated as RN. In addition, in order to reduce the length of the focus, the source code listed in the article have been simplified, delete non-critical code. Since this article focuses on the implementation of the iOS side, everything that follows is for the iOS platform (most of which is shared).
The preparatory work
There are probably two kinds of purposes for reading source code. One is to understand the implementation details through the source code. The second is to understand the overall structure and principle of its implementation. In the first case, you can dive directly into the source code to quickly capture the focus; It’s easy to get bogged down in implementation details (especially in large source code) if the latter are too preoccupied with source code. At this point, you should first understand the overall structure of the code, grasp the critical path, and use class diagrams if necessary.
The RN communication mechanism discussed in this article is the core of the entire RN, from beginning to end, involving objective-C /Java, C++, and JavaScript interactions. Therefore, it is necessary to understand the overall structure of RN and the class diagram of its key classes before further analysis.
Cross-platform, hot update
C++
JS
Objective-C
C++
C++
BatchedBridge.js
MessageQueue.js
NativeModules.js
Let’s start by looking at a few key classes and their relationships to get an idea of the overall structure:
RCTBridge
withRCTCxxBridge
It is unique to iOS platform. The former is RN’s interface to the business layer (other classes in the figure belong to internal classes and are not aware of the business layer), and the specific work is in its subclassesRCTCxxBridge
The complete;- The core of RN is in the cross-platform C++ layer, where many of the functions of the classes are known by their names and described in more detail later.
- JS must be supported in RN. As can be seen from the figure above, communication between JS and Native takes place in
JavaScript
与C++
This is also the focus of this analysis.
When it comes to communication, there is nothing more than JS to Native, Native to JS, this paper focuses on the analysis of JS to Native.
JavaScript – > Native
With JavaScriptCore launched by Apple, it is not difficult to realize JS to Native communication. There are two main ways:
block
— In a complex application scenario like RNblock
Appears to be somewhat inadequate;JSExport
Protocol – Implementing this protocol exposes Native interfaces to JS (but does not have cross-platform capabilities).
Therefore, Instead of using the two methods provided by JavaScriptCore, RN implements its own communication mechanism. Let’s start with a simple example: CalendarManager encapsulates the iOS calendar control for JS invocation.
// CalendarManager.h
#import <React/RCTBridgeModule.h>
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
Copy the code
// CalendarManager.m
@implementation CalendarManager
// To export a module named 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
We know that to expose Native modules to JS, the module implements the RCTBridgeModule protocol and inserts the RCT_EXPORT_MODULE macro into the implementation. The specific exposure method also needs to be defined through the RCT_EXPORT_METHOD macro.
// JS
import { NativeModules } from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party'.'4 Privet Drive, Surrey');
Copy the code
At this point, can be used in JS NativeModules. CalendarManager. AddEvent (…). Method to call Native interface. We’ll take a look at this process one by one, starting with the two key macros mentioned above:
RCT_EXPORT_MODULE
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
Copy the code
As you can see, adding the RCT_EXPORT_MODULE macro defines the load and moduleName methods. It is in the load method that the RCTRegisterModule method is called to register the Module(which will affect App startup speed ^_^).
void RCTRegisterModule(Class moduleClass) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
});
// Register module
[RCTModuleClasses addObject:moduleClass];
}
Copy the code
The RCTRegisterModule simply collects all the classes that need to be exposed to JS.
RCT_EXPORT_METHOD
The interface exposed to JS needs to be defined by the RCT_EXPORT_METHOD macro.
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location) {
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
Copy the code
The final appearance of the expansion (due to the length of the specific expansion process is not described) :
+ (NSArray *)__rct_export__531 {
return@ [@ "".@"addEvent:(NSString *)name location:(NSString *)location", @NO];
}
- (void)addEvent:(NSString *)name location:(NSString *)location {
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
Copy the code
You can see that a new method has been added through the macro named __rct_export__+__LINE__+__COUNTER__. Note that all methods exposed to JS return void, and return results via callback.
Using macros gracefully and skillfully is full of skill.
The RCT_EXPORT_MODULE and RCT_EXPORT_METHOD macros described above are compile-time processing, which we will analyze step by step from an execution perspective.
Registered NativeModule
RCTCxxBridge._initModulesWithDispatchGroup
- (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
{
NSMutableArray<Class> *moduleClassesByID = [NSMutableArray new];
NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new];
NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
// Set up moduleData for automatically-exported modules
for (Class moduleClass in RCTGetModuleClasses()) {
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self];
moduleDataByName[moduleName] = moduleData;
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
// Store modules
_moduleDataByID = [moduleDataByID copy];
_moduleDataByName = [moduleDataByName copy];
_moduleClassesByID = [moduleClassesByID copy];
}
Copy the code
- The above code
8
lineRCTGetModuleClasses()
That is to get throughRCTRegisterModule
Registered Module classes (that is, all classes exposed to JS); - through
RCTRegisterModule
The registered module is used by defaultinit
Method. If the initialization of a module requires parameters, pass theRCTBridgeDelegate
->extraModulesForBridge
ormoduleProvider
Provides an initialized module instance.
At this point, all modules that need to be exposed to JS have been registered and stored in the RCTCxxBridge as RCTModuleData.
Most modules are lazily loaded; only modules that need to be initialized in the main thread and have constants that need to be exported are instantiated at registration time.
Instance
RCTCxxBridge._initializeBridge
- (void)_initializeBridge:(std::shared_ptr<JSExecutorFactory>)executorFactory
{
if (_reactInstance) {
_reactInstance->initializeBridge(
std::unique_ptr<RCTInstanceCallback>(new RCTInstanceCallback(self)),
executorFactory,
_jsMessageThread,
[self_buildModuleRegistry]); }}Copy the code
The most important thing the _initializeBridge method does is initialize Instance _reactInstance, This procedure converts all modules exposed to JS from RCTModuleData format to ModuleRegistry format and passes it into Instance.
- (std: :shared_ptr<ModuleRegistry>)_buildModuleRegistry
{
auto registry = std::make_shared<ModuleRegistry>(createNativeModules(_moduleDataByID, self, _reactInstance));
return registry;
}
Copy the code
At this point, it’s worth introducing a few classes related to NativeModule:
JSCNativeModules
–C++
In class,JSCExecutor
The use of;ModuleRegistry
–C++
Class,NativeModule
A collection of;NativeModule
–C++
Abstract classes that define interfaces associated with NativeModule.RCTNativeModule
To realize theNativeModule
Interface defined in;RCTModuleData
–OC
Class, which is a data structure that stores exposed moudle;RCTBridgeMethod
–OC
Class, which stores data structures exposed to JS interfaces (methods).
Instance is a transit class that doesn’t do much, so we move on.
NativeToJsBridge
At this time, came to the Instance: : initializeBridge – > NativeToJsBridge: : NativeToJsBridge, obviously NativeToJsBridge is Native to JS bridge. All calls from Native to JS are made from the interface in the NativeToJsBridge. Two member variables are initialized in its constructor:
m_executor
–JSExecutor
Type, as seen aboveJSExecutor
Is aC++
An abstract class,m_executor
The actual point toJSCExecutor
As the JS engine, both Native to JS and JS to Native ultimately need this class to process, which will be analyzed one by one later.m_delegate
–JsToNativeBridge
Pointer to type, as the name implies, JS to Native bridge, the member variable is used for initialization onlyJSCExecutor
Instance.
JSCExecutor
Coordinates to NativeToJsBridge: : NativeToJsBridge – > JSCExecutor: : JSCExecutor:
JSCExecutor::JSCExecutor(std: :shared_ptr<ExecutorDelegate> delegate,
std: :shared_ptr<MessageQueueThread> messageQueueThread,
const folly::dynamic& jscConfig) throw(JSException) :
m_delegate(delegate),
m_messageQueueThread(messageQueueThread),
m_nativeModules(delegate ? delegate->getModuleRegistry() : nullptr),
m_jscConfig(jscConfig) {
initOnJSVMThread();
installGlobalProxy(m_context, "nativeModuleProxy",
exceptionWrapMethod<&JSCExecutor::getNativeModule>());
}
Copy the code
The JSCExecutor constructor does one very important thing: it sets up a global proxy, nativeModuleProxy, in the JS Context, which ultimately points to the getNativeModule method of the JSCExecutor class. As for why nativeModuleProxy is important, I’ll leave it to you later, but let’s look at installGlobalProxy.
void installGlobalProxy(
JSGlobalContextRef ctx,
const char* name,
JSObjectGetPropertyCallback callback) {
JSClassDefinition proxyClassDefintion = kJSClassDefinitionEmpty;
proxyClassDefintion.attributes |= kJSClassAttributeNoAutomaticPrototype;
proxyClassDefintion.getProperty = callback;
const bool isCustomJSC = isCustomJSCPtr(ctx);
JSClassRef proxyClass = JSC_JSClassCreate(isCustomJSC, &proxyClassDefintion);
JSObjectRef proxyObj = JSC_JSObjectMake(ctx, proxyClass, nullptr);
JSC_JSClassRelease(isCustomJSC, proxyClass);
Object::getGlobalObject(ctx).setProperty(name, Value(ctx, proxyObj));
}
Copy the code
Combine the above two pieces of code to summarize:
nativeModuleProxy
In JS Context is a withJSObjectGetPropertyCallback
Property object;JSObjectGetPropertyCallback
A callback is triggered when an object property (Object.propertyName) is accessed in JS.nativeModuleProxy
The corresponding callback will eventually be calledJSCExecutor::getNativeModule
.
The initOnJSVMThread method is also called in the JSCExecutor constructor:
void JSCExecutor::initOnJSVMThread() {
installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
}
Copy the code
InitOnJSVMThread method has a notable point: hook JSCExecutor: in JS Context: nativeFlushQueueImmediate. In short, hooks, JS invokes the global. NativeFlushQueueImmediate (…). , the actual call Native JSCExecutor: : nativeFlushQueueImmediate method. In addition, the native Module registration information is converted to JSCNativeModules format and stored in the constructor.
JsToNativeBridge
Coordinates to NativeToJsBridge: : NativeToJsBridge – > JsToNativeBridge: : JsToNativeBridge:
JsToNativeBridge(std: :shared_ptr<ModuleRegistry> registry,
std: :shared_ptr<InstanceCallback> callback)
: m_registry(registry)
, m_callback(callback) {}
Copy the code
Down the call chain, the final Module registration information is passed into the JsToNativeBridge, which will be used by subsequent JS to Native calls. At this point, the communication mechanisms discussed in this article are almost complete during RN initialization. In summary, there are probably several points:
- Collect all modules exposed to JS (or generate a native Module registry)
- It’s set in the JS Context
nativeModuleProxy
As well asnativeFlushQueueImmediate
; - Initializes the associated classes, such as:
NativeToJsBridge
,JsToNativeBridge
As well asJSCExecutor
And so on.
Now it can be said that “everything is ready, only the east wind” — JS initiated the call to Native. Let’s continue down the call path.
JS NativeModules
We have this section in JS ^-^
// JS
import { NativeModules } from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party'.'4 Privet Drive, Surrey');
Copy the code
Going back to the previous example, it calls the addEvent: Location: method of CalendarManagermodule. Expand the above call and the final form looks like this:
NativeModules.CalendarManager.addEvent('Birthday Party'.'4 Privet Drive, Surrey')
Copy the code
NativeModules are defined in node_modules->react-native->Libraries->BatchedBridge->NativeModules.
let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
NativeModules = global.nativeModuleProxy;
}
Copy the code
NativeModuleProxy is mentioned above, by the Native injection JS with JSObjectGetPropertyCallback attributes of the object. . As a result, the JS NativeModules CalendarManager attribute values () is equivalent to call:
JSCExecutor::getNativeModule(NativeModules, `CalendarManager`)
Copy the code
NativeModules. ModuleName is equivalent to Native:
JSCExecutor::getNativeModule(NativeModules, `moduleName`)
Copy the code
This is why nativeModuleProxy is very important, all calls from JS to Native need it as an intermediate proxy. At this point, it’s time to cut into the Native environment.
ModuleRegistry::getConfig
Since you will need nativemodule information soon, get it ready. Locating the ModuleRegistry: : getConfig, through the above knowable stored in ModuleRegistry nativemodule information,
folly::Optional<ModuleConfig> ModuleRegistry::getConfig(const std: :string& name) {
auto it = modulesByName_.find(name);
NativeModule* module = modules_[it->second].get();
// string name, object constants, array methodNames (methodId is index), [array promiseMethodIds], [array syncMethodIds]
folly::dynamic config = folly::dynamic::array(name);
std: :vector<MethodDescriptor> methods = module->getMethods();
for (auto& descriptor : methods) {
methodNames.push_back(std::move(descriptor.name));
}
if(! methodNames.empty()) { config.push_back(std::move(methodNames));
}
return ModuleConfig({it->second, config});
}
Copy the code
The getConfig method finally assembles the Native Module information into an array in the following format: [Modulename, Module derived Constants, [methodNames], [promiseMethodIds], [syncMethodIds]] Here is the information for RCTWebSocketModule derived:
RCTModuleData in this process. The methods play a key role: ModuleRegistry: : getConfig – > RCTNativeModule: : getMethods – > RCTModuleData. The methods
// RCTModuleData.m
- (NSArray<id<RCTBridgeMethod>> *)methods
{
if(! _methods) {NSMutableArray<id<RCTBridgeMethod>> *moduleMethods = [NSMutableArray new];
unsigned int methodCount;
Class cls = _moduleClass;
while(cls && cls ! = [NSObject class] && cls ! = [NSProxy class]) {
Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
IMP imp = method_getImplementation(method);
NSArray *entries = ((NSArray* (*) (id, SEL))imp)(_moduleClass, selector);
id<RCTBridgeMethod> moduleMethod =
[[RCTModuleMethod alloc] initWithMethodSignature:entries[1]
JSMethodName:entries[0]
isSync:((NSNumber *)entries[2]).boolValue moduleClass:_moduleClass]; [moduleMethods addObject:moduleMethod]; }}}}return _methods;
}
Copy the code
If __rct_export__ is familiar, rctmoduleData.methods iterates through all methods prefixed with __rct_export__ and executes to export the interface exposed to JS.
JSCNativeModules::createModule
The book continues, JS to Native NativeModules.moduleName(JS)->JSCExecutor::getNativeModule->JSCNativeModules::getModule->JSCNativeModules::createModule Along the invocation chain to JSCNativeModules: : createModule:
folly::Optional<Object> JSCNativeModules::createModule(const std: :string& name, JSContextRef context) {
if(! m_genNativeModuleJS) {auto global = Object::getGlobalObject(context);
m_genNativeModuleJS = global.getProperty("__fbGenNativeModule").asObject();
m_genNativeModuleJS->makeProtected();
}
auto result = m_moduleRegistry->getConfig(name);
Value moduleInfo = m_genNativeModuleJS->callAsFunction({
Value::fromDynamic(context, result->config),
Value::makeNumber(context, result->index)
});
folly::Optional<Object> module(moduleInfo.asObject().getProperty("module").asObject());
return module;
}
Copy the code
In createModule approach, through ModuleRegistry: : getConfig (line 8) got to invoke native module information (including export constants, exposure of the interface, etc.). We also get an attribute in the JS Context named __fbGenNativeModule (line 4) that generates NativeModule information on the JS side. __fbGenNativeModule is defined in NativeModules
// export this method as a global so we can call it from native
global.__fbGenNativeModule = genModule;
Copy the code
NativeModules.genModule
NativeModules.moduleName(JS)->JSCExecutor::getNativeModule->JSCNativeModules::getModule->JSCNativeModules::createModule- >NativeModules. genModule(JS)
function genModule(config: ? ModuleConfig, moduleID: number): ?{name: string, module? :Object} {
const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
const module = {};
methods && methods.forEach((methodName, methodID) = > {
const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
module[methodName] = genMethod(moduleID, methodID, methodType);
});
Object.assign(module, constants);
return { name: moduleName, module };
}
Copy the code
GenModule iterates through the methods array and calls genMethod to generate methods on the JS side:
function genMethod(moduleID: number, methodID: number, type: MethodType) {
let fn = null;
fn = function(. args: Array
) {
args = args.slice(0, args.length - callbackCount);
BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);
};
fn.type = type;
return fn;
}
Copy the code
The function generated by genMethod is very simple and just enlists the call information to native. At this point, the chain of calls initiated by NativeModules. ModuleName finally ends and returns a JS object with methodName as the key and function as value generated by genMethod. NativeModules. ModuleName is equivalent to {methodName: fn}.
ModuleID and methodID in JS correspond to the array subscripts in the native Moudle registry.
Of course, things did not end, continue to expand NativeModules. ModuleName. MethodName (args). Through the above analysis, indicated that NativeModules. ModuleName. MethodName (args) is equivalent to: BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess)
MessageQueue.enqueueNativeCall
NativeModules.moduleName.methodName
->BatchedBridge.enqueueNativeCall
const MessageQueue = require('MessageQueue');
const BatchedBridge = new MessageQueue();
Copy the code
You can see that BatchedBridge is MessageQueue
enqueueNativeCall(moduleID: number, methodID: number, params: Array<any>, onFail:?Function.onSucc:?Function) {
this._queue[MODULE_IDS].push(moduleID);
this._queue[METHOD_IDS].push(methodID);
this._queue[PARAMS].push(params);
const now = new Date().getTime();
if (global.nativeFlushQueueImmediate &&
(now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
this._inCall === 0)) {
var queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now; global.nativeFlushQueueImmediate(queue); }}Copy the code
The enqueueNativeCall method calls the information (moduleID, methodID, Params, etc.), If the time since the last Flush queue exceeds 5ms(MIN_TIME_BETWEEN_FLUSHES_MS), flush queue immediately (for performance reasons). There are images for nativeFlushQueueImmediate? (in JSCExecutor constructor talked ^_^). Global nativeFlushQueueImmediate (queue) is equivalent to JSCExecutor: : nativeFlushQueueImmediate (queue)
JsToNativeBridge::callNativeModules
JSCExecutor: : nativeFlushQueueImmediate – > JSCExecutor: : flushQueueImmediate – > JsToNativeBridge: : callNativeModules along the chain, Locate the JsToNativeBridge: : callNativeModules.
void callNativeModules(
JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) override {
for (auto& call : parseMethodCalls(std::move(calls))) {
m_registry->callNativeMethod(call.moduleId, call.methodId, std::move(call.arguments), call.callId); }} 'callNativeModules' parse calls passed from JS one by onequeueEach call in. ```cppvoid ModuleRegistry::callNativeMethod(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) {
modules_[moduleId]->invoke(methodId, std::move(params), callId);
}
Copy the code
There are a few more details along the call chain that I won’t go into for space, mainly as follows:
RCTModuleMethod.invokeWithBridge
[_invocation invokeWithTarget:module];
summary
JS to Native call sequence diagram:
- NativeModules. ModuleName — This process is mainly to obtain information about native Modules (moduleID, methodID), and finally package as JS object ({methodName: fn});
- NativeModules. ModuleName. MethodName (params) — execution call.
That’s all for today. Next we will analyze Native to JS.