想象一家跨国科技公司有两个分部:JS 分部只讲 JavaScript 语,负责界面逻辑与业务编排;原生分部只讲 Java/Objective-C 语,掌握摄像头、定位、文件系统这些“硬技能”。两个分部隔着大洋,既不能直接对话,也不能共享办公室,所有协作都只能通过公司内部唯一的“桥接邮局”——Bridge——传递标准化信件(JSON 消息)。这套邮局系统的效率,直接决定了整个 App 的响应速度。后来,管理层先后推出了“现代化快递分拣中心”(TurboModule)和“直拨专线电话”(JSI)两轮改革。本文就用这个“两地通信”的比喻,把 React Native 与原生端的通信事件机制讲个明白。
理解 RN 与原生端的通信机制,是跨平台开发和性能优化的核心技能,也是 AI 辅助移动端工程落地时绕不开的基础知识。
可视化演示
演示RN与原生端的通信机制:JS Bridge序列化消息传递、事件订阅模式与JSI直连的延迟差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
JS Bridge序列化 JS调用 序列化 JSON Bridge 消息经桥接序列化传递
Bridge 桥接机制
想象一家跨国公司的两个分部:JS 分部只讲 JavaScript 语,负责界面逻辑和业务编排;原生分部只讲 Java/Objective-C 语,掌管摄像头、定位、文件系统等“硬技能”。两个分部位于大洋两岸,既不能直接对话,也不能共享办公室,一切协作都只能通过公司内部唯一的“桥接邮局”——Bridge——来完成。邮局的吞吐效率,就是两个分部协作效率的天花板。
在 React Native 的架构里,Bridge 就是这座邮局。JS 跑在一个独立的 JavaScriptCore(或 Hermes)运行时里,原生代码跑在各自平台的运行时里,两者完全隔离,互不感知。任何一次跨端请求,都必须先写成一封“标准信件”——一个 JSON 格式的消息对象——才能投递给对方。这套设计让两端彻底解耦、互不阻塞,但也意味着每一次协作都要走完整的“写信—寄信—收信”流程,产生肉眼不可见但真实存在的时延。
Bridge 自 2015 年随 RN 发布以来,一直维护着 JS 调用原生的消息队列、原生回调 JS 的队列以及事件消息队列。所有消息都被序列化为 JSON 字符串,通过桥接线程异步搬运。端到端总时延可以写成四个环节的叠加:
这座“邮局”奠定了 RN 的一切能力基础,却也埋下了性能隐患。后续所有架构演进,本质上都是在优化这四个环节中的某一个。
- T_serialize(序列化时延)JS 侧把调用参数编码成 JSON 字符串所耗费的时间。
- T_queue(排队时延)消息在桥接队列中等待批量发送的时间。
- T_transfer(传输时延)桥接线程把消息搬运到原生侧的时间。
- T_deserialize(反序列化时延)原生侧读取 JSON 字符串、恢复出原始参数结构的时间。
消息传递流程
现在把镜头拉近,看一封“跨洋信件”从 JS 分部寄到原生分部的完整旅程。写信人(JS 代码)要先确认三件事:寄给哪个部门(模块 ID)、办什么业务(方法 ID)、附上什么材料(参数)。邮局工作人员把这些信息严格填入标准表单,投进待发邮箱。
在代码层面,当 JS 调用原生模块方法时,React Native 的 MessageQueue 并不会立即把请求发出去,而是先构造一条消息:模块 ID + 方法 ID + 参数 + 回调 ID,然后推入 JS 侧消息队列。队列不会逐条投递,而是攒到一定数量或到达特定时机后,才批量打包发送给原生线程。这个过程在通信事件机制里叫“队列批量发送”。
原生端收到一批消息后,逐条反序列化并执行相应的原生方法;执行完毕,把结果连同回调 ID 一起回传到 JS 侧队列,JS 侧根据回调 ID 找到最初的调用点,触发回调或 resolve Promise。消息在队列里堆积得越多,整体等待越久:
就像发往同一城市的信件会凑成一麻袋统一装车,批量确实节省了运输成本;但一旦赶上“双十一”,分拣中心信件堆积成山,每一封的等待时间反而暴涨。RN 开发者遇到的许多卡顿,根源往往不是原生执行得慢,而是“邮局门口堵车了”。
- N(消息条数)当前堆在队列里的等待消息数量。
- S(单条消息大小)一条消息经 JSON 序列化后的字节数。
- B(桥接吞吐带宽)桥接线程单位时间能搬运的字节量。
事件订阅机制
上面的流程都是 JS 主动“寄信”给原生。但原生想主动通知 JS 怎么办?比如网络从 Wi-Fi 切到 5G,或后台下载完成——总不能等 JS 每隔一秒“查岗”。原生分部于是建了一座“广播电台”:它发射信号(事件名 + 数据),JS 分部只需要订阅收听即可。
这套广播收听的机制,在 RN 里由 NativeEventEmitter 和 DeviceEventEmitter 提供。原生模块像电台一样广播事件,JS 端通过 addListener 注册监听回调。当事件消息通过桥接到达 JS 侧时,RN 根据事件名查表,找到对应的监听器列表并逐个调用回调。这就是事件订阅模式的核心。
事件机制有两个坑。第一,事件消息同样要走 Bridge,仍然有序列化和排队开销;第二,JS 端必须手动维护监听生命周期。组件卸载时若忘记 removeListener,监听器会继续残留在事件表里,形成“幽灵订阅”——这是内存泄漏的经典来源:
这就像搬家后忘了向广播电台退订,电台依然每天往旧地址寄节目单。回调对象永远不会被回收,App 越用越臃肿,最终被系统“制裁”。记住通信事件机制的铁律:注册与释放,永远成对出现。
- N_total(监听器总数)事件表中某个事件名对应的全部监听器条目。
- N_active(活跃监听数)组件仍存活、需要正常接收事件回调的监听器。
- N_leaked(泄漏监听数)组件卸载后未被移除、残留在事件表中的监听器。
通信性能瓶颈
为什么老一代 RN 在列表快速滚动或频繁交互时会明显卡顿?回到邮局场景:每一封信都要手工誊写(序列化),高峰期排长队(队列积压),邮路还只有一条(单线程桥接)。业务量小的时候游刃有余,流量一大,瓶颈立刻现形。
从工程角度,传统 Bridge 的延迟主要来自四个方面。一是序列化开销随消息体量线性增长;二是高频小消息让队列频繁批处理,排队时延被不断拉高;三是单条大消息会长时间独占桥接线程;四是 JS 线程与原生线程之间的切换本身也有成本。其中序列化开销是最大的元凶。
把 JS 对象转成 JSON 字符串、再把字符串解析回原生对象,这个过程的 CPU 消耗与负载大小成正比:
传一个 10 字节的小参数,开销只有几十微秒;但若把整张图片的 base64(几百 KB)塞进桥,一次序列化就能让 UI 线程明显掉帧。这正像邮局里每封信都要称重贴票、录入系统,碰到一个“巨大的包裹”,整个柜台都要被占用很久。于是设计师开始思考:与其让所有业务挤在一条邮路上,不如给关键部门装一部“直拨电话”。
- α(单位开销系数)每字节负载消耗的 CPU 时间,取决于引擎与平台实现。
- L(负载大小)传入数据的规模,比如字符串长度或对象包含的字段数。
- β(固定启动成本)创建 JSON 容器等固定开销,与负载大小无关。
TurboModule 改进
面对邮局拥堵,管理层推出的第一轮改革叫 TurboModule——相当于把旧邮局升级成现代化快递分拣中心:寄件人不再手写地址,而是用电子面单(接口定义)自动录入;不再每次都在柜台翻箱倒柜找收件人,而是事先建好客户档案(模块常驻引用)。
TurboModule 是 RN 新架构的四大支柱之一。旧版 Bridge 中,JS 每次调用原生模块,都要在模块表里动态查找、组装消息、装箱拆箱。TurboModule 则通过 Codegen 代码生成器,在构建期就根据接口定义生成类型安全的胶水代码,JS 端持有原生模块的常驻引用,调用时直接进入原生方法,省去了大量动态查找和类型转换操作。
结合前面的时延模型,TurboModule 的收益可以这样量化:
TurboModule 还支持懒加载:JS 端第一次真正用到某个模块时才初始化,App 启动时不再把几百个原生模块全部加载一遍。从旧邮局到现代化快递中心,TurboModule 确实让“寄件”快了不少,但它仍然没有根治问题——信件该序列化还是序列化,该排队还是排队。真正釜底抽薪的,是下一轮通信架构的革命。
- T_legacy(传统调用时延)旧 Bridge 下一次完整调用的总耗时。
- T_turbo(Turbo 调用时延)TurboModule 下同一次调用的总耗时。
- η(时延下降比例)优化带来的相对提升,η 越大,收益越明显。
JSI 新架构
如果说传统 Bridge 是邮局,那么 JSI(JavaScript Interface,JavaScript 接口层)就是两个分部之间拉起的“直拨专线”。不需要写信,不需要寄件,不需要邮车——两个部门的人拿起电话就能直接通话,甚至可以把同一份文件同时摊在两张桌子上看。
JSI 是 RN 新架构的底层基石,取代了旧的 Bridge 通信通道。它提供了一层轻量级 C++ 接口,让 JavaScript 引擎可以直接持有原生 C++ 对象的引用,并直接调用其方法。对象引用直接传递,不再需要 JSON 序列化和反序列化;调用既支持异步,也支持同步执行。
在 JSI 模式下,前文的时延模型被大幅改写:
从“跨洋寄信”到“专线通话”,JSI 让 JS 与原生不再是两个只能靠邮局沟通的孤岛,而更像是同一间办公室里随时可以转头讨论的同事。当然,专线电话需要铺设光缆——JSI 引入了大量 C++ 代码,性能提升背后是复杂度的上升。理解这层取舍,才是真正的架构智慧。
- T_call(直调开销)JSI 下调用经接口直接进入 C++ 方法,仅剩函数调用本身的成本。
- T_serialize → 0(序列化归零)对象引用直接传递,JSON 打包与解析开销趋近于零。
AI 应用场景
跨平台 App 通信瓶颈优化
阶段:性能诊断与优化
解决的问题:传统 Bridge 下,高频桥接调用导致队列积压、UI 掉帧。人工打点分析通信日志,既耗时又难定位根因。AI 可以自动分析调用日志,识别高频模块访问、超大负载消息和异常排队时延,并给出合并调用、迁移到 TurboModule/JSI 等优化建议。
具体案例:某电商 App 在商品列表页滚动时频繁调用原生模块读取缓存,AI 分析日志后发现每秒有数百条小消息过桥,建议将多次小调用合并为一次批量快照读取,修复后页面帧率从 42fps 提升到 58fps。
AI 辅助生成 TurboModule 胶水代码
阶段:AI 辅助代码生成
解决的问题:手写 TurboModule 需要同时维护 JS 接口、原生实现和 Codegen 配置三处代码,还要保证类型对齐,繁琐且易错。AI 可以根据原生模块的接口定义,自动生成类型安全的 JS 调用包装器和原生侧胶水代码,并补全异常处理。
具体案例:开发者提供一个 Objective-C 方法定义,AI 生成对应的 TurboModule 接口声明、JS 封装函数和 C++ 桥接代码,随后直接通过类型检查并跑通调用链路。
NativeEventEmitter 泄漏检测
阶段:可观测性与稳定性分析
解决的问题:事件监听器未成对释放导致的内存泄漏很难肉眼排查。AI 对 addListener / removeListener 的调用日志做序列匹配分析,自动标记未配对的监听器,并定位到泄漏页面与组件。
具体案例:在长列表页面反复进出 20 次后,AI 检测到“网络状态”事件监听器数量持续增长,定位到某个弹窗组件未在卸载时清理监听,修复后内存占用回落 12%。
Python 演示
# -*- coding: utf-8 -*-# 简化模拟: RN Bridge 的消息队列批量发送与事件订阅/释放import jsonfrom collections import deque class BridgeSimulator: """模拟传统 Bridge: 消息批量入队/批量发送, 支持事件订阅与释放""" def __init__(self, batch_size=5): self.call_queue = deque() # 待发送的调用消息队列 self.listeners_map = {} # 事件名 -> 回调函数列表 self.batch_size = batch_size # 每次批量发送的消息条数 self.next_callback_id = 0 # 回调 ID 自增器 self.total_calls = 0 # 累计调用计数 def call_native(self, module_id, method_id, args, callback): """JS 端发起调用: 序列化后入队, callback 等待原生回传结果""" msg = { "module_id": module_id, "method_id": method_id, "args": args, "callback_id": self.next_callback_id, } self.call_queue.append(json.dumps(msg)) # 模拟 JSON 序列化 self.next_callback_id += 1 self.total_calls += 1 callback("ok") def flush(self): """原生端批量取出消息并反序列化, 模拟批量发送""" batch = [] while self.call_queue and len(batch) < self.batch_size: raw = self.call_queue.popleft() batch.append(json.loads(raw)) # 模拟反序列化 return batch def emit_event(self, event_name, data): """原生端广播事件: 给所有订阅者投递消息, 模拟 NativeEventEmitter""" if event_name not in self.listeners_map: return payload = json.dumps({"event": event_name, "data": data}) for callback in list(self.listeners_map[event_name]): callback(payload) def add_listener(self, event_name, callback): """JS 端添加监听, 等价 NativeEventEmitter.addListener""" self.listeners_map.setdefault(event_name, []).append(callback) def remove_listener(self, event_name, callback): """JS 端移除监听, 等价 NativeEventEmitter.removeListener""" if event_name in self.listeners_map: self.listeners_map[event_name].remove(callback) def on_network_change(raw): msg = json.loads(raw) print("收到事件: {} -> {}".format(msg["event"], msg["data"])) def main(): bridge = BridgeSimulator(batch_size=3) # JS 端订阅事件 bridge.add_listener("networkChange", on_network_change) # JS 端发起两次原生调用 bridge.call_native("NetInfo", "getStatus", [], lambda r: print("回调:", r)) bridge.call_native("Camera", "open", {"mode": "photo"}, lambda r: print("回调:", r)) # 原生端批量处理队列 batch = bridge.flush() print("原生批量处理 {} 条消息:".format(len(batch))) for msg in batch: print(" - module={} method={}".format(msg["module_id"], msg["method_id"])) # 原生端主动广播事件 bridge.emit_event("networkChange", {"online": True, "type": "wifi"}) # JS 端取消订阅: 注册与释放必须成对 bridge.remove_listener("networkChange", on_network_change) bridge.emit_event("networkChange", {"online": False}) # 不再有任何输出 if __name__ == "__main__": main()
self.call_queue.append(json.dumps(msg))模拟 JS 端调用原生时,把消息序列化为 JSON 字符串后投入桥接队列。batch = bridge.flush()模拟原生端一次性从队列中批量取出多条消息进行处理。json.loads(raw)模拟原生端反序列化 JSON,恢复出模块 ID、方法 ID 和参数。bridge.add_listener("networkChange", on_network_change)等价于 RN 中 NativeEventEmitter 的 addListener,注册事件回调。self.listeners_map.setdefault(event_name, []).append(callback)按事件名维护监听器列表,事件到达时按列表分发。bridge.remove_listener("networkChange", on_network_change)模拟 removeListener 手动释放监听,防止泄漏。
常见误区
误区既然新架构 JSI 这么强,传统 Bridge 是不是马上就被完全废弃了?新项目完全不需要了解 Bridge 了。
正解JSI 确实是新架构的基石,但存量 RN 项目中大量旧原生模块仍通过兼容层(Interop Layer)以 Bridge 的异步消息模型工作。Bridge 的“队列批量 + JSON 序列化”思想也仍是理解 RN 通信事件机制的基础。理解两者能共存,才算真正读懂 RN 架构演进。
为何常见:技术文章喜欢强调 JSI 的“颠覆性”,让读者误以为新旧架构是瞬间切换,忽略了渐进兼容的现实。
误区JS 调用原生方法后,消息会像网络请求一样立即逐条到达原生端,实时性很强。
正解传统 Bridge 的消息是批量异步发送的。JS 端把消息放进队列,由 MessageQueue 攒够一批或在特定时机才统一发送。这也是为什么高频小调用会带来明显的排队时延。
为何常见:现代通信给我们的直觉是“发出即到达”,很少有人意识到 RN 桥接内部存在队列攒批的机制。
误区JS 调用原生方法后,JS 线程会傻等结果返回,期间 UI 卡住不动。
正解传统 Bridge 是异步的,JS 调用后立刻继续执行,结果通过回调或 Promise 在原生处理完后送回。只有 JSI 才提供同步调用能力,且那是新架构的主动选择,而不是桥接的默认行为。
为何常见:把 JSI 的同步能力错误地投射到传统 Bridge 上,或者把某些同步 API 的体验当成了底层机制。
误区addListener 注册的监听器,组件卸载后 RN 会像浏览器卸载页面一样自动帮忙清掉。
正解RN 不会自动移除监听。必须在组件卸载(componentWillUnmount 或 useEffect cleanup)中手动调用 removeListener / removeAllListeners,否则监听器残留在事件表里造成内存泄漏。
为何常见:前端开发者习惯了浏览器事件监听随页面销毁自动回收,迁移到 RN 时容易沿用错误的心智模型。
误区TurboModule 就是把旧模块换了个名字,接口看起来差不多,性能提升是宣传话术。
正解TurboModule 通过 Codegen 生成类型安全接口、懒加载模块并缓存常驻引用,消除了旧 Bridge 每次调用都进行的全局动态查找和装箱拆箱,调用时延普遍下降 40% 以上。名字没变的背后,调用链路已经完全不同。
为何常见:从外部 API 看新旧写法差异不大,只有深入序列化和调用链路才能感受到内部实现的质变。
面试题
Q1. RN 传统架构中,JS 与原生通信的主要通道是?
- A.HTTP 请求通道
- B.Bridge 消息桥接← 正确答案
- C.WebSocket 通道
- D.文件读写通道
答案:B。Bridge 是 RN 中 JS 与原生之间的唯一消息通道,像“跨洋邮局”一样承担全部跨端通信。HTTP、WebSocket 是网络层能力,文件读写则与跨端调用无关。
Q2. Bridge 消息采用哪种序列化格式?
- A.可扩展标记语言格式
- B.轻量级数据交换格式← 正确答案
- C.紧凑型二进制数据格式
- D.纯文本自定义传输格式
答案:B。Bridge 消息使用 JSON(JavaScript Object Notation)序列化,也就是题干中的“轻量级数据交换格式”。它跨语言、易读写,但也因此带来序列化与反序列化的 CPU 开销。
Q3. 传统 Bridge 中 JS 调用原生方法的发送方式是?
- A.实时逐条立即发送
- B.队列批量异步发送← 正确答案
- C.同步直连等待返回
- D.共享内存直接写入
答案:B。传统 Bridge 的消息通过 MessageQueue 攒批后统一发送,属于队列批量异步模型。实时逐条、同步直连、共享内存都不是传统桥接的行为,后两者是 JSI 时代才具备的能力。
Q4. 关于 NativeEventEmitter 的事件订阅机制,下列说法正确的是?
- A.组件卸载时监听自动清理
- B.注册监听函数接收事件← 正确答案
- C.事件监听器需要手动释放← 正确答案
- D.事件名称必须保证全局唯一
答案:BC。addListener 负责注册监听,removeListener 负责手动释放,二者必须成对出现,因此 B、C 正确。RN 不会在组件卸载时自动清理监听,A 错误;不同原生模块可以发出同名事件,事件名并不要求全局唯一,D 错误。
Q5. 传统 Bridge 通信中导致延迟的主要架构原因是?
- A.消息反复序列化与排队← 正确答案
- B.原生系统 API 接口太少
- C.JS 引擎运行速度过慢
- D.网络请求带宽不够用
答案:A。传统 Bridge 每次通信都要经历 JSON 序列化、队列排队、线程切换与反序列化,这正是正文时延模型的四个环节。API 数量、JS 引擎速度、网络带宽都不是桥接延迟的核心来源。
Q6. RN 新架构中 JSI 的中文全称是?
- A.通用标准交互接口
- B.脚本引擎运行接口
- C.原生交互接口层← 正确答案
- D.序列化通信接口
答案:C。JSI 的全称是 JavaScript Interface,即 JavaScript 与原生 C++ 对象之间交互的接口层。它让 JS 可以直接持有并调用原生对象,免去 JSON 序列化,是“直拨专线”式的能力接口。
Q7. TurboModule 带来哪些具体改进?
- A.代码生成器产出安全接口← 正确答案
- B.模块懒加载减少启动成本← 正确答案
- C.常驻引用消除重复查找← 正确答案
- D.完全移除消息队列传参
答案:ABC。TurboModule 通过 Codegen 生成类型安全接口、支持模块懒加载、以常驻引用消除每次调用的动态查找,因此 A、B、C 正确。它大幅减少了序列化和队列传参,但并未完全移除消息队列,D 过于绝对。
Q8. JSI 相比传统 Bridge 的核心突破是什么?
- A.增加消息队列长度上限
- B.免序列化直接调用原生← 正确答案
- C.改用二进制消息提速
- D.编译原生代码为 JS
答案:B。JSI 的核心突破是让 JS 直接持有原生对象引用并调用方法,序列化开销趋近于零。它改变了通信模型本身,而不是在原有模型上做参数调整,更不是把原生代码编译成 JS。
Q9. 移除 NativeEventEmitter 监听的主要作用是避免什么?
- A.事件名冲突导致分发错乱
- B.监听器残留造成内存泄漏← 正确答案
- C.桥接队列消息重复发送
- D.原生模块重复初始化
答案:B。未移除的监听器会残留在事件表中,形成“幽灵订阅”,导致回调对象无法被回收、内存持续增长。这正是 removeListener 必须与 addListener 成对出现的原因。事件名冲突、消息重复发送、模块重复初始化都与监听释放无关。
Q10. 关于 JSI 调用能力的说法正确的是?
- A.JS 可以同步调用原生函数← 正确答案
- B.原生对象引用可被 JS 直接持有← 正确答案
- C.消息仍必须经 JSON 序列化
- D.C++ 对象可与 JS 共享内存← 正确答案
答案:ABD。JSI 支持同步与异步两种调用方式,JS 可以直接持有 C++ 对象引用,并实现内存级共享,因此 A、B、D 正确。JSI 的最大突破恰恰是取消了 JSON 序列化,C 描述的是传统 Bridge 的模型,与 JSI 相反。