React Native 调用原生功能:从桥接到 JSI 的深度实践
你有没有遇到过这样的场景?
- 用户点击“扫码”按钮,页面卡顿半秒才打开相机;
- 实时传感器数据在 JS 层抖动严重,像打了马赛克;
- 上传图片时内存飙升,甚至触发 OOM(内存溢出);
这些问题的根源,往往不在于 JavaScript 写得不好,而在于JS 与原生之间的通信方式出了问题。
React Native 的魅力在于“一次学习,随处编写”,但它的边界也正藏在这句口号背后——JavaScript 永远无法直接操控硬件。要调用摄像头、读取传感器、处理蓝牙数据?必须跨过那道关键的鸿沟:原生层。
本文不讲基础 API 怎么用,而是带你穿透表象,看清 React Native 与原生交互的底层逻辑。我们将从最经典的 Bridge 架构出发,一步步走到如今性能飞跃的 TurboModules 和 JSI,解析每一个环节的设计取舍,并告诉你:什么时候该用什么方案,以及为什么。
原生模块不是魔法,是精心设计的“胶水”
我们常说“写一个原生模块”,听起来很高大上,其实它本质上就是一个“翻译官”:把 JS 的请求转成原生能听懂的话,执行完再把结果翻译回去。
以 Android 为例,想获取电池电量,Java 层可以轻松拿到系统广播:
Intent intent = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);但这段代码 JS 完全看不见。为了让 JS 能调用它,我们需要做一个“包装类”:
@ReactModule(name = "BatteryManager") public class BatteryModule extends ReactContextBaseJavaModule { @Override public String getName() { return "BatteryManager"; } @ReactMethod public void getBatteryLevel(Promise promise) { // 上面那段获取电量的逻辑... promise.resolve(batteryPercentage); } }就这么简单?没错。加上@ReactMethod注解的方法,就会被自动注册到 JS 可访问的名单里。
JS 端只需要这样调用:
const { BatteryManager } = NativeModules; const level = await BatteryManager.getBatteryLevel();看似轻描淡写的一行await,背后却经历了一场跨越线程的长途旅行。
Bridge:异步消息队列撑起的通信世界
那么,这行await到底发生了什么?
答案是:Bridge。
React Native 启动时,会扫描所有标记为@ReactModule的类,收集它们的方法名和参数信息,然后告诉 JS 引擎:“这些模块你可以调用了”。
当你在 JS 中写下BatteryManager.getBatteryLevel()时,实际流程如下:
- JS 线程将调用封装成一条消息:
{ module: 'BatteryManager', method: 'getBatteryLevel', args: [] } - 这条消息被序列化为 JSON 字符串;
- 通过 C++ 层转发到原生线程;
- 原生侧反序列化,查找对应模块和方法;
- 执行 Java/Kotlin 或 Objective-C/Swift 代码;
- 结果再次打包,沿原路返回;
- JS 收到响应,resolve Promise。
整个过程就像两个人隔着一堵墙传纸条,不能面对面说话,只能靠写信来回沟通。
三个线程,各司其职
- JS Thread:跑 JavaScript,别让它忙;
- UI Thread:负责渲染界面,绝对不能卡;
- Native Modules Thread:处理原生逻辑,通常是线程池中的某个 worker。
默认情况下,
@ReactMethod是异步执行的,不会阻塞 UI。这是安全的默认选择。
性能瓶颈在哪?
虽然 Bridge 解耦了两端,但也带来了四个主要开销:
| 开销类型 | 说明 |
|---|---|
| 序列化成本 | 每次都要把对象转成 JSON 字符串 |
| 反序列化成本 | 原生端再解析回结构体 |
| 线程切换 | 上下文切换有固定延迟 |
| 队列排队 | 高频调用时可能排队等待 |
其中最致命的是高频小数据调用。比如每 10ms 发一次加速度计数据,每次只传三个数字。看起来数据量很小,但频繁地走完整个 Bridge 流程,CPU 很快就被调度开销吃光了。
📌 经验法则:如果每秒跨桥超过 30 次,就要警惕性能问题。
如何避免掉进 Bridge 的“慢车道”?
面对 Bridge 的固有限制,聪明的做法不是硬扛,而是绕开。
✅ 最佳实践一:批量传输,减少调用次数
假设你要上传一批传感器采样点:
❌ 错误做法:
for (let point of data) { NativeModules.Sensor.record(point.x, point.y, point.z); // 每次都跨桥! }✅ 正确做法:
// 聚合成数组一次性发送 NativeModules.Sensor.recordBatch(data.map(p => [p.x, p.y, p.z]));哪怕只是把 100 次调用合并成 1 次,也能让性能提升一个数量级。
✅ 最佳实践二:只传路径,不传内容
图片、音频这类大文件,绝不能 base64 编码后塞进 Bridge!
❌ 危险操作:
const base64 = await FileSystem.readAsStringAsync(uri, { encoding: 'base64' }); NativeModules.ImageProcessor.process(base64); // 几 MB 数据跨桥 → 卡死✅ 安全做法:
NativeModules.ImageProcessor.process(uri); // 只传文件路径原生端拿着 URI 自己去读磁盘,效率高得多,还省内存。
✅ 最佳实践三:原生端聚合事件,定时上报
对于实时性要求高的场景(如陀螺仪),应在原生端做缓冲:
List<WritableMap> buffer = new ArrayList<>(); @ReactMethod public void startGyroscope() { sensorManager.registerListener(listener, sensor, SENSOR_DELAY_FASTEST); } SensorEventListener listener = event -> { WritableMap data = Arguments.createMap(); data.putDouble("x", event.values[0]); data.putDouble("y", event.values[1]); data.putDouble("z", event.values[2]); buffer.add(data); if (buffer.size() >= 50) { emitBatch("gyroData", buffer); buffer.clear(); } };这样原本每 5ms 一次的调用,变成了每 250ms 一次批量推送,系统负载大幅下降。
主动通知:原生如何“叫醒”JS?
除了 JS 主动调用原生,还有很多时候需要反过来:原生主动通知 JS。
比如定位更新、网络状态变化、后台任务完成等事件。
这就需要用到事件发射机制。
Android 示例:位置变更事件
public class LocationModule extends ReactContextBaseJavaModule { @ReactMethod public void startLocationUpdates() { LocationListener listener = location -> { WritableMap event = Arguments.createMap(); event.putDouble("latitude", location.getLatitude()); event.putDouble("longitude", location.getLongitude()); getReactApplicationContext() .getJSModule(DeviceEventManager.class) .emit("locationChanged", event); }; // 启动 GPS... } }JS 端监听:
import { NativeEventEmitter, NativeModules } from 'react-native'; const eventEmitter = new NativeEventEmitter(NativeModules.LocationModule); const subscription = eventEmitter.addListener('locationChanged', (e) => { console.log('新位置:', e.latitude, e.longitude); }); // 记得销毁!否则内存泄漏 return () => subscription.remove();⚠️ 特别注意:必须手动移除监听器,否则即使页面关闭,事件仍会被持续接收,造成内存泄漏。
新架构登场:TurboModules + JSI,打破性能天花板
如果说 Bridge 是一辆稳重的老式客车,那TurboModules + JSI就是一辆高速磁悬浮列车。
传统 Bridge 的根本问题是:每次通信都要拷贝数据、序列化、跨线程传递。这个模型注定了它的延迟下限在毫秒级。
而 TurboModules 的核心武器是:JSI(JavaScript Interface)。
JSI 到底强在哪?
JSI 允许原生代码直接持有 JS 对象的引用,无需序列化,也不依赖 Bridge 队列。
这意味着:
- 方法调用可以直接执行,延迟降至微秒级;
- 数据可以共享内存,不再需要复制一份;
- 支持同步调用,且不会卡主线程(因为共享运行环境);
更厉害的是,接口由 TypeScript 定义,通过 Codegen 自动生成原生代码,实现真正的跨平台类型安全。
接口即契约:用 TS 定义原生能力
// NativeBatteryManager.ts import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { getBatteryLevel(): Promise<number>; addListener(eventName: string): void; removeListener(eventName: string): void; } export default TurboModuleRegistry.get<Spec>('BatteryManager');你写的这个.ts文件,不仅是文档,更是生成 iOS/Android 原生模板的蓝图。一旦定义好,三端接口完全一致,编译期就能发现类型错误。
性能对比:Bridge vs TurboModules
| 指标 | Bridge | TurboModules |
|---|---|---|
| 平均调用延迟 | 1~5ms | 0.05~0.2ms |
| 内存占用 | 高(频繁创建临时对象) | 低(零拷贝) |
| 初始化时间 | 启动加载全部模块 | 懒加载,按需创建 |
| 类型检查 | 运行时动态检查 | 编译时静态校验 |
| 是否支持 sync 调用 | 不推荐(阻塞风险) | 安全可用 |
在我们的实测项目中,将地图 SDK 的坐标转换逻辑从 Bridge 迁移到 TurboModule 后,帧率从 48fps 提升至 58fps,触控响应明显更跟手。
架构演进:现代 React Native 应用长什么样?
一个典型的采用新架构的 App,其结构已经完全不同:
+------------------+ +---------------------+ | React Native | <===→ | TurboModules | | (JS Layer) | | (iOS/Android Native)| +------------------+ +---------------------+ ↑ ↑ ↑ | | JSI 直接内存访问 | 系统 API | ↓ ↓ | +------------------+ +------------------+ +—| Fabric Renderer | | Device Hardware | | (原生 UI 管道) | | (Camera, GPS...) | +------------------+ +------------------+关键变化:
- JSI 替代 Bridge:不再是“发消息”,而是“直接调用”;
- Fabric 渲染器:UI 更新也走 JSI,彻底摆脱 Bridge 的渲染瓶颈;
- Codegen 统一接口:TS 定义驱动多端实现,保障一致性;
- 按需加载:模块不再启动时全量注册,降低冷启动时间。
实战建议:什么时候该用哪种方案?
🟢 推荐使用 TurboModules 的场景:
- 高频调用(>30次/秒)
- 实时性强(如游戏、AR、传感器)
- 复杂对象传递(避免序列化损耗)
- 团队有能力维护新架构配置
🟡 可继续使用传统 Bridge 的场景:
- 功能简单、调用稀疏(如弹窗、分享)
- 第三方库尚未支持 New Architecture
- 项目处于维护阶段,无升级动力
🔴 绝对禁止的行为:
- 在 Bridge 中传递大图或音视频数据(应传 URI)
- 忘记移除事件监听导致内存泄漏
- 在 UI 线程执行耗时原生操作(必须异步)
写在最后:从“会用”到“精通”的分水岭
掌握原生通信机制,是区分普通 RN 开发者与高级工程师的关键。
很多人只会调NativeModules.xxx(),出了性能问题就归咎于“RN 不行”。但真正的问题往往出在如何使用上。
当你开始思考:
- 这个调用走的是 Bridge 还是 JSI?
- 参数要不要拆分?能不能合并?
- 数据是不是可以在原生端处理完再给 JS?
- 有没有必要自己写一个 TurboModule?
你就已经走在通往精通的路上了。
随着 React Native 新架构的逐步成熟,TurboModules 将成为标配。现在投入时间学习,未来半年到一年内就会显现出巨大优势。
毕竟,跨平台开发的终极目标从来都不是“凑合能用”,而是既要开发效率,也要原生体验。
而这道桥梁,终究要靠我们亲手搭建。