原文链接:www.callstack.com/blog/how-to…
基于Fabric和TurboModules的React Native新架构,在性能和开发者体验方面实现了重大飞跃。在多数情况下,TurboModules是首选方案:它提供类型安全的自动生成绑定,既消除了冗余代码又降低了桥接开销。但有时您需要更多支持——当抽象层无法完全覆盖您的具体用例时。
此时,JavaScript接口(JSI)便应运而生!
JSI 是支撑 TurboModules 的底层 API。它允许您将自定义函数直接注入 JavaScript 运行时环境,持有对 JS 对象的引用,并实现无需序列化的数据传输。当 TurboModules 功能不足时,它便是您需要使用的工具。
为何超越TurboModules?
以下是一个真实案例。在处理音频转录时,我们需要将大量二进制数据(ArrayBuffer)从JavaScript传递至原生语音转文本模块。当时TurboModules尚未直接支持ArrayBuffer,而退而求其次采用base64编码也行不通。这种方式既耗时又占用大量内存。数据本已存在于内存中,我们只需直接暴露它即可。
此时自定义JSI绑定便大显身手。它能绕过冗余转换,直接访问JS引擎的内存空间。
逃生舱:RCTTurboModuleWithJSIBindings
React Native 恰好为这类场景提供了一个协议。若您的 TurboModule 实现了 RCTTurboModuleWithJSIBindings,则可在模块初始化时注入自定义绑定。
步骤 1. 在头文件中声明遵从性
#import <ReactCommon/RCTTurboModuleWithJSIBindings.h>
@interface MyCustomModule <RCTTurboModuleWithJSIBindings>
@end
步骤 2. 实现协议方法
- (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)rt callInvoker:(const std::shared_ptr<facebook::react::CallInvoker> &)jsInvoker {
rt.global().setProperty(runtime, "__MyCustomGlobal", "Hello World!");
}
这使您能够直接访问JSI运行时和调用器。
示例:向本机传递ArrayBuffer
借助JSI,我们可以创建一个本机函数,该函数能直接从JavaScript接收ArrayBuffer,且无需任何序列化开销。我们可注入全局函数__native__processBytes__(arrayBuffer),该函数可访问原始数据。
以下是在 installJSIBindingsWithRuntime 方法中逐步实现该功能的步骤:
步骤 1:定义宿主函数
首先,从 C++ lambda 创建一个 JSI 函数。该函数可从 JavaScript 调用。函数主体中需添加处理 ArrayBuffer 的逻辑。
auto processFunc = jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, "__native__processBytes__"),
1, // expects one argument (arrayBuffer)
[](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
// function body goes here return
jsi::Value::undefined();
});
让我们快速解析传递给 createFromHostFunction 的参数:
rt:这是对 JSI 运行时的引用,即 JavaScript 世界的执行上下文。创建和操作任何 JS 值都需要它。jsi::PropNameID::forAscii(...):为函数名称创建标识符。JSI 使用此 ID 进行查找比使用原始字符串更高效。1:参数计数,告知JSI运行时该函数需接收来自JavaScript的一个参数。[](...) { ... }:作为宿主函数的C++lambda表达式。当函数被JavaScript调用时,此原生代码将执行。它接收自身参数以访问运行时环境、JS this上下文及来自JS的传递参数。
步骤 2:验证输入并访问原始数据
在 lambda 函数内部,验证第一个参数是否为 ArrayBuffer。
if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isArrayBuffer(rt)) {
throw jsi::JSError(rt, "First argument must be an ArrayBuffer");
}
然后,获取指向其底层数据的直接、零拷贝指针。
auto arrayBuffer = args[0].asObject(rt).getArrayBuffer(rt);
const void *data = arrayBuffer.data(rt);
size_t size = arrayBuffer.size(rt);
NSLog(@"Received ArrayBuffer of size: %zu", size);
步骤 3:全局注册函数
最后,将新创建的 JSI 函数附加到全局 JavaScript 对象上,使其可通过 global.__native__processBytes__ 访问。
rt.global().setProperty(rt, "__native__processBytes__", std::move(processFunc));
步骤4:从JavaScript中调用它
const bytes = new Uint8Array([1, 2, 3, 4]);
global.__native__processBytes__(bytes.buffer);
这展示了核心概念:从原生代码直接访问 JavaScript 的内存。
若需更高级的示例(涵盖数组缓冲区处理和返回 Promise),可参考 React Native AI 中的代码。
为何重要
TurboModules 可满足大多数原生集成需求。但当遇到特殊情况时——例如不支持的类型或高级优化需求——JSI 绑定能提供以下灵活性:
- 暴露自定义全局函数
- 操作原始 JS 引擎对象
- 将异步原生 API 直接桥接到 Promise
- 无需序列化即可优化数据传输
数组缓冲区即将登陆Turbo Modules
我们正全力为TurboModules提供原生级ArrayBuffer支持。鉴于此类边缘场景需求广泛,我们决定推出通用解决方案,让您能使用更简洁的代码生成器。
🎉 重要公告:TurboModules即将支持原生ArrayBuffer!
告别base64序列化,二进制数据性能飞跃!
以8MB有效负载为例:
- base64处理 → ~666毫秒 🐢
- 直接ArrayBuffer处理 → ~1毫秒 🚀
向@_ikswodarap致以热烈掌声👏! pic.twitter.com/OsGXqgqEtH
— Mike (@grabbou) 2025年9月8日
我们团队对全新架构和TurboModules充满热情,并将持续探索进一步优化方案。
结语
若说TurboModules是安全平坦的铺装道路,JSI便是开阔的荒野。你很少需要它,但掌握如何运用它,才能真正释放React Native原生层的全部潜力。
在Callstack,我们已在注重性能和底层访问的实际项目中运用这些技术。若您正面临类似的边缘案例,欢迎与我们交流。