前言
开始之前,为啥有这篇文章呢,这是一个自问自答的问题。
我的职业生涯几乎一直在和 React Native(RN) 打交道。故事开始于读研期间,我做为前端开发和教研室两个后端师兄一起为导师做一个 C 端的项目,受到 React 的生态和 RN 的 Slogn 「write once run anywhere」的感召,我使用了基于 Expo 的 RN 技术栈来完成了 App 项目的开发与上线。
而找实习时也凭借了 PC 和 App 的 React 技术栈经验得到了前司鹅厂的 offer,实习期间使用了 Hippy-vue(公司的类 RN 框架,JS 侧使用 Vue 框架)实现了王者营地的战绩详情页。回想起那段实习的日子也是额外的充实,大部分工作时间投入在了 Coding 上,披星戴月下班回家在夜深没人的路上唱着自己喜欢的歌,觉得自己有无限可能。
在正式进入工作后,我的第一份工作中大量使用了 Hippy-react(🐧 的类 RN 框架,JS 侧使用 React)来构建王者营地的页面。到现在我的第二份工作中也使用了 MRN(美团基于 RN 改造的框架,API 兼容)。
可见我的职业生涯贯穿了 RN 这个框架,在大量应用实践后也明白了 RN 桥架构的优势和局限,明白了 RN 适合用于什么场景。但是,我还想更进一步探索 RN 的原理,既是满足自己的好奇心,也是给自己的职业生涯的一个回应。
桥架构通信的本质
虽然现在 RN 已经重构并演进到基于 JSI 的新架构了,但出于学习的目的,我还是从经典的桥架构出发,对 RN 进行最小还原。在搞清楚桥架构的原理后,再进入到新架构的深入学习中。
我们知道,RN 提供了用 React 写 JS 代码渲染 Native 应用的能力,我们前端开发写 RN 应用时最常打交道的就是 JS 代码,而最终运行在 Native 的产物其实也是 JS 代码。在 RN 中 JS 显然不是在端侧的 WebView 运行的,而是在嵌入 JS 引擎中引擎运行的,故而 JS 和端侧通信不是和 WebView 打交道而是需要和 Native 打交道。那么关键问题来了,JS 是怎么和 Native 通信的呢?下面我们以 RN 获取屏幕/视窗尺寸为例,进行说明。
typescript
// JS 侧从 Native 提供的模块获取数据
// 调用关系: JS -> Native
const windowDimensions = Dimensions.get('window');
const screenDimensions = Dimensions.get('screen');
const App = () => {
const [dimensions, setDimensions] = useState({
window: windowDimensions,
screen: screenDimensions,
});
useEffect(() => {
const subscription = Dimensions.addEventListener(
'change',
({window, screen}) => {
// Native 尺寸变化通知 JS 侧
// 调用关系:Native -> JS
setDimensions({window, screen});
},
);
return () => subscription?.remove();
});
// ...
}
从代码可以看见,JS 和 Native 的数据流向是双向的,JS 既可以调用 Native 模块的方法获取数据,Native 也可以主动推送数据给 JS。刚刚我们有提到,RN 中的 JS 的运行环境是嵌入 JS 引擎,具体的在桥架构版本中的 JS 引擎是 JavaScriptCore (JSC)。Native 用 JSC 加载了 JS 代码,再通过 C++ 桥这个中间层进行双向通信,这套核心代码是用 C++ 编写的,C++ 是一个高性能跨平台语言,用 C++ 来处理和 JSC 的交互以及进行桥实现来在 iOS 和 Android 双端复用是再适合不过了。
稍微展开讲讲,RN 选择 C++ 实现 Bridge 层的原因。首先,C++ 作为编译型语言具有接近原生的性能,这对于频繁的跨语言通信至关重要 ------ 每次 JS 调用 Native 都涉及数据序列化、消息队列管理等操作,C++ 的高性能可以最小化这些开销。其次,C++ 可以直接调用 JSC 的 C/C++ API,无需额外的语言绑定层。最后,一套 C++ 代码可以同时编译到 iOS 和 Android,确保两端桥通信的行为完全一致,避免平台差异带来的 bug。
回到这节的主题,我们关注的是桥通信,先贴下经典的桥架构图如下所示,大家可能已经在其他地方见到过很多次这张图了。我也会继续使用这张图来说明桥架构通信的本质是什么。

刚刚有提到 JS 和 Native 的通信是双向的,我们先看看 JS 是如何调用 Native 的,在这里我们其实可以和浏览器进行类比。不同于在浏览器中运行的 JS 其宿主是浏览器,而 RN JS 的运行宿主是嵌入在手机 OS 的 JSC 引擎。就如浏览器给 JS 环境提供了 window 对象及其方法供 JS 调用,嵌入 JS 引擎也是如此,为 JS 环境 注入了 global 对象及其方法供 JS 调用,而其中的方法就包含了 global.nativeCallSyncHook 这个关键方法(简称 callSync)。callSync 的入参包含了 JS 需要调用 Native 侧具体什么方法的信息,而每次 JS 侧调用 callSync 就可以触发 C++ 侧提前为该方法注册的回调函数,可以理解为 JS 调用该方法时就会把入参输入经过桥给 C++ 的对应函数并执行,而这一切是通过 JSC 的 API 做到的。再以之前的用 Native 模块获取尺寸的代码为例,JS 侧调用 Dimensions.get('window') 时最终会调用 callSync 方法,入参包含了模块信息为 Dimensions、方法为 get、参数为 'window',到 C++ 回调后再交由 C++ 转发给具体模块的实现进行处理。
再看看 Native 是如何通知 JS 数据更新的,还是以之前的代码为例: JS 监听了 Dimension 模块的 change 事件,Native 是如何触发该事件的呢。JS 在初始化时在 global 对象上初始化了供 Native 调用的方法,即 global.__fbBatchedBridge.callFunctionReturnFlushedQueue 方法,简称 callFn。在 Native 尺寸发生变化时,C++ 环境通过获取 JS 方法的句柄并调用,这样 JS 环境的 callFn 方法就会被调用,再通过 JS 侧的提前注册好的事件监听触发了 Dimension 模块的 change 事件。C++ 调用 JS 方法,也是通过 JSC 的 API 做到的。
由此我们可以发现,C++ 环境创建并管理了 JS 环境,既可以为 JS 环境注入全局变量和方法供 JS 调用,也可以主动调用 JS 环境中的方法,C++ 环境对 JS 上下文有着极高的控制权。这也是桥通信的奥秘所在。
正式开始前:任务概览
本项目的桥架构实现参考 react-native@v0.57.8 版本,该版本只包含 Bridge 代码不包含 JSI 代码,便于学习和参考。
在了解完跨语言间是如何相互调用实现通信之后,我们接下来就来看看完整实现 JS ↔ Native 双向通信需要做些什么。
首先,需要在 C++ 中管理 JS 上下文,这是我们出发的基础中的基础,有了 JS 可运行的环境后,再在 JS 环境中实现消息的发送和接收,实现的载体即是消息队列,然后再完成模块系统,实现一个示例模块的方法调用和消息接收。经历这一连串的路程,我们便实现了刚开始那段代码中 JS ↔ Native 双向调用的能力。
为了轻装上阵,初始阶段只在 macOS 系统中运行该项目。一步一步来,把 macOS 跑通后迁移到 iOS 并不复杂,而支持 Android 端还有一些适配工作需要做,不过受篇幅限制这些工作并不在本阶段完成。
C++ 环境集成 JS 引擎
在一切开始之前,我们需要搭建在 macOS 中运行 JS 的环境,除此之外,在为 JS 环境注入日志函数,供 JS 输出日志便于调试,这便是第一步需要做的。值得一提的是,JS 的核心规范是 ECMAScript ,这意味着 console 对象并不是 JS 语言本身的一部分,而是由宿主环境(如 Chrome, Node)提供的,单纯的 JSC 引擎并不包含 console 对象。
好的,现在我们开始用 C++ 以及 JavaScriptCore 来管理 JS 上下文。首先,我们把 C++ 的头文件定义好,我摘取了其中两个重要成员变量和两个重要方法如下。C++ 的 .h 文件用于声明接口,其实和 TypeScript 的 .d.ts 文件有着相似的作用。
c++
// JSCExecutor.h
class JSCExecutor {
private:
/**
* JSXXX 都是 JavaScriptCore 的 API,一些是类型(如
* JSObjectRef)一些是方法(如 JSGlobalContextCreate) 通过使用
* JSXXX,可以用 C++ 调用/管理 JS 引擎,可以理解为跨语言的桥梁和安全句柄
* JavaScriptCore API Reference:
* https://developer.apple.com/documentation/javascriptcore
*/
// JavaScript 运行环境(全局上下文)
JSGlobalContextRef m_context;
// JS 运行环境的全局对象 global
JSObjectRef m_globalObject;
public:
/**
* 加载并执行 JavaScript 应用代码
* @param script JavaScript 代码内容
* @param sourceURL 代码来源 URL(用于调试)
*/
void loadApplicationScript(const std::string &script,
const std::string &sourceURL);
/**
* 向 JavaScript 环境注入全局函数
* @param name 函数名称
* @param callback Native 回调函数
*/
void installGlobalFunction(const std::string &name,
JSObjectCallAsFunctionCallback callback);
};
有了 JSCExecutor.h 接着我们需要实现 JSCExecutor.cpp。我们还是关注关键变量的初始化和关键方法的实现,先对 JS 运行上下文和 global 对象进行初始化,代码如下。初始化都是通过了 JavaScriptCore API 来完成的。PS:Apple 平台(如 iOS、macOS)的系统内置了 JSC,引用/链接 JSC 库相对于 Android 平台更为便捷,这也是用 JSC 作为 RN JS 引擎的一个优势,即 iOS 只需引用系统内置的 JSC 引擎即可,对包大小友好。
C++
// JSCExecutor.cpp
JSCExecutor::JSCExecutor() : m_context(nullptr), m_globalObject(nullptr) {
// 调用 JSC API 初始化 JS 运行上下文
m_context = JSGlobalContextCreate(nullptr);
if (!m_context) {
throw std::runtime_error("Failed to create JavaScript context");
}
// 获取 global 全局对象
m_globalObject = JSContextGetGlobalObject(m_context);
}
然后再来实现加载 JS 脚本和向 JS 环境注入方法,代码如下。加载 JS 脚本的关键在于用 JSEvaluateScript 在 JS 上下文中加载 JS 脚本的字符串,向 JS 环境注入方法关键在于用 JSObjectSetProperty 在 JS 上下文的全局变量上设置属性。而这些都是借助 JSC API 实现的。
c++
// JSCExecutor.cpp
void JSCExecutor::loadApplicationScript(const std::string &script,
const std::string &sourceURL) {
// 将 C++ 的字符串转换成 JSC 可识别的字符串类型
JSStringRef scriptStr = JSStringCreateWithUTF8CString(script.c_str());
JSStringRef sourceURLStr =
sourceURL.empty() ? nullptr
: JSStringCreateWithUTF8CString(sourceURL.c_str());
JSValueRef exception = nullptr;
// 核心调用:执行 JS 脚本
JSValueRef result = JSEvaluateScript(m_context, scriptStr, nullptr,
sourceURLStr, 0, &exception);
if (exception) {
handleJSException(exception);
}
// 释放 JSC 字符串占用的内存
JSStringRelease(scriptStr);
if (sourceURLStr) JSStringRelease(sourceURLStr);
}
void JSCExecutor::installGlobalFunction(
const std::string &name,
JSObjectCallAsFunctionCallback callback) {
JSStringRef funcName = JSStringCreateWithUTF8CString(name.c_str());
// 创建一个 JS 函数对象,并指定它的 C++ 回调函数实现
JSObjectRef func =
JSObjectMakeFunctionWithCallback(m_context, funcName, callback);
// 将刚创建的函数对象挂载到全局对象 global 上
JSObjectSetProperty(m_context, m_globalObject, funcName, func,
kJSPropertyAttributeNone, nullptr);
JSStringRelease(funcName);
}
然后在 JSCExecutor 构造器中调用 installGlobalFunction 给 JS 引擎注入 log 方法,供 JS 脚本调用端上方法进行输出日志,便于调试。
c++
// JSCExecutor.cpp
JSCExecutor::JSCExecutor() : m_context(nullptr), m_globalObject(nullptr) {
// ...
installGlobalFunction(
"nativeLoggingHook",
[](JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
size_t argumentCount, const JSValueRef arguments[],
JSValueRef *exception) -> JSValueRef {
// 获取 JSCExecutor 实例
auto *executor = JSCExecutor::getCurrentInstance();
if (argumentCount >= 2) {
// JS 类型转 C++ 类型
std::string levelStr = jsValueToString(level);
std::string messageStr = jsValueToString(message);
// 使用 C++ 标准输出流
std::cout << "[" << levelStr << "] " << messageStr << std::endl;
} else {
std::cout << "[Bridge] Warning: nativeLoggingHook called with "
"insufficient arguments"
<< std::endl;
}
return JSValueMakeUndefined(ctx);
});
}
实现了 JSCExecutor.cpp 后,就可以新建一个测试文件 test.cpp 来试试加载并运行 JS 脚本了。经过一番编译运行,可以发现运行测试程序成功输出了 JS 调用并打印的内容,这样就测试成功了。
C++
// test.cpp
int main() {
JSCExecutor executor;
// C++ 原始字符串字面量语法,支持多行编写字符串
std::string bridgeTestScript = R"(
if (typeof global.nativeLoggingHook === 'function') {
global.nativeLoggingHook('INFO', 'This is a test log from JavaScript');
global.nativeLoggingHook('DEBUG', 'Bridge logging is working!');
}
)";
executor.loadApplicationScript(bridgeTestScript, "bridge_test.js");
return 0;
}
总结一下,这节我们构建了一个可以加载并管理 JS 上下文的 C++ 程序 JSCExecutor。有了这一步我们才有了 JS ↔ Native 双向通信的可能。
JS 侧桥核心方法实现
有了 JS 可运行的环境后,接下来进入到 JS 侧桥的实现,JS 侧桥的本质是一个消息队列,本节的任务是 JS 侧通过消息队列完成和 Native 的通信。
Why Messagequeue(MQ)? 主要原因有两:异步通信和优化性能。
- 异步通信:JS 引擎和 Native 代码运行在不同线程上,由于线程隔离,JS 无法直接调用 Native 的函数,必须通过某种跨线程通信机制,消息队列是实现这种跨线程异步通信的经典方案
- 优化性能:队列包含了一批消息信息,有了 MQ 后 JS 可以一次批传输多个消息给 Native,相比于每个消息单个传输给 Native 这可以减少跨线程通信的开销。
接下来,来看看 MessageQueue.js 的接口设计,我摘取了关键成员变量和方法,代码如下。PS:文章开头代码中 global.nativeCallSyncHook 是同步方法,不经过 MQ 调用,MQ 只承载异步方法的调用。
typescript
class MessageQueue {
// [moduleIDs, methodIDs, params, callbackIDs]
private _queue: [number[], number[], number[], number[]] = [[], [], [], []];
// 回调 id,递增记录
private _callID: number = 0;
// 回调 map,key 为 回调 id
private _callbacks: { [key: number]: { onSucc: Function; onFail: Function } } = {};
// JavaScript → Native 调用(异步)
public enqueueNativeCall(moduleID, methodID, params, onFail, onSucc);
// Native → JavaScript 调用,场景为模块的事件通知
public callFunctionReturnFlushedQueue(module, method, args);
// Native → JavaScript 调用,场景为模块的异步回调
public invokeCallbackAndReturnFlushedQueue(cbID, args);
}
本节中只展开 JS 调用 Native 的实现,Native 调用 JS 的实现会随模块系统章节展开。那么现在来实现 JS 调用 Native:JS 侧会先调用 MQ 的 enqueueNativeCall 方法,往内部 _queue 队列追加当前消息。注意 _queue 是一个二维数组,每行是一条消息,四列分别存储模块 id、方法 id、参数、回调 id 的数据。然后再调用 _flushQueue ,其中的核心是通过 C++ 注入的 nativeFlushQueueImmediate 方法来触发 C++ 方法回调,处理 JS 传入的队列数据。为了快速验证,C++ 的 nativeFlushQueueImmediate 方法的回调先简单实现为输出 JS 调用的消息信息,这个方法在下一节再完善其实现。
js
class MessageQueue {
enqueueNativeCall(moduleID, methodID, params, onFail, onSucc) {
// 生成回调ID并注册回调函数
let callbackID = null
if (onFail || onSucc) {
callbackID = this._callbackID++
this._callbacks[callbackID] = {
onFail: onFail,
onSucc: onSucc,
}
}
// 将调用添加到队列中
this._queue[0].push(moduleID) // moduleIds
this._queue[1].push(methodID) // methodIds
this._queue[2].push(params || []) // params
this._queue[3].push(callbackID) // callbackIds
// 立即刷新队列(简化版实现,实际 RN 会做批量优化)
this._flushQueue()
}
_flushQueue() {
if (this._queue[0].length === 0) {
return // 队列为空,无需刷新
}
const queue = this.flushedQueue()
// 重要:调用 Native 注入的方法,会触发 C++ 的回调
global.nativeFlushQueueImmediate(queue)
}
}
实现了 MessageQueue.js 后,再参考 RN 的实现把 BatchedBridge.js 完成,这个文件的作用是在 JS 环境中把桥实例注册到 global 上的变量上供 C++ 调用并把桥实例导出供 JS 调用,精简代码如下:
js
// BatchedBridge.js
// JSC 环境没有 require 的实现,这里为了便于理解原理先用 require 来演示代码
const MessageQueue = require('./MessageQueue.js')
const BatchedBridge = new MessageQueue()
// 注册到 global 上,供 C++ 调用
Object.defineProperty(global, '__fbBatchedBridge', {
configurable: true,
value: BatchedBridge,
})
module.exports = BatchedBridge;
完成了 JS 侧批量桥(本质是消息队列)的实现后,就可以新建一个测试文件 test_mq.cpp 来测试下 JS 调用 Native 的功能了,代码如下。经过一番调试,C++ 日志中成功输出了 JS 侧调用并输入的参数。
c++
// test_mq.cpp
int main() {
JSCExecutor executor;
// 由于现在还没接入打包工具
// 需要先手动加载 BatchedBridge.js 到 JS 环境中
std::cout << "Loading BatchedBridge.js..." << std::endl;
std::string batchedBridgeJS = readFile("src/js/BatchedBridge.js");
executor.loadApplicationScript(batchedBridgeJS, "BatchedBridge.js");
// 运行测试
std::string messageQueueTestScript = R"(
// 没有 require 方法,为了方便理解先这样实现
const BatchedBridge = require('BatchedBridge')
BatchedBridge.enqueueNativeCall(
2,
1,
['test_param'],
function (error) {
// JSC 环境没提供 console
// JS 侧封装 global.nativeLoggingHook 方法得到,过程省略
console.log('Error callback executed with:', error)
},
function (result) {
console.log('Success callback executed with:', result)
},
)
)";
executor.loadApplicationScript(messageQueueTestScript,
"test_messagequeue.js");
return 0;
}
总结一下,本节我们定义好了 JS 侧桥关键方法的接口并实现了其中 JS 调用 Native 的方法。有了消息队列,便有了 批量&异步 调用的可能。
Native 模块系统实现
上一节,我们实现了 JS 调用 Native,但是 Native 回调 JS 还没有打通。本节中,我们将借助模块系统来实现 Native 调用 JS 这条链路,由此我们还需要实现一个简单的 Native 模块供 JS 调用。我们以一个设备信息 DeviceInfo 模块为例进行展开,这个模块包含了同步和异步接口,JS 侧可调用的方法和使用示例如下:
typescript
interface DeviceInfo {
static getUniqueId: () => Promise<string>; // 获取设备 UUID(异步)
static getSystemVersion: () => string; // 获取系统版本(同步)
}
import { DeviceInfo } from 'react-native'
async function foo() {
const uniqueId = await DeviceInfo.getUniqueId()
const systemVersion = DeviceInfo.getSystemVersion()
}
模块的实现与注册
设备模块需要读取 Native 侧的系统信息。当前阶段 Native 运行在 macOS 平台,因此我们需要使用 Objective-C++(OC++) 结合 Apple 提供的 macOS SDK 来访问系统和设备相关的底层能力。 由于 macOS 平台的原生开发主要使用 Objective-C ,而我们的核心逻辑是以 C++ 实现的,为了在两者之间进行无缝互调,我们采用 OC++ 作为桥接语言。
OC++ 语言的文件是 *.mm 后缀的,我们新建一个 macOS 平台专属的设备模块文件 DeviceInfoModule.mm 并实现所需要的两个关键方法,摘要代码如下。由于是 OC++ 实现且调用了 Apple 提供的 macOS SDK,前端工程师看不懂也没关系,只需要知道我们实现了 macOS 模块的相关方法就好。
objective-c
// DeviceInfoModule.mm
std::string DeviceInfoModule::getUniqueIdImpl() const {
@autoreleasepool {
io_registry_entry_t ioRegistryRoot =
IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/");
CFStringRef uuidCf = (CFStringRef)IORegistryEntryCreateCFProperty(
ioRegistryRoot, CFSTR(kIOPlatformUUIDKey), kCFAllocatorDefault, 0);
IOObjectRelease(ioRegistryRoot);
NSString* uuid = (__bridge NSString*)uuidCf;
std::string result = [uuid UTF8String];
CFRelease(uuidCf);
return result;
}
}
std::string DeviceInfoModule::getSystemVersionImpl() const {
@autoreleasepool {
NSProcessInfo* processInfo = [NSProcessInfo processInfo];
NSOperatingSystemVersion version = [processInfo operatingSystemVersion];
std::ostringstream oss;
oss << version.majorVersion << "." << version.minorVersion << "." << version.patchVersion;
return oss.str();
}
}
macOS 平台的设备信息模块及其方法实现后,JS 就需要想方设法调用它,那么怎么做呢?本节开始时,我们给到了使用 DeviceInfo.js 模块上方法的 JS 片段,不难看出 JS 侧调用的方法其实和 Native 侧最终实现的方法其实是对应上的,现在的问题就在于 JS 和 Native 两侧的模块和方法如何建立对应关系。
在上一章节中,MQ 的接口中有一个方法 public enqueueNativeCall(moduleID, methodID, params, onFail, onSucc); 是专门用于 JS 异步调用 Native 的,可以看到 JS 调用时需要告诉 Native 自己要调用哪个模块(moduleID)的哪个方法(methodID)。而这些 id 是 int 类型的而非 string,JS 显然无法预设 Native 模块的模块名和方法的对应 id 是多少,需要 Native 侧把模块们的信息告诉 JS,JS 完成注册,方可被使用。这个过程便是模块的注册。
用下面的数据转换图来说明下模块注册的流程(为了便于理解,图内的代码使用类似 JS 的伪代码)。首先 Native 侧存储了模块及其方法的信息(包含了模块名、模块 id、方法名、方法 id 信息),两个数据格式均为 Array<string>。然后再把模块信息进行数据组装,格式为 Array<[ModuleName, Constants, Methods, PromiseMethods, SyncMethods]> 二位数组,其中数组的下标为模块 id,数组内单行为模块的信息,单号元素的信息依次为模块名、常量、方法名、promise 方法下标、同步方法下标。得到了该信息后,再将数组注入到 JS 环境的全局变量 global.__fbBatchedBridgeConfig.remoteModuleConfig 上。这时 JS 就可以通过遍历模块配置,动态生成模块方法的调用了。

这样就完成了 Native 方法到 JS 调用的信息对齐,其关键在于定义好配置数据格式,让 Native 可以方便地组装配置数据,让 JS 可以方便地解析配置数据。
JS ↔ Native 双向调用流程
常见的模块调用方法类型包含同步 和异步 的(DeviceInfo 模块刚好两种都有,便于演示和学习)。开篇"桥架构通信的本质"这章节中其实已经稍微讲解了同步调用是如何运作的,例子是 Dimensions.get('window'),可以再来用 DeviceInfo 模块进行展开并温习下。以 JS 侧调用 DeviceInfo.getSystemVersion() 为例,在调用方法前 Native 已经提前给 JS 环境注册好了模块信息和 global.nativeCallSyncHook(moduleId, methodId, args) 方法,在调用方法时的调用 global.nativeCallSyncHook(0, 1, null)。然后同步等待 C++ 匹配具体模块的方法并调用并返回数据。整个过程是同步的,不经过桥(桥的消息队列机制主要用于异步通信)。同步调用直接通过 JSC API 在当前线程完成,避免了跨线程通信的开销,但也意味着会阻塞 JS 线程直到 Native 返回结果,因此同步方法应当尽量避免耗时操作。
同步调用的时序图如下。

再贴一个 C++ 同步处理并返回结果的简化代码如下(模块注册器如何分发的实现略)。
c++
// JSCExector.cpp
JSValueRef JSCExecutor::nativeCallSyncHook(JSValueRef moduleID,
JSValueRef methodID,
JSValueRef args) {
// 将 JSValue 参数转换为 C++ 类型
double moduleIdDouble = JSValueToNumber(m_context, moduleID, nullptr);
double methodIdDouble = JSValueToNumber(m_context, methodID, nullptr);
unsigned int moduleIdInt = static_cast<unsigned int>(moduleIdDouble);
unsigned int methodIdInt = static_cast<unsigned int>(methodIdDouble);
// 将参数转换为 JSON 字符串
std::string argsJson = jsValueToJSONString(args);
// 模块注册器进行分发并调用具体方法,同步返回结果
std::string result = m_moduleRegistry->callSerializableNativeHook(
moduleIdInt, methodIdInt, argsJson);
// C++ 类型数据转 JS 类型
return stringToJSValue(result);
}
另一种是异步调用,比同步调用要稍微复杂些,可以用 await DeviceInfo.getUniqueId() 为例。调用该方法后会先走到 MessageQueue.js 的 enqueueNativeCall 方法(在"JS 侧桥核心方法实现"有展开讲这个方法的实现),前面的铺垫就是为了这里能够串联 JS 到 Native 的调用。再提一嘴,enqueueNativeCall 的实现实际是简化的,当前的实现是把消息推入队列后直接 flush 没有接入 Timer 做异步批处理,导致每次调用 enqueueNativeCall 事实上都只会给 Native 侧发一条消息,性能一般般。真实的 RN 实现中会引入 Timer 将多个调用累积到一帧内批量发送,从而减少跨线程通信次数。当前这么做是为了便于理解,不引入太多其他内容。
然后 JS 侧最终会调用到 global.nativeFlushQueueImmediate(queue) 方法,参数和同步调用的差别是多了 callbackID 参数,这个新增的参数是连接 Native 到 JS 异步回调的桥梁。每次异步调用时,MessageQueue 都会生成一个唯一的递增 callbackID,并将对应的成功/失败回调函数存储在 _callbacks map 中。当 Native 异步返回结果后,会携带这个 callbackID 回调 JS,JS 侧再通过 callbackID 查找并执行对应的回调函数,执行完成后会从 map 中删除该回调,避免内存泄漏。
接着我们来到 Native 侧,先看看处理调用的方法,代码如下。其实和同步调用处理的思路是类似的,都是用模块 id、方法名 id 定位到具体方法并调用。关键的不同的是结果没有同步立即的返回。
c++
// JSCExector.cpp
void JSCExecutor::nativeFlushQueueImmediate(JSValueRef queue) {
// Step 1: JSValue -> JSON字符串 (对齐RN: queue.toJSONString())
std::string queueStr = jsValueToJSONString(queue);
// Step 2: JSON字符串 -> BridgeMessage (替代 folly::parseJson)
std::cout << "[JSCExecutor] Parsing JSON with SimpleBridgeJSONParser..."
<< std::endl;
mini_rn::bridge::BridgeMessage message =
mini_rn::utils::SimpleBridgeJSONParser::parseBridgeQueue(queueStr);
// Step 3: 处理消息
for (size_t i = 0; i < message.getCallCount(); i++) {
unsigned int moduleId = static_cast<unsigned int>(message.moduleIds[i]);
unsigned int methodId = static_cast<unsigned int>(message.methodIds[i]);
const std::string ¶ms = message.params[i];
int callId = message.callbackIds[i];
// 通过 ModuleRegistry 调用 Native 模块方法
m_moduleRegistry->callNativeMethod(moduleId, methodId, params, callId);
}
}
由于调用 C++ 后数据不是同步返回的,所以得来看看异步回调 是怎么实现的,代码如下。核心还是通过 JSC 的 API 获取到 JS 环境上全局变量上的回调方法,即 global.__fbBatchedBridge.invokeCallbackAndReturnFlushedQueue,然后组装参数:把 C++ 类型的参数转为 JS 类型的,最后调用并传参。
c++
// JSCExector.cpp
void JSCExecutor::invokeCallback(int callId, const std::string &result,
bool isError) {
// 实现将结果返回给 JavaScript 的机制
// 调用 JavaScript 的 invokeCallbackAndReturnFlushedQueue 方法
// 获取全局 __fbBatchedBridge 对象
JSObjectRef bridgeObject = JSValueToObject(
m_context,
JSObjectGetProperty(m_context, m_globalObject,
JSStringCreateWithUTF8CString("__fbBatchedBridge"),
nullptr),
nullptr);
// 获取 invokeCallbackAndReturnFlushedQueue 方法
JSValueRef methodValue = JSObjectGetProperty(
m_context, bridgeObject,
JSStringCreateWithUTF8CString("invokeCallbackAndReturnFlushedQueue"),
nullptr);
// 准备回调参数
// React Native 回调约定:第一个参数是错误,后续参数是结果
// 错误情况:[error]
// 成功情况:[null, result]
std::string argsJson =
isError ? "[\"" + result + "\"]" : "[null, " + result + "]";
// 创建参数数组
JSValueRef arguments[2];
arguments[0] = JSValueMakeNumber(m_context, callId); // callbackID
// 解析 JSON 参数
arguments[1] = JSValueMakeFromJSONString(
m_context, JSStringCreateWithUTF8CString(argsJson.c_str())); // args
// 调用 JavaScript 回调方法
JSValueRef exception = nullptr;
JSValueRef callResult =
JSObjectCallAsFunction(m_context, (JSObjectRef)methodValue,
bridgeObject, // thisObject
2, // argumentCount
arguments, // arguments
&exception);
}
异步调用的时序图展示了更复杂的流程:
- JS 调用后立即返回 Promise,不阻塞线程
- 消息经过 MessageQueue 批量发送(本实现简化为立即发送)
- Native 处理完成后,通过
invokeCallbackAndReturnFlushedQueue回调 JS - JS 根据
callbackID找到对应的 Promiseresolve/reject函数并执行
再贴一张异步调用的时序图如下。

彩蛋
经过了一路旅途,很高兴你可以看到最后!🎉 文章篇幅有限,代码也只是贴了相关片段,如果想要了解完整的实现,可以详参项目源码。
项目地址 : github.com/zerosrat/mi...
当前项目中包含了本篇文章中的全部内容:
- ✅ C++ 集成 JavaScriptCore 引擎的完整实现
- ✅ JS 侧消息队列 (MessageQueue) 的实现
- ✅ Native 模块系统和注册机制
- ✅ 同步/异步双向调用的完整流程
- ✅ DeviceInfo 示例模块 (macOS 平台)
- 📝 详细的注释和构建说明
【后续计划】当前项目的进展到只是其开始阶段,接下来我计划继续完善:
- 多端支持: 当前只支持 macOS 平台,后续将适配 iOS 和 Android 平台
- 渲染系统: 当前只支持逻辑通信,后续将接入渲染层,能够渲染简单的组件
- 新架构探索: 当前只支持桥架构,后续将研究 JSI、TurboModules 等新架构特性
项目从开始到完成这篇博客前前后后花了一个月左右的时间,都是用打工后和周末的业余时间完成的,来之不易~希望大家可以多多给项目点赞支持 star ⭐。有其他的想法也可以多多交流喔。
参考
📝 本文首发于个人博客: zerosrat.dev