做 React Native 混合开发,只要业务碰过原生能力,几乎都会遇到同一个问题:JS 层怎么调原生方法,原生又把结果怎么回给 JS。这篇文章基于我自己的项目经验,把“原生和 RN 互相调用”以及“事件监听”这条链路完整拆一遍,从桥接原理到两端代码,从调用姿势到实际坑点,尽量一次讲透。适合正在做 RN 原生集成、被两端通信绕晕的新手,也适合想系统梳理这块知识的老手。
先说一句我在项目中反复强调的话:React Native 和原生之间的每一次通信,本质都不是普通函数调用,而是一条异步消息通道。你要是理解了这句话,后面一半的坑都能提前避开。
1. 先搞明白“桥接”到底在桥什么
1.1 为什么互调不能当成普通函数调用
React Native 的 JS 运行在 JavaScriptCore 或 Hermes 引擎里,原生代码运行在各自平台 Runtime 中,两边不在同一个线程,也不共享内存。RN 页面调用一个原生方法,并不是 JS 直接执行原生函数,而是把模块名、方法名、参数序列化成消息,塞进一个队列,原生模块在自己的工作线程上消费这条消息并执行。原生要把数据回传给 JS,同样走消息队列或事件通道。
你可以把这座 Bridge 理解成一个快递中转站。JS 写好包裹,扔给中转站;中转站按地址派给原生模块;原生模块处理完再打包寄回去。整个过程是异步的,而且包裹内容必须能简单序列化。所以有一个老生常谈的约束:跨端传递的数据只能是字符串、数字、布尔、数组、对象这类 JSON 兼容类型,Date、Function、Map 这些不能直接传,传过去也不是原来的对象。
1.2 为什么方法只能“异步返回”
经常有人问,能不能让 JS 同步拿到原生返回值?桥接机制决定了这条路基本走不通。如果强行同步,JS 线程要一直阻塞等待原生线程返回,页面卡顿是小事,两个线程互相等还可能造成死锁。
所以 React Native 从设计上强制了异步。RN 调用原生后,原生如果要把结果返回,只能用三种姿势:Callback、Promise、事件推送。Promise 是现在最常用的,责任链清晰,配合 async/await 写出来的调用代码和普通 Promise 差不多,可读性高。Callback 在老项目中偶尔还有,iOS 的 RCT_EXPORT_METHOD 里也能按老 API 传两个回调。但事件推送更适合“原生主动通知 JS”的场景,实际上事件监听这套机制,也正是为了解决“原生自发消息,RN 被动收”这类需求。
1.3 新旧架构对通信方式的影响
老架构下所有 JS 和原生通信都通过 Bridge,消息要序列化、排队、跨线程中转,在调试模式下还能明显感觉到延迟。新架构推出后,改为用 JSI 让 JS 直接持有原生方法引用,跳过了 JSON 序列化这一步,通信延迟大幅降低,这就是 TurboModule 的核心思路。
那篇文章里我不会一上来劝所有项目都去迁移新架构。真实情况是,很多存量项目依然跑在老架构或兼容模式下,传统 NativeModules、RCT_EXPORT_MODULE 这些 API 仍然能工作。理解老桥接机制依然有价值,因为新架构下的调用方式尽管底层变了,业务层的代码长得很像,而且老代码过渡到新架构通常有兼容层。做混合开发,先把“桥接模式下事件怎么发、模块怎么暴露、参数怎么传”弄明白,新架构来了也能平滑迁移。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RN 调用原生:自定义原生模块的完整套路
2.1 Android 端:写一个模块并注册进应用
RN 调用原生,第一步是原生侧暴露模块。Android 上最基础的写法是继承 ReactContextBaseJavaModule,然后通过 @ReactMethod 注解暴露 JS 可调用的方法。
我以扫码模块为例,这是混合开发里特别典型的需求。在 Kotlin 里大致长这样:
kotlin复制class ScanModule(private val reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "ScanModule"
@ReactMethod
fun openScanner(type: Int, promise: Promise) {
// 跳转到原生扫码页
// 成功后 resolve 结果
// 失败或用户取消时 reject
}
}
注意两点。第一,getName() 返回的名字就是 JS 侧 NativeModules.ScanModule 里的键名,两边的名字必须对得上。第二,@ReactMethod 标注的方法返回值必须是 void,不能直接 return 一个字符串或对象回去。如果不用 Promise 也不是 Callback,你即使写了返回值,JS 侧也拿不到。
光写模块还不够,Android 需要把模块通过 ReactPackage 注册进去。我经常见新手在这里漏掉注册,JS 端报 ScanModule is null,排查半天才发现是包没挂上。
code复制public class ScanPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ScanModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
然后在 MainApplication 的 getPackages() 里把 new ScanPackage() 加进去。这一步做完,RN 端才可能拿到这个原生模块。
如果项目里同时有多个模块,不要图省事全丢在某个 Activity 里。建议一个独立业务模块对应一个 ReactPackage,Application 那层只做聚合。这样后面按需裁剪功能时,改动面会小很多。
2.2 iOS 端:OC 模块的导出与注册差异
iOS 侧的写法和 And
