我们知道 RN 是一个跨语言跨线程的框架,所以要了解 RN 的通信机制,我们有两个问题需要解决:
- 如何跨线程?
- 如何跨语言?
因为 RN 的初版架构是基于 Android 与 IOS 搭建的,所以我们要先来聊聊这两个系统是如何处理跨线程通信(ITC)的
Android 的跨线程通信
最原始的 Android 跨线程通信(后文简写为 ITC)包含了三个概念:Looper
、MessageQueue
、Handler
MessageQueue 可以简单理解为一个代办任务清单,而 Looper 是一个循环机制,每次循环都会从 MessageQueue 中取出任务来执行
Handler 则是负责将代办任务添加进 MessageQueue 中
下面是一个简单的例子:
java
// 获取主线程的 Handler
Handler mainHandler = new Handler(Looper.getMainLooper());
// 模拟在背景线程中的操作
new Thread(new Runnable() {
@Override
public void run() {
// 给主线程发送一段可执行程序
mainHandler.post(new Runnable() {
@Override
public void run() {
System.out.println("Runnable running on: " +
Thread.currentThread().getName());
}
});
}
}).start();
在 Android 中的 RN 就是基于这三个概念封装了一套平台无关的跨线程通信方法
IOS 的跨线程通信
GCD
IOS 的 ITC 跟 Android 有比较大的差异,主要源于系统中的 GCD(Grand Central Dispatch) APIs
GCD 将跨线程的资源调度与任务分配抽象成了不同的队列(Queue)
默认情况下,IOS 系统会创建 1 个 Main queue 以及 5 个 Global queue
Main queue 是唯一一个与主线程绑定的队列,dispatch_get_main_queue()
方法会返回 Main queue
Global queue 有 5 个,分别代表了系统规划的 5 种 QoS(Quality of Service,代表任务的重要性,高优的任务有优先调度资源的权力),分别为:
userInteractive
:优先级最高,代表用户交互的事件,比如触摸事件,动画事件,滚动事件userInitiated
:优先级次高,代表用户发起且希望能够马上获得结果的事件,比如打开一篇文档,加载另一个页面default
:优先级中等,代表默认的事件,如果不知道当前任务要放哪个优先级,就放这个utility
:优先级次低,代表用户不会期望能立刻获得结果的事件,这类事件通常会有一个进度条来表示,比如加载批量文件background
:优先级最低,代表用户不会感知发生的事件,比如内容预加载、清理内存、数据同步
GCD 优雅的地方在于,除了 Main queue 绑定了主线程之外,其他的 Queue 都没有绑定特定线程
开发者只要把任务塞给相对应优先级的 Queue,系统就会帮其分配线程资源来执行任务,这个期间开发者是无感知的(不用分配线程,也不需要销毁资源)
除了上述系统创建的两种队列之外,开发者还可以根据自身需求创建队列,创建队列方法如下:
arduino
/**
* dispatch_queue_create 用于创建自定义队列
* label 一个字符串用于标识该队列,用于 debug
* attr 队列的属性,可以在这里设置队列的 QoS,以及设置队列为 Serial/Concurrent 模式
**/
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
系统创建的 Main queue 与 Global queue 都是 Serial 模式,Serial 模式下的队列一次只会在一个线程执行一个任务,保证队列中的任务会以 FIFO 的顺序执行
自定义的队列可以手动设置为 Concurrent 模式,改模式下队列一次可能会在多个线程中同步执行多个任务,好处就是能加快队列中任务处理速度,坏处就是无法保证该队列中任务执行完成的顺序
如果要将任务委托给 queue 的时候,可以使用 dispatch 方法:
objc
// 同步方式,会冻结当前 queue 直到 block 中的任务结束
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 异步方式,会直接执行当前 queue 后面的代码,如果执行 block 的 queue 需要返回结果,则再使用一次 dispatch_async 就好
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
NSThread
IOS 的 GCD 提供了一种从系统层面调度线程的方案,开发者无法感知也无法控制特定的线程
但是当我们从 RN 的视角看这个方案的时候,情况就有点尴尬了:RN 要求开发者至少能够控制 JS 线程的创建与销毁(这样才能保证 JS 代码始终有环境可以运行)
还好 IOS 提供了一种不用 GCD 控制线程的解决方案:NSThread
NSTread 简单来说就是完全受控于开发者的 IOS 线程,RN 在创建 NSThread 后做了四件事:
- 创建了一个永不停止的循环任务(RunLoop)确保线程不会闲置后被清理
- 用
RCTMessageThread
类包裹了这个 RunLoop,并对外提供了runOnQueue
与runOnQueueSync
方法来发送可执行代码给 RunLoop 执行(这俩方法底层调用了 IOS 系统提供的CFRunLoopPerformBlock
方法) - 使用上述方法,让 JSC 引擎在 RunLoop 中创建并完成初始化
- 使用上述方法,将打包好的 js 代码交给 JSC 执行
至此,我们获得了一个完全受我们控制的线程,并且完成了运行 js 代码的前置配置
而 runOnQueue
与 dispatch_async
则为其他线程与 NSThread 的通信提供了底层支持
跨语言通信
了解完 IOS 与 Android 是如何跨线程通信之后,我们来聊聊 RN 是如何跨不同语言来通信的
我们知道 IOS 与 Android 的应用程序分别是使用 Objc 与 Java 来编写的,并且他们通过运行 JS 引擎(由 C++ 编写)来执行 JS 代码
所以,我们可以得到以下四种通信方向(以函数调用为例):
Java -> C++ -> JS
:Android App 调用 JS 函数Objc -> C++ -> JS
:IOS App 调用 JS 函数JS -> C++ -> Java
:JS 调用 Android 的 Native moduleJS -> C++ -> Objc
:JS 调用 IOS 的 Native module
再总结一下,我们想要解决 RN 不同语言之间通信,只需要解决以下三个问题:
Java <-> C++
:Java 与 C++ 函数如何互相调用Objc <-> C++
:Objc 与 C++ 函数如何互相调用JS <-> C++
:JS 与 C++ 函数如何互相调用
Java <-> C++
关于 Java 与 C++ 之间的通信,Java 社区已经有了解决方案:JNI (Java Native Interface)
JNI 是由 JVM(Java 虚拟机)提供的一系列 C 接口 API 组成的机制
C 系列语言(包括 C++)只需要通过调用这些 API(比如 FindClass
, CallVoidMethod
等)就可以调用 Java 的方法
反之,Java 也可以调用一些 C++ 侧通过 JNI 注册的一些方法
JNI 甚至允许 Java 与 C++ 同时持有对同一块内存的引用
所以这个问题被完美解决了
Objc <-> C++
关于 Objc 与 C++ 之间的通信也有现成的解决方案:Objective-C++
Objective-C++(.mm
文件)允许在同一个文件中混写 C++ 与 Objc 代码(包括类型与方法等)也能调用一些 C API(比如上述 dispatch_queue_t
)
当然,持有同一份引用以及互相调用方法也不在话下
这一切要归功于 Objc 的编译器,它支持将 .mm
文件中的 C++ 与 Objc 编译为同一份目标代码
所以这个问题也被完美解决啦
JS <-> C++
最后,我们来聊聊最关键的一环,也就是 Js 与 C++ 之间的通信
我们以函数调用为例,来分别聊聊这两者是如何互相调用对方的函数的
一般来说,要跨语言调用对方函数,有两个问题需要克服:
- 调用方式:要么取得对方函数的引用,要么将意图封装成某种 DSL(Domain specific language) 并让对方解析
- 参数类型转换:将调用方的调用参数转换成被调用方的类型以供被调用方执行
首先是调用方式:
我们知道 JS 是运行在由 C++ 编写而成的引擎中的(在 RN 初版架构的例子中,这个引擎是 JSCore)
JSCore 除了提供了一个运行 JS 的环境之外,它还对外暴露了一系列的 C API 来让开发者控制它的部份能力
RN 就是使用了其中的 JSGlobalContextCreateInGroup 来创建了一个 context
context 代表了 JS 执行的上下文,当 JSCore 完成了一个 context 的创建,我们可以认为他完成了运行 JS 代码前的准备工作
除此之外,RN 还调用了 JSContextGetGlobalObject 来通过 context 获取 JS Global 对象的引用
有了 Global 的引用,C++ 侧就有了在任意时候读取 JS 放在 Global 对象上的属性/方法的能力
其次是类型转换:
类型转换分成两个情况:
- JS 调用 C++ 方法时,我们需要将 JS 的参数类型转换成 C++ 的类型
- 如果 C++ 调用 JS 方法时,我们则需要将 C++ 的类型转换成 JS 类型
cpp
// in JSCExecutor.cpp
// JS 调用 C++ 方法
void JSCExecutor::flushQueueImmediate(Value&& queue) {
// 将 JS 类型的 queue 通过 JSON 转换成 C++ 的 folly::dynamic 类型
auto queueStr = queue.toJSONString();
m_delegate->callNativeModules(*this, folly::parseJson(queueStr), false);
}
// C++ 调用 JS 方法
void JSCExecutor::callFunction(
const std::string& moduleId,
const std::string& methodId,
const folly::dynamic& arguments) {
// ... 略过部份代码
// 将 C++ 的 string,folly::dynamic 类型转换成 JS 的类型
m_callFunctionReturnFlushedQueueJS->callAsFunction(
{Value(m_context, String::createExpectingAscii(m_context, moduleId)),
Value(m_context, String::createExpectingAscii(m_context, methodId)),
Value::fromDynamic(m_context, std::move(arguments))});
// ... 略过部份代码
}
让我们先看看 JS 调用 C++ 方法的部份,可以看到 RN 选择了先将 JS 的类型转换成了 JSON,再转换成 C++ 的类型,那么问题来了,为什么要这么麻烦呢?为什么不直接转换呢?
个人认为有两个因素:
- RN 在真机上运行的时候,是否可以直接转换类型呢?其实答案是可以的,但是会非常局限:JSCore 提供的公开 API 中确实有类似于 JSValueToBoolean,JSValueToNumber 能将原始类型转换成对应 C++ 类型的方法,但是面对 String,Object 类型的时候就有点力不从心了
- 在聊 JS 线程的时候,我们有聊到 RN 支持在 chrome 上调试 js 代码,在这个场景下 JS 跟 Native 的沟通主要依赖 ws 的连接,在这种情况下,我们只能把数据通过 JSON 进行传送,这个是无法绕过的,再进一步考虑到 dev/prod 环境下的表现一致性,JSON 是权衡之下的较优解
当然,RN 也在这个背景下做了些优化,queue 中其实并不是一个方法调用,而是类似一个先进先出的栈,里面存放了多个调用,最大程度的减少 JSON 带来的性能损耗
接下来我们来看看 C++ 调用 JS 方法的部份,这个部份主要用了两个类型转换的方法:String::createExpectingAscii
, Value::fromDynamic
,我们来看看他们是怎么实现的:
cpp
// in Value.h
static String createExpectingAscii(JSContextRef context, const char* ascii, size_t len) {
#if WITH_FBJSCEXTENSIONS
return String(context, JSC_JSStringCreateWithUTF8CStringExpectAscii(context, ascii, len), true);
#else
return String(context, JSC_JSStringCreateWithUTF8CString(context, ascii), true);
#endif
}
// in Value.cpp
Value Value::fromDynamic(JSContextRef ctx, const folly::dynamic& value) {
#if USE_FAST_FOLLY_DYNAMIC_CONVERSION
JSDeferredGCRef deferGC = JSDeferGarbageCollection(ctx);
JSLock(ctx);
JSValueRef jsVal = Value::fromDynamicInner(ctx, value);
JSUnlock(ctx);
JSResumeGarbageCollection(ctx, deferGC);
return Value(ctx, jsVal);
#else
auto json = folly::toJson(value);
return fromJSON(String(ctx, json.c_str()));
#endif
}
非常有意思的是,这两个方法都用了两套实现
还记得我们之前聊 JS thread 的时候提到的 android-jsc 吗?WITH_FBJSCEXTENSIONS
与 USE_FAST_FOLLY_DYNAMIC_CONVERSION
中的实现是专门为了它写的,不像在 ios 中的 jsc,RN 团队可以完全控制 Android 的 jsc,所以也可以用一些比较 "野" 的写法
先来看看 createExpectingAscii
这个方法的两个实现,区别在于在 Android 中用了 JSStringCreateWithUTF8CStringExpectAscii
,在 IOS 中用了 JSStringCreateWithUTF8CString
这两个方法的区别在于,前者保证传进去的参数是 ascii 编码的字符,所以当 JSC 在转换类型的时候,只需要考虑 7-bit 的 ascii 编码就好,速度会比需要考虑 UTF-8 的后者快,而 createExpectingAscii
方法传入的 ascii 参数是类似于 AppRegistry
之类的字符,且完全受 RN 所控制,所以能保证编码类型;至于为什么 IOS 不能用前者,自然是 Apple 没有公开这个 API 啦~
再来看看 fromDynamic
这个方法,Android 中的实现(if 的部份)做了三件事:
- 暂停了 JSC 的垃圾回收 GC:这个目的是为了防止转换类型的中间产物被 GC 回收导致不可控的错误
- 用
JSLock
锁住了当前的 context(这里有个小知识点,由于 JSC 是单线程的,为了防止多个线程同时调用其 API,JSC 会把大部分的 API 调用前后锁住,以免发生资源争夺的情况,由于fromDynamicInner
内部会调用大量的 JSC API,所以这里提前锁上了避免大量额的加锁解锁资源损耗) - 使用
fromDynamicInner
转换类型:
cpp
JSValueRef Value::fromDynamicInner(JSContextRef ctx, const folly::dynamic& obj) {
switch (obj.type()) {
// For primitive types (and strings), just create and return an equivalent JSValue
case folly::dynamic::Type::NULLT:
return JSC_JSValueMakeNull(ctx);
case folly::dynamic::Type::BOOL:
return JSC_JSValueMakeBoolean(ctx, obj.getBool());
case folly::dynamic::Type::DOUBLE:
return JSC_JSValueMakeNumber(ctx, obj.getDouble());
case folly::dynamic::Type::INT64:
return JSC_JSValueMakeNumber(ctx, obj.asDouble());
case folly::dynamic::Type::STRING:
return JSC_JSValueMakeString(ctx, String(ctx, obj.getString().c_str()));
case folly::dynamic::Type::ARRAY: {
// Collect JSValue for every element in the array
JSValueRef vals[obj.size()];
for (size_t i = 0; i < obj.size(); ++i) {
vals[i] = fromDynamicInner(ctx, obj[i]);
}
// Create a JSArray with the values
JSValueRef arr = JSC_JSObjectMakeArray(ctx, obj.size(), vals, nullptr);
return arr;
}
case folly::dynamic::Type::OBJECT: {
// Create an empty object
JSObjectRef jsObj = JSC_JSObjectMake(ctx, nullptr, nullptr);
// Create a JSValue for each of the object's children and set them in the object
for (auto it = obj.items().begin(); it != obj.items().end(); ++it) {
JSC_JSObjectSetProperty(
ctx,
jsObj,
String(ctx, it->first.asString().c_str()),
fromDynamicInner(ctx, it->second),
kJSPropertyAttributeNone,
nullptr);
}
return jsObj;
}
default:
// Assert not reached
LOG(FATAL) << "Trying to convert a folly object of unsupported type.";
return JSC_JSValueMakeNull(ctx);
}
}
至于为什么 IOS 用的是 JSON 转换呢~当然是因为阻止 GC 以及 JSLock 的 API 在 IOS 中并不是公开的 API 啦~
至此!我们讲完了 RN 跨语言通信的一些基础机制与准备,接下来我们看看 Bridge 是如何利用这些构建连接的桥梁
Bridge in Native
接下来我们来看看 bridge 做了什么,以及它是如何与其他模块交互的
首先我们需要介绍三个重要角色:
-
Instance
:代码实现在Instance.cpp
。- 可以看成 RN app 的大管家,它接受原生程序的委托,主要负责管理 Native module 注册、初始化 Bridge、加载 JS bundle 等事务,同时它也是原生程序调用 JS 方法的唯一入口
-
bridge
:代码实现在NativeToJsBridge.cpp
。- bridge 由两个部分组成:
NativeToJsBridge
与JsToNativeBridge
。 - 前者主要把
Instance
的委托的加载 JS bundle 以及调用 JS 方法的任务传递给JSCExecutor
(此外它还肩负着委托JSCExecutor
启动 JS 引擎的重责大任) - 后者则是负责处理从 JS 传来的 Native module 调用请求
- bridge 由两个部分组成:
-
JSCExecutor
:代码实现在JSCExecutor.cpp
。- 接受
NativeToJsBridge
的请求启动 JS 引擎(用 JSGlobalContextCreateInGroup 创建 JS context) - 负责消费在 JSC 中加载 JS bundle 的任务
- 往 JS global 对象上挂方法/读取 JS global 对象上的方法,为相互调用做准备
- 负责将从 bridge 传来的调用 JS 方法的请求通过 global 对象传递到 JS 侧
- 负责将从 JS 侧传来的调用 Native module 的请求传递给 bridge
- 接受
三个角色的关系如图所示:

从图中可以看到,RN 在代码结构上大致分了三层,Instance
之下是原生代码写的 APP 的壳(包括但不限于 APP 启动的原生代码,Native modules 的原生代码实现等)这个部份是平台区分的
从 Instance
开始到 Bridge
再到 JSCExecutor
,这三个部份都是通过 C++ 语言实现,这个部份提供了跨平台的实现(同一套代码运行在不同的原生平台上)
JSCExecutor
之上,就是 JS 的领域了,JS 跟 JSCExecutor
主要是通过 global 对象进行通信,上述类型转换的代码也大多被封装到 JSCExecutor
的实现中
从原生代码或者 Native module 的角度来看,调用 JS 方法只需要调用 Instance
提供的 callJSFunction
后面的流程他们不需要感知
从 JS 的角度来看,调用原生方法则只需要调用 global 对象上的 nativeFlushQueueImmediate
方法,后面的流程也无需感知了
其实到这里 RN 的通信机制也讲的差不多了,后面我写了一点关于 JSCExecutor 跟 JS 互相调用的核心代码解析,有兴趣的可以看看
JS <-> C++ 互相调用核心代码解析
- 首先是 C++ 调用 JS 函数:
我们来看看 JS 侧,为了能够让 C++ 顺利调用我们的方法,我们需要往 Global 上挂点东西:
typescript
// in MessageQueue.js
class MessageQueue {
// ...
callFunctionReturnFlushedQueue(module: string, method: string, args: any[]) {
const moduleMethods = this.getCallableModule(module);
moduleMethods[method].apply(moduleMethods, args);
return this.flushedQueue();
}
}
// in BatchedBridge.js
const BatchedBridge = new MessageQueue();
Object.defineProperty(global, '__fbBatchedBridge', {
configurable: true,
value: BatchedBridge,
});
这两个文件在 JS 侧提供了给 C++ 调用特定函数的途径(为了方便讲解我简化了调用,但是方法名不变)
首先是 MessageQueue
的 callFunctionReturnFlushedQueue
方法,它会根据 module
去查找是否有可被调用的 module,如果有,则将其初始化后返回,最后使用 apply
调用其方法(flushedQueue
我们会在后续的布局与绘制篇中讲解)
接着是 BatchedBridge
,它将上述的 MessageQueue
作为 __fbBatchedBridge
的值挂上了 globa 对象(configurable
值设为 true 是为了方便框架测试)
让我们再看回 C++ 侧,有了 JS 的 global 之后,我们可以通过以下方式调用 JS 方法:
cpp
// 根据 context 获得 global 对象引用
auto global = Object::getGlobalObject(m_context);
// 从 global 对象中获取 __fbBatchedBridge 属性得到 MessageQueue 的实例
auto batchedBridgeValue = global.getProperty("__fbBatchedBridge");
auto batchedBridge = batchedBridgeValue.asObject();
// 从 MessageQueue 实例中取得 callFunctionReturnFlushedQueue 方法的引用
m_callFunctionReturnFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnFlushedQueue").asObject();
// 调用 JSC 的 callAsFunction api,传入对应 moduleId,methodId,arguments 以调用 callFunctionReturnFlushedQueue 方法
m_callFunctionReturnFlushedQueueJS->callAsFunction({
Value(m_context, String::createExpectingAscii(m_context, moduleId)),
Value(m_context, String::createExpectingAscii(m_context, methodId)),
Value::fromDynamic(m_context, std::move(arguments))
})
值得注意的是,由于 C++ 与 JS 的类型是不同的,所以在 callAsFunction
中,C++ 需要先进行类型转换
其中 moduleId
与 methodId
都是固定的 String 类型,可以直接转换,但是 arguments
类型不固定,所以 RN 在 Value::fromDynamic
方法中采用了最简单粗暴的方法:序列化与反序列化
cpp
Value Value::fromDynamic(JSContextRef ctx, const folly::dynamic& value) {
auto json = folly::toJson(value);
return fromJSON(String(ctx, json.c_str()));
}
至此我们完成了所有 C++ -> JS 的函数调用准备工作
- 接下来我们来看看 JS 是如何调用 C++ 函数的:
与 C++ 调用 JS 函数类似,只不过这次变成了 C++ 往 Object 上挂东西:
cpp
// in JSCExecutor.cpp
/**
* installNativeHook 主要用于将特定 C++ 方法的钩子安到 global 对象中
* template 是 C++ 的语法,他会接收符合对应返回值(JSValueRef)与参数(size_t, const JSValueRef[])的 JSCExecutor 实例方法并将其放到 installNativeHook 中(在这个例子里,他会将其放进 exceptionWrapMethod 这个方法的 template 中)
**/
template <JSValueRef (JSCExecutor::*method)(size_t, const JSValueRef[])>
void JSCExecutor::installNativeHook(const char* name) {
installGlobalFunction(m_context, name, exceptionWrapMethod<method>());
}
/**
* exceptionWrapMethod 主要职责是调用传入的 method,并且处理其的 Error
**/
template <JSValueRef (
JSCExecutor::*method)(JSObjectRef object, JSStringRef propertyName)>
inline JSObjectGetPropertyCallback exceptionWrapMethod() {
struct funcWrapper {
static JSValueRef call(
JSContextRef ctx,
JSObjectRef object,
JSStringRef propertyName,
JSValueRef* exception) {
try {
auto executor = Object::getGlobalObject(ctx).getPrivate<JSCExecutor>();
if (executor &&
executor->getJavaScriptContext()) { // Executor not invalidated
// 核心代码,执行对应的 C++ 方法
return (executor->*method)(object, propertyName);
}
} catch (...) {
*exception = translatePendingCppExceptionToJSError(ctx, object);
}
return Value::makeUndefined(ctx);
}
};
// 返回对于 call 方法的引用,这个 call 方法会执行传入的 method 方法并返回其返回值
return &funcWrapper::call;
}
/**
* installGlobalFunction 主要职责是将上述的 call 方法放进 JS 的 global 对象中,以便后续让 JS 调用
**/
void installGlobalFunction(
JSGlobalContextRef ctx,
const char* name,
JSObjectCallAsFunctionCallback callback) {
String jsName(ctx, name);
JSObjectRef functionObj = JSC_JSObjectMakeFunctionWithCallback(
ctx, jsName, callback);
Object::getGlobalObject(ctx).setProperty(jsName, Value(ctx, functionObj));
}
// 最后 C++ 调用上面的所有方法把 nativeFlushQueueImmediate 方法放进了 global 对象中
installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
从上述代码中,我们能看到 C++ 侧成功的将 nativeFlushQueueImmediate
方法放进了 global 对象中
接下来我们来看看 JS 是如何使用这个方法的:
js
// in MessageQueue.js
class MessageQueue {
// ...
// JS 侧调用 C++ 方法的入口,为了简化我删除了部份代码(包括开发模式、性能打点、注册回调函数等等),有兴趣的可以自己去看看:https://github.com/facebook/react-native/blob/v0.57.0/Libraries/BatchedBridge/MessageQueue.js#L168
enqueueNativeCall(
moduleID: number,
methodID: number,
params: any[],
){
// 将当前要调用的方法放入队列中
this._queue[MODULE_IDS].push(moduleID);
this._queue[METHOD_IDS].push(methodID);
this._queue[PARAMS].push(params);
const now = new Date().getTime();
// 如果符合清空队列的条件,则一股脑将当前队列全部发给 C++
if (global.nativeFlushQueueImmediate &&
now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
) {
const queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
// 调用 global 中 C++ 的方法
global.nativeFlushQueueImmediate(queue);
}
}
}
JS 传入 nativeFlushQueueImmediate
方法的是一个队列,它的类型是:_queue: [number[], number[], any[], number];
- 第一个元素是一个数字数组,代表需要调用的 native module 的 id
- 第二个元素也是一个数字数组,代表需要调用的方法的 id
- 第三个元素也是数组,代表调用方法的参数
- 最后一个元素是一个数字,唯一标识了当前的调用 id
- 如果我们需要从 queue 中取出方法调用,我们只需要对前三个元素取出同一下标的元素即可
scss
// 举个例子
module = _queue[0][0][0]
method = _queue[0][1][0]
args = _queue[0][2][0]
module[method](...args)
最后,我们来看看 C++ 是如何处理这个 queue 以及处理 JS -> C++ 的类型转换的:
cpp
// in JSCExecutor.cpp
JSValueRef JSCExecutor::nativeFlushQueueImmediate(
size_t argumentCount,
const JSValueRef arguments[]) {
Value queue = Value(m_context, arguments[0])
// 序列化 queue
auto queueStr = queue.toJSONString();
// 反序列化 queue 得到 folly:dynamic
m_delegate->callNativeModules(*this, folly::parseJson(queueStr), false);
return Value::makeUndefined(m_context);
}
可以看到,RN 使用了序列化-反序列化 的操作将 JS 的 Value
类型转换成了可以在 C++ 中使用的 folly:dynamic
类型
这个方法无疑简单粗暴的解决了类型转换的问题,但同时也增加了 JS 与 C++ 方法互相调用的性能开销,考虑到这条链路的触发频率,这个部份有较大概率会成为 RN 的性能瓶颈,所以才有了后来用 JSI 取代序列化-反序列化的新架构(这个我们后面聊)