React native 使用 JSI 库 实现 C++和JS互通
一、可选方案:
1、JSI 直接通信
原理:
通过 JavaScript Interface (JSI) 实现 C++ 对象与 JS 对象的直接内存引用映射,支持同步函数调用与数据共享。JS 可直接持有 C++ 对象的引用,绕过序列化步骤,调用时直接操作内存数据。
技术实现:
Host Object 绑定:C++ 继承 jsi::HostObject 暴露方法,JS 通过全局对象直接访问
同步执行:JS 线程与原生线程直接交互,无异步队列
RN版本支持
RN 0.64+(新架构默认启用(React native 0.64+)
原生环境依赖
Hermes引擎必选,需C++支持
典型应用场景
高频同步调用(如实时手势处理)、内存密集型操作(图像/音视频处理)
实现难度
需深入掌握 C++ 内存操作与线程同步,直接操作 jsi::HostObject 易引发崩溃
性能对比
≤1ms (内存直读)、10万+ ops/sec、共享 ArrayBuffer、实时音视频处理
优点:
零序列化开销:JavaScript与C++直接共享内存,同步调用延迟≤1ms,性能碾压异步方案
细粒度控制:支持直接操作原生对象(如jsi::ArrayBuffer),适合高频计算(如物理引擎)
线程灵活性:可绕过UI线程限制,实现多线程并行渲染(如Fabric架构)
缺点
开发门槛高:需精通C++内存管理及线程同步,调试崩溃风险显著增加
维护成本:脱离RN框架约束,需自行处理平台差异(如Android/iOS指针对齐)
兼容性风险:Hermes引擎版本升级可能导致JSI接口变更
2、TurboModules
原理:
基于 JSI 的模块化封装,通过 Codegen 生成类型安全的 JS 接口,实现按需加载原生模块(C++/Java/OC)。模块首次调用时初始化,减少启动开销
技术实现:
类型安全绑定:自动生成 JS 与原生端的接口文件,确保参数类型匹配
按需加载:原生模块延迟初始化,优化性能
RN版本支持
RN 0.68+(需手动启用新架构)
原生环境依赖
Codegen工具链,类型声明文件
典型应用场景
跨平台原生模块开发(如蓝牙、传感器)、需类型安全的商业级模块
实现难度
Codegen 配置复杂,需维护类型声明文件,跨平台适配需处理 JNI/OC 桥接
性能对比
≤5ms (JSI 封装)、5万+ ops/sec、轻量封装对象、跨平台蓝牙模块
优点
类型安全:Codegen自动生成TS/Flow类型定义,减少跨语言调用错误
按需加载:模块延迟初始化使应用启动时间优化30%-50%
官方支持:Facebook主力维护,与Fabric渲染器深度集成
缺点
迁移成本:旧模块需重写Spec文件并适配Codegen工具链
灵活性受限:强依赖RN框架,无法实现底层硬件直连(如USB协议栈)
调试复杂性: 跨语言调用栈难以追踪(需结合LLDB+Chrome DevTools)
3、引擎扩展注入
原理:
直接向 JavaScript 引擎(如 Hermes/V8)注入 C++ 函数,绕过 React Native 框架层。JS 全局对象可直接调用注入的原生方法
技术实现:
引擎 API 调用:通过 jsi::Runtime 在引擎初始化时注册全局函数
独立通信通道:适用于非 React Native 绑定的底层功能扩展
RN版本支持
无RN版本限制(依赖引擎版本)
原生环境依赖
需定制JS引擎(如Hermes/V8)
典型应用场景
底层性能优化(如游戏引擎集成)、非RN框架的原生功能扩展(如嵌入式设备控制)
实现难度
需定制 JS 引擎(Hermes/V8),深入理解引擎 ABI,脱离 RN 框架维护成本高
性能对比
≤1ms (引擎级优化)、10万+ ops/sec、零拷贝内存引用、游戏引擎集成
优点
极致性能:过RN框架层,直接调用引擎API(如Hermes/V8),吞吐量达10万+ ops/sec
场景扩展性:支持非RN生态的原生功能接入(如嵌入式设备控制)
内存高效:零拷贝数据传输(如共享WebAssembly内存)
缺点
技术垄断:需深度定制JS引擎,绑定特定厂商(如Facebook Hermes团队)
升级风险:引擎ABI变更可能导致注入接口失效
生态孤立:无法复用RN社区模块(如React Navigation)
4、旧版 Bridge
原理:
通过异步消息队列和 JSON 序列化实现 JS 与原生通信。JS 调用经 Bridge 序列化为 JSON 传递到原生端,结果通过回调返回
技术实现:
异步队列:JS 与原生线程通过 Bridge 线程通信,存在序列化开销25。
性能瓶颈:大量数据传递时延迟显著,最高达 100ms+
RN版本支持
RN 0.67及以下(旧架构默认)
原生环境依赖
无特殊要求,JSON序列化
典型应用场景
旧项目维护、简单功能调用(如弹窗/Toast)、低性能要求的工具类模块
实现难度
只需基础 JSON 序列化知识,原生模块注册标准化,文档完善
性能对比
50-100ms (异步队列)、1千 ops/sec、2倍序列化缓存、简单 Toast 提示
优点
开发简单:JSON序列化机制学习成本低,文档完备
生态兼容:支持RN 0.67及以下版本,存量项目维护成本低
调试友好:MessageQueue可监控通信流量
缺点
性能瓶颈:万次调用延迟≥1200ms,列表滚动易卡顿
内存浪费:JSON序列化导致2倍内存占用(如Base64图片传输)
淘汰风险:Facebook已停止功能更新,新架构特性不可用
二、选型经过
第一阶段:
想选个简单的 旧版 Bridge,遇到风险:依赖停止维护相互冲突,没有办法找到稳定的版本。被迫升级版本。
第二阶段:
按照难度选了一个 TurboModules,但是它配置有点复杂,在原先React native 项目上 不能直接集成,需要先升级到新的框架,换一波依赖(有些依赖包国内没有 镜像源,下载速度很慢,或者直接失败),被配置和依赖包升级打败了,最主要的一点 需要Xcode 16以上,在使用的macBook 上现有安装的是15.4. 更新需要先更新macbook系统,要15.4G. 劝退。
第三阶段:
只剩下两个性能高的了,一个直连,一个用引擎,稍微了解了下技术使用,因为是示例 所以没有太设计底层的业务,直连也就只要实现一个桥接就行。引擎的那个是要学习引擎的,劝退。
最后:
选型为 JSL直连。
三、JSL直连技术研究
1、大概原理了解
首先了解到JSL 直连的话是通过一个objective C++ 混合C++来实现 C++桥接到底层内存,访问到JS主线程,和公共对象。
然后就能实现 C++的值和方法 通过指针的暴露复杂一份指针到 js共用对象上实现了JS可调用C++共享的内存内容。
当然这个内存共享的过程 是RN 包中集成的JSI库Hosobject 对象 监听 JS调用 自动实现的。
2、JSL支持所需环境
动手之前先了解下JSL直连的,环境选择。
nodeJS必须得React native 项目,React 基础环境。这里我们之前选择的React native版本为0.72.5,是中间版本,依赖有问题很容易找到相近的版本无论升级降级。node的话只要 16以上就行了,20以下,太新的node环境会出现依赖冲突和毁损,毕竟底层的编译逻辑有变化。这里我们用nvm 管理工具先选择一个18 稳定版本,nvm也方便后续切换node版本。
这里我们用macBook 和 Xcode 调试IOS版本的程序,这里React native 就需要cacapods依赖管理库,这是个自动管理依赖的工具,可以为每个项目当度创建一个Pods文件存放依赖文件。工具下载可以用macBook自带的ruby下载。也可以下载一个Homebrew 开源软件包管理器来下载。
当然 很多开源包都是从github上下载的,可以把源地址切换成国内的镜像源地址。
3、JSL直连实现路径
JSL直连实现可以选择两个路径 新架构的TurboModules 实现,和旧架构的内置集成实现。之前说过了TurboModules 的配置,那就跳过,选旧架构。直接 C++ ->objective-C++->jsi 对象(内存)->JS (虽然是直连,但是React native不是天然支持C++,需要objective-C++ 混编为C语言体才能跨语言调用)
这里选择旧的架构 Bridge(桥模式),JSI 也有两种选择 引擎选择JavaScriptCore 和 Hermes,0.68以上版本 默认开启 Hermes 引擎加速,所以 JSI也是和其内核走要引用 Hermes的一些包,但是现在不支持自动切换,还需要配置头文件地址之类的。引擎加配置,那就,没多大的选择了,虽然JavaScriptCore 内核构建的时候比较慢,但谁让它在老版本是在React native 包内呢?只需要和React native 包路径一起配置就行。关闭Hermes配置。
4、创建RN项目
选择好了方向,那就开始建项目了。先下一个公共的react native 构建包
@react-native-community/cli 然后cd到要创建项目的目录下,
npx @react-native-community/cli init XXX(项目名称) --0.72.5 创建一个版本为0.72.5 的React native 项目。
等一段时间其中要选一些配置比如问你是否要下cacapods,如果本地存在这个工具就不用下了。
5、拉起依赖包
等项目下后了后,用npm 或者 yarn 拉取依赖包,有些包0.72.5找不到了,可以选临近的包。
依赖包拉取完了 我们cd到要调试的ios目录下,用cacapods 的命令 pod install 拉取依赖,这个命令用到的地址一般都是 github和国外的一些包管理网站,可能需要针对性加速和替换成国内的镜像源。(注意boost这个包下载地址包管理器需要换成官方的地址和SHA256 Hash)
6、配置Xcode
接下来就是 打开Xcode 进行一些配置了,比如C++语言支持0.72.5默认是c++17支持,还有一些C++文件的联调引入,默认只引入AppDelegate.mm和main.m.
点运行自动进行app生成和模拟器安装打开以及C语言。java、C++的预编译。(注意:相关版本的模拟器需要提前下载)
7、JSL直连示例实现
运行一个示例程序下面就要到 JSI 直连实现了。
(1)、jsi直连逻辑实现(头定义)
首先我们在项目根目录下建立cpp文件,存放我们实现文件和jsi注册头和jsi集成C++逻辑的C++实现类。
cpp/NativeModule.h
#pragma once
#include <jsi/jsi.h>
using namespace facebook::jsi;extern "C" class NativeModule : public facebook::jsi::HostObject {
private:
facebook::jsi::PropNameID getSymbolToStringTagId(facebook::jsi::Runtime &rt);
public:
// 必须公开析构函数
virtual ~NativeModule() = default;
static void install(Runtime &runtime);
// 必须重写get方法
facebook::jsi::Value get(facebook::jsi::Runtime& runtime,
const facebook::jsi::PropNameID& name) override;
// 新增toStringTag处理
std::vector<facebook::jsi::PropNameID> getPropertyNames(facebook::jsi::Runtime& rt) override;
};
这里把NativeModule 这个类暴露成C的头样 之后可能在桥接实现中引用。
公开构造函数方便HostObject 自动代理
install 静态方法就是实现注册公用HostObject对象到Js的global公用对像的方法。
get 函数 就是HostObject 自动代理到get中获取对应c++逻辑的实现。
getSymbolToStringTagId和 getPropertyNames
都是为了解决 JS的 Symbo.StringTagId 类型找不到的问题。
(2)、jsi直连实现
cpp/NativeModule.cpp
这里截取一些重要的片段
void NativeModule::install(Runtime &runtime) {
auto instance = std::make_shared<NativeModule>();
创建智能指针,自动实现一次性分配内存和内存自动释放。
//绑定方法
runtime.global().setProperty(
runtime,
"nativeModule",
Object::createFromHostObject(runtime, std::move(instance))
这里使用 facebook::jsi::Runtime 运行时对象 挂载NativeModule对象到JS的global 共用对象上。JS直接可以 global.nativeModule 访问挂载对象。
std::vector<facebook::jsi::PropNameID> NativeModule::getPropertyNames(facebook::jsi::Runtime& rt) {
std::vector<facebook::jsi::PropNameID> props;
props.reserve(4); // 预分配空间优化性能
props.push_back(facebook::jsi::PropNameID::forAscii(rt, "exampleMethod"));
props.push_back(facebook::jsi::PropNameID::forAscii(rt, "add"));
props.push_back(facebook::jsi::PropNameID::forAscii(rt, "multiply"));
props.push_back(facebook::jsi::PropNameID::forAscii(rt, "Symbol.toStringTag"));
return props;
}
这个方法把字符串的方法名生成PropNameID,HostObject 对象监听方法名转化为PropNameID在转给get方法。
Value NativeModule::get(Runtime &runtime, const PropNameID &name) {
try {
auto methodName = name.utf8(runtime);
if (methodName == "exampleMethod") {
return Function::createFromHostFunction(
runtime,
name,
0, // 参数数量修正
[](Runtime &runtime, const Value &, const Value *, size_t) {
return String::createFromAscii(runtime, "Hello from C++");
}
);
}
这样get就把PropNameID 转化为字符串匹配方法名用 Function::creactFromHostFunction 把C++的逻辑和内存暴露给 JSI方向。
这里JSI把C++内存 和逻辑暴露给JS的实现就写好了,接下来就是桥接到入口文件进行注册了,这里我们只进行了IOS环境的调研。
这里使用了一个例子方法,你也可以导入你的C++实际业务类。调用实现的方法。
(3)用objective-C++ 的mm文件 进行桥接
ios/XXX(项目名称)/
和入口AppDelegate.mm 同一个文件夹下建立桥接头和文件
NativeModuleWrapper.h
#pragma once
#import <React/RCTBridgeModule.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import <jsi/jsi.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>@interface NativeModuleWrapper : NSObject <RCTBridgeModule>
+ (void)setupWithRuntime:(facebook::jsi::Runtime &)runtime;
@end
这里桥接的头定义要写C语言的头
桥接接口继承 NSObject <RCTBridgeModule>
定义了一个自定义的注册接口
(4)桥接的实现
ios/XXX(项目名称)/NativeModuleWrapper.mm
// React Native 核心
#import <React/RCTBridgeModule.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>// JSI 和 C++ 支持
#import <jsi/jsi.h>
#import <ReactCommon/CallInvoker.h>
#import <vector>
#import <string>// 桥接和系统
#import <Foundation/Foundation.h>
#import <objc/runtime.h>// 本地模块
#import "NativeModuleWrapper.h"
#import "../../cpp/NativeModule.h"@implementation NativeModuleWrapperRCT_EXPORT_MODULE(NativeModule);+ (void)setupWithRuntime:(facebook::jsi::Runtime &)runtime {
RCTLogInfo(@"Module registered");
NativeModule::install(runtime); // 调用 C++ 的 install 方法
}@end
这里主要逻辑RCT_EXPORT_MODULE(NativeModule);暴露NativeModule头的内容。
在自定义注册方法中,添加进C++的注册逻辑
(5)入口文件改造
ios\XXX(项目名)\AppDelegate.mm(项目创建的时候自动生成的)
在这里补充一下监听以及注册的逻辑
#include "NativeModuleWrapper.h"
引入桥接的头
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge {
return @[[NativeModuleWrapper new]]; // 返回需注册的模块
}
扩展桥接模块。
- (void)setBridge:(RCTBridge *)bridge {
[super setBridge:bridge];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(bridgeDidFinishLoading:)
name:RCTJavaScriptDidLoadNotification
object:bridge];
}
这里在桥对象初始话的方法中重新 添加JS加载完成的监听,回调到bridgeDidFinishLoading 方法
保证注册是在桥对象和JS都初始化完成后
- (void)bridgeDidFinishLoading:(NSNotification *)notification {
RCTBridge *bridge = notification.object;
// 2. 安全地在JS线程执行
dispatch_async(dispatch_get_main_queue(), ^{
[self installJsiModule:bridge];
});
}
使用JS主线程安全执行,执行默认在主线程队列中进行。
下面是installJsiModule 部分逻辑
if (bridge && [bridge respondsToSelector:@selector(batchedBridge)]) {
id batchedBridge = [bridge batchedBridge];
if (batchedBridge && [batchedBridge respondsToSelector:@selector(runtime)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([batchedBridge respondsToSelector:@selector(runtime)]) {
auto *runtimePtr = (__bridge facebook::jsi::Runtime*)[batchedBridge performSelector:@selector(runtime)];
facebook::jsi::Runtime &runtime = *static_cast<facebook::jsi::Runtime*>(runtimePtr);
auto global = runtime.global();
[batchedBridge
dispatchBlock:^{
[NativeModuleWrapper setupWithRuntime:runtime];
}
queue:RCTJSThread];
}
#pragma clang diagnostic pop
}
}
这里通过 batchedBridge 的runtime对象 降级获取,0.72.5版本 不加 降级参数 会编译报错,提示已废弃,私有化不能获取。
后面就是不同类型之间的强转。
最后在批量桥的对象的线程安全保证中 执行桥的初始话方法,传递运行时对象到桥注册方法。
到这里 C++业务实现类 和 objective-C++桥接类和主入口都改完了。
(6)、React前端的调用
seEffect(() => {
showMessage();
//回收
return () => {tiemRef?.current && clearTimeout(tiemRef.current);}
}, [])
初始话调用测试方法 返回 Hello from C++
const showMessage = () => {
console.log("JSI 模块可用:", 'nativeModule' in global);
if('nativeModule' in global) {
tiemRef?.current && clearTimeout(tiemRef.current);
const module = global.nativeModule as NativeModule | undefined ;
if(module) {
const _res = module?.exampleMethod();
setMessage(_res);
}
} else {
tiemRef.current = setTimeout(() => {
showMessage();
}, 100);
}
}
初始话调用的话可能 JSI 还没初始话好,重复调用。
这里重新用Xcode 联调的话就能看到从C++发过来的消息了