2. 原生模块通信原理

在 React Native 中,JavaScript 代码运行在 JavaScript 引擎(旧架构下为 JSC,新架构下可为 Hermes)中,而原生模块运行在原生线程(iOS 主线程或模块队列、Android 主线程或 NativeModules 线程)上。两者处于不同的运行时、不同的内存空间,甚至不同的语言体系。要让 JS 调用原生能力,必须有一套跨语言、跨线程的通信机制,这就是“桥接(Bridge)”。理解桥接机制,是编写稳定原生模块的前提。

一、桥接的本质:消息队列 + 序列化

旧架构下,JS 与原生之间并不是直接函数调用,而是通过一条异步消息通道传递数据。JS 侧调用 NativeModules.XxxModule.method() 时,实际发生的是:把模块名、方法名、参数打包成一条消息,写入 JS 到原生的消息队列;原生侧由桥接层从队列取出消息,反序列化后找到对应模块和方法执行;执行结果再以同样方式回传。整个过程是异步的,JS 调用后立即返回,不会阻塞 JS 线程等待原生结果。

由于消息必须跨语言传递,参数和返回值需要经过序列化与反序列化。桥接层只支持有限的数据类型:字符串、数字、布尔、null、数组、对象(键为字符串)。日期、函数、类实例、Symbol、循环引用等都无法直接传递。例如把 new Date() 作为参数传给原生模块,实际到达原生侧时往往变成字符串或数字时间戳,需要开发者自行约定格式。这也是原生模块接口设计中必须明确参数类型的原因。

JS 与原生桥接通信模型(旧架构) JavaScript 线程 JS 调用 NativeModules.M.get() 序列化 JSON 兼容类型 写入消息队列 异步、不阻塞 Promise 挂起等待 Bridge 消息通道 JS → Native 队列 模块名 / 方法 / 参数 Native → JS 队列 回调 ID / 结果 / 错误 批量调度 每帧合并消息 异步、可跨线程 原生线程 反序列化 还原为原生类型 查找模块方法 反射 / 注册表 执行原生逻辑 可能耗时 / 阻塞 回调 / Promise 回传

二、异步调用模型与回调 ID

桥接通信是异步的,因此原生模块的方法通常返回 Promise,或接受成功/失败回调。桥接层为每次调用分配一个唯一的回调 ID,原生执行完成后,把结果和该 ID 一起写回 Native → JS 队列,JS 侧根据 ID 找到对应的 Promise 并 resolve 或 reject。这解释了为什么原生模块方法不能像同步函数那样直接返回结果——返回值必须通过回调或 Promise 传递。

一个典型的原生模块方法签名(Android 示例)如下:

@ReactMethod
public void readFile(String path, Promise promise) {
    try {
        String content = FileUtils.read(path);
        promise.resolve(content);      // 成功回传
    } catch (IOException e) {
        promise.reject("E_READ", e);   // 失败回传
    }
}

JS 侧调用 NativeModules.FileModule.readFile(path) 得到 Promise,await 后拿到字符串内容。注意:promise.resolve 只能调用一次,重复调用会被忽略;参数中的 path 必须是字符串,若传入对象会在序列化阶段被拒绝或丢失。

三、新架构下的通信方式差异

旧架构的桥接是“全异步、批量、JSON 序列化”的,存在两个明显问题:一是所有通信都要经过序列化,大数据量时开销大;二是无法同步调用,某些需要立即返回值的场景(如读取布局尺寸)只能绕路。新架构(Fabric + TurboModules + JSI)对此做了根本性改变。

JSI(JavaScript Interface)允许 JS 引擎直接持有 C++ 对象的引用,JS 可以同步调用 C++ 方法,不再强制经过 JSON 序列化。TurboModules 基于 JSI 实现,原生模块的方法通过 HostObject 暴露给 JS,调用时参数以 JSI Value 形式传递,避免了字符串化。Fabric 渲染器同样通过 JSI 与 C++ 层通信,UI 更新可以更细粒度、更同步。简单说:旧架构是“消息队列 + 序列化”,新架构是“直接对象引用 + 按需序列化”。

对比维度旧架构(Bridge)新架构(JSI / TurboModules)
调用方式异步消息,全部排队可同步、可异步,直接调用
数据传递JSON 序列化,类型受限JSI Value,支持更多类型
性能开销序列化 + 队列调度减少序列化,按需转换
模块加载启动时全量注册按需懒加载
线程模型JS 线程与原生线程分离仍分离,但可同步等待

四、实际开发中的判断依据

理解通信原理后,编写原生模块时有几个可操作的判断点。第一,参数和返回值尽量使用桥接支持的基础类型;需要传复杂对象时,先转成普通对象或 JSON 字符串,并在原生侧按约定解析。第二,耗时操作不要放在原生主线程执行,否则会阻塞 UI;Android 可用 Promise 配合线程池,iOS 可用 dispatch_async。第三,如果模块需要频繁调用(如每帧读取传感器数据),旧架构下应合并调用、减少消息数量,新架构下则可考虑用 JSI 直接暴露同步接口。

判断一个原生模块是否“桥接友好”,可以看它的方法是否只依赖基础类型、是否异步返回、是否避免在主线程做重活。满足这三点,旧架构下也能稳定运行。

最后需要明确:无论新旧架构,JS 与原生始终是两个运行时,跨边界调用都有成本。桥接机制的设计目标不是消除边界,而是让边界上的数据传递尽可能可控、可预期。掌握序列化规则、异步模型和回调 ID 机制,就能在调试原生模块时快速定位“参数丢失”“回调不触发”“主线程卡顿”这类典型问题。