RN与原生通信机制深度解析
从 Native Modules 的四种通信模式到 Bridge 消息队列的内部机制,完整拆解 RN 与原生层通信的面试要点。
一句话概括
RN 与原生层的通信就像两个人对话:旧架构 Bridge 是”写信—翻译—送信—翻译”(异步、序列化、批处理),新架构 JSI 是”打电话”(同步、直接引用、无中间层)。四种通信模式——同步方法、回调、Promise、事件——覆盖了 JS↔原生 交互的所有场景。
核心知识点
1. Native Module 注册:旧架构 vs 新架构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 旧架构:通过 ReactPackage 注册,启动时全量加载所有模块
public class MyAppPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext ctx) {
return Arrays.asList(
new CalculatorModule(ctx), // 不管用不用,全部初始化
new BluetoothModule(ctx),
new ImageCacheModule(ctx)
);
}
}
// 新架构 TurboModule:懒加载,JS 首次访问时才初始化
// 通过 Codegen 生成类型安全的 C++ 桥接代码
// Calculator 模块:首次 await NativeModules.Calculator 时才 init
注册流程:Package → createNativeModules → ModuleRegistry → JS 侧 requireNativeComponent/NativeModules。旧架构的问题是所有模块全部初始化,100+ 个模块的 App 启动时初始化开销巨大。TurboModules 懒加载 + JSI 直接绑定完美解决。
2. 四种通信模式 & 怎么选
1
2
3
4
5
6
// ① 同步返回(isBlockingSynchronousMethod = true)
// 适合:获取设备信息、读取常量。⚠️ 耗时操作会直接阻塞 JS 线程!
@ReactMethod(isBlockingSynchronousMethod = true)
public String getDeviceId() {
return Settings.Secure.getString(ctx.getContentResolver(), "android_id");
}
1
2
3
4
5
6
7
8
// ② Callback:支持多次回调
// 适合:进度更新、流式数据。缺点:回调地狱,忘记释放会内存泄漏
@ReactMethod
public void download(String url, Callback onProgress, Callback onComplete) {
onProgress.invoke(50); // 50%
onProgress.invoke(100);
onComplete.invoke("success");
}
1
2
3
4
5
6
7
8
9
10
// ③ Promise:单次结果返回,支持 await
// 适合:大多数一次性的请求-响应操作。✅ 推荐
@ReactMethod
public void add(int a, int b, Promise promise) {
try {
promise.resolve(a + b);
} catch (Exception e) {
promise.reject("CALC_ERROR", e.getMessage());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// ④ 事件推送(原生 → JS)
// 适合:蓝牙设备发现、位置更新、推送通知。⚠️ JS 侧未加载时事件会丢失
@ReactMethod
public void startScan() {
scanner.setCallback((device) -> {
WritableMap map = Arguments.createMap();
map.putString("name", device.getName());
getReactApplicationContext()
.getJSModule(RCTDeviceEventEmitter.class)
.emit("onDeviceDiscovered", map);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// JS 侧统一调用方式
import { NativeModules, NativeEventEmitter } from 'react-native'
const { Calculator } = NativeModules
// 同步
const id = Calculator.getDeviceId()
// 回调
Calculator.download(url,
(pct) => console.log(pct + '%'),
(res) => console.log(res)
)
// Promise
const result = await Calculator.add(3, 5) // 8
// 事件
const emitter = new NativeEventEmitter(NativeModules.Scanner)
emitter.addListener('onDeviceDiscovered', (device) => {
console.log('发现设备:', device.name)
})
选型决策表:单次请求→Promise;进度/多次→Callback;通知/广播→Event;静态值/极快操作→同步方法。
3. Bridge 消息队列:批处理的代价
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JS 线程 原生线程
│ │
│ Calculator.add(3,5,callback) │
│ → {type:"call",args:[3,5]} │
│ → 入队 │
│ │
│ Text.setValue("hello") │
│ → {type:"call",args:["h"]} │
│ → 入队 │
│ │
│ ── flush batch ──────────────→│ 两个调用打包为一个 JSON 数组
│ [{add,3,5},{setValue,"h"}] │ → JSON.parse
│ │ → 依次执行
│ │ → 结果序列化回传
│←─ callback(8) ────────────────│
批处理的好处是减少线程切换次数(一次传一批 vs 一条一条传),代价是增加延迟——消息要在队列里等,直到下一个 flush 周期(约 16ms,与帧率对齐)。高频场景(如连续帧动画)下,16ms 的批处理延迟就是”一整帧的延迟”。
4. 事件丢失问题 & 三种解法
1
2
3
4
5
6
7
8
9
10
11
12
13
// 问题:原生 startScan() 立即发出 onDeviceDiscovered 事件
// 但 JS 侧的 addListener 还没注册好 → 事件丢失
// 解法 1:延迟发送(不推荐)
// 原生侧 Handler.postDelayed(() -> emit(...), 500)
// 缺点:500ms 太武断,快了可能还没注册,慢了浪费用户等待
// 解法 2:事件缓存队列(OK)
// 原生侧维护缓存队列,JS 侧注册后调 getCachedEvents() 补捞丢失的事件
// 解法 3:pull 代替 push(推荐)
// 原生模块不主动发事件,JS 侧在合适时机调 getLatestState()
// 符合 Fabric 的设计理念:JS 主动查询,避免推模式下的时序问题
5. JSI 通信 vs Bridge 通信对比
1
2
3
4
5
6
7
8
// Bridge:异步、有延迟、必须回调/Promise
let result
NativeModules.Storage.fetch('key', (val) => { result = val })
// 这一行之后 result 还是 undefined!
// JSI:同步、即时、直接返回
const result = NativeModules.NewStorage.fetch('key')
// result 已经是值了,可以立即用
JSI 的关键风险:如果在 HostFunction 里做了耗时同步操作(如大文件读取),会直接阻塞 JS 线程。旧架构至少”异步”天然防阻塞。新架构的开发者必须清楚哪些操作该跑在独立线程上。
其实你每天都在用
NativeModules.xxx.yyy()的每次调用都触发通信 — 你调NativeModules.CameraManager.takePicture(),背后是完整的一次 Bridge/JSI 往返Linking.openURL()就是在调原生模块 — iOS 调UIApplication.openURL,Android 调Intent,JS 侧能打开系统浏览器全靠这里- 推送通知的点击事件走的是 DeviceEventEmitter — 原生收到推送 → emit 事件 → JS 侧
addListener→ 跳转页面 react-native-permissions等社区库就是 NativeModule 的封装 — 因为每个平台的权限系统完全不同,只能通过原生模块暴露接口- WebSocket 在 RN 里是原生实现的 —
new WebSocket()的底层是原生 TCP socket,不是 JS 的 WebSocket API,这是为了绕过 JS 引擎的网络限制
常见误解(FAQ)
❌ 误区:「同步方法 = 快,应该多用于高频通信」 恰恰相反——同步方法阻塞 JS 线程,任何超过 1ms 的同步原生调用都是性能隐患。iOS App Store 审核明确规定不得滥用同步调用阻塞 JS 线程。同步方法只适用于返回值是”已经计算好的常量”,如 getDeviceId()、getAppVersion()。
❌ 误区:「Callback 和 Promise 本质上一样,只是语法糖」 Callback 支持多次回调(进度条场景),Promise 只支持单次。底层实现也不同——RN 框架为 Promise 做了专门的状态管理(resolve/reject 封装),Callback 的引用如果忘记释放会导致内存泄漏(JS 侧持有回调函数引用),而 Promise 的 resolve 后引用自动释放。
❌ 误区:「原生模块的事件一定比 JS 快」 事件的传递也要走通信层。旧架构的 emit → Bridge → JS 需要序列化 + 排队,延迟可能超过一帧(16ms)。而且如果 JS 线程正忙(GC、长任务),事件的处理还要排队等 JS 事件循环空闲。
❌ 误区:「TurboModule 迁移只需要改注解」 从 @ReactMethod(isBlockingSynchronousMethod = true) 到 TurboModule,不仅是语法变化,更是整个线程模型的重新理解——你需要显式声明方法在哪个线程执行,同步/异步的语义也变了。迁移不仔细的话,原本异步安全的方法在 TurboModule 里可能变成 JS 线程杀手。
一句话总结
RN 与原生通信的核心矛盾永远是”跨越语言边界的开销”——Bridge 靠”批处理”降低频率但增加延迟,JSI 靠”共享引用”消除拷贝但引入线程安全风险,没有银弹,只有取舍。