2.2 新架构:JSI、Fabric 与 TurboModules


2.2 新架构:JSI、Fabric 与 TurboModules

本节摘要:新架构由三件套组成——JSI 提供同步直调的通信底座,Fabric 重建统一渲染管线,TurboModules 让原生模块按需加载并类型化对接。三者的共同目标是把 2.1 节诊断出的「排队与翻译税」逐项清零。本节用新旧路径对照讲清每件设计,并演示一份类型化模块声明如何被代码生成器变成双端胶水代码。学完后你将具备第 7 章动手写原生模块的全部概念准备。

回顾换代前的最后一根稻草

承接上一节的病理清单,把时间拨回旧架构的全盛期。那时社区对消息桥的抱怨已经高度模式化:长列表滚动掉帧、手势跟手性差、启动时模块全量注册拖慢首屏。团队做过各种外科手术——批量合并、数据拍平、事件节流——都是绕着桥修路,而不是修桥本身。真正的转机来自一个设计反思:JavaScript 与原生之间为什么一定要隔着一层「异步 + 序列化 + 排队」的通用消息协议?如果 JavaScript 引擎能直接持有原生对象的引用、像调用本地函数一样调用它们,翻译税不就天然消失了吗?这个反思催生了 JSI,也牵出了整套新架构。

JSI 是一座用 C++ 写成的薄接口层,名字里的「Interface」就是它的全部野心:定义一套与具体引擎无关的调用约定,让 JavaScript 能持有「宿主对象」的引用并同步调用其方法。注意「同步」二字的分量——旧架构里「调用原生、等结果」是一场跨线程的异步信件往来,JSI 里它变成了函数调用。引擎也从「被绑死」中解放出来:旧架构与 JavaScriptCore 深度耦合,JSI 让 Hermes、V8 等引擎可以即插即换,0.70 起 Hermes 成为默认正是踩在这块地基上。

新旧更新路径对照

把「用户点了一下按钮、界面变了个颜色」这条最小链路在两代架构下的路径并排画出,差异一目了然。

图:一次组件更新的两代路径对比

图:一次组件更新的两代路径对比

三件套各自的岗位职责

JSI 负责通信。 它把「跨语言调用」从协议降级为函数调用,是整个新架构的地基。对业务开发的直接体感是:手势驱动的界面反馈不再被队列延迟,动画参数同步传递成为可能。

Fabric 负责渲染。 它用 C++ 维护一棵统一的影子树(界面结构的权威副本),布局计算在这棵树上进行,双端共享同一套算法——这也意味着布局层面的两端差异从「实现不同」收敛为「策略不同」。更新的提交按优先级调度:手势跟随这类高优先级任务可以同步完成,数据刷新这类低优先级任务可以让路,渲染管线第一次有了「红绿灯」。

TurboModules 负责模块。 旧架构启动时注册全部原生模块,应用越大启动越慢;TurboModules 改为按需初始化,并且模块方法经由 JSI 直调、接口由类型声明驱动。三个模块三种职责,共同指向 2.1 节病理单上的每一项。

新架构下的心智迁移

从旧架构转来的工程师,有几处心智要主动换代。从「异步必然」到「按需同步」:旧时代所有跨语言调用默认异步排队,新架构下同步调用成为可能但不是默认——同步调用会阻塞调用方线程直到返回,轻量查询用它,耗时操作仍然老老实实走异步,滥用同步等于把旧桥的拥堵搬进新世界。从「消息 payload」到「类型合同」:旧时代传什么全凭两端默契,新架构的类型声明由代码生成强制兑现,「传了对方看不懂的东西」从运行时事故变成编译期报错。从「启动注册一切」到「按需加载」:模块不再全员报到,副作用要考虑「首次调用时才初始化」的时序——依赖模块构造时完成的逻辑(比如提前注册系统监听)需要显式处理初始化时机。

这些迁移不是语法变化,是「默认值」的变化:旧架构的默认是保守(异步、松散、全量),新架构的默认是高效(可选同步、强类型、按需),高效同时意味着把责任交还给工程师——判断调用该同步还是异步、类型是否声明准确、初始化时机是否正确。理解了这层责任转移,新架构就不是「多了一些新概念」,而是「同一些问题的更锋利的答案」。

动手:一份类型化模块声明

新架构下写原生模块,第一步不再是直接写双端代码,而是先用 TypeScript 声明接口——代码生成器会依据这份声明自动产生两端的胶水层,类型不匹配在编译期就会暴露:

// 模块接口声明:双端原生实现的「合同」 import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { // 异步方法:返回 Promise,适合耗时操作 getDeviceName(): Promise<string>; // 同步方法:经 JSI 直调,适合轻量即时调用 isTablet(): boolean; // 事件常量:原生侧主动推送给 JS 侧的数据 readonly BATTERY_LEVEL_EVENT: string; } export default TurboModuleRegistry.getEnforcing<Spec>('DeviceToolkit');
// JS 侧使用:与调用本地模块无异,类型全程可推断 import DeviceToolkit from './DeviceToolkit'; async function showDeviceInfo() { // 同步直调:立即返回 if (DeviceToolkit.isTablet()) { console.log('平板布局'); } // 异步调用:Promise 化 const name = await DeviceToolkit.getDeviceName(); console.log('设备名称:' + name); }

对照旧架构,同样的模块要手写双端注册表、手维护方法名到两端实现的映射,方法名拼错要到运行时才炸。声明驱动的价值不只是省代码,更是把「接口正确性」从运行时风险变成编译期约束——这是大型团队协作里的隐性红利。

完整案例:旧模块的换装之旅

背景:某团队的扫码模块是旧式原生模块,启动全量注册,方法名靠文档约定,双端各有一套实现,历史上出过两次「双端方法名不一致」的低级事故。操作:先按上面的形式补写接口声明;再逐一核对双端实现与声明的签名一致性,发现一处 Android 侧参数顺序与文档相反但恰好被调用方容错掩盖的隐患;然后把注册方式迁移为按需加载,模块的初始化时机从启动推迟到首次扫码调用;最后在双端真机回归扫码全链路,重点验证权限拒绝、取消、成功三类出口。结果:启动路径上少了一个模块的初始化开销,类型声明让那处参数顺序隐患在核对阶段现形。解读:迁移的真正收益是「合同化」——接口声明成为双端与 JS 三方共同的对账单。变式:如果模块涉及高频事件流(相机帧数据),还可以进一步评估是否走 JSI 宿主对象直调,把每帧的跨语言开销也压掉,这一步的实操细节在第 7 章展开。

本节要点回顾

  • JSI 是地基:把跨语言调用从异步协议变成同步函数调用,序列化税与排队延迟一并消失;
  • Fabric 是渲染红绿灯:统一影子树加优先级调度,跟手性由调度保证;
  • TurboModules 是按需仓库:模块延迟到首次使用时装载,启动负担随用随付;
  • 声明驱动开发:TypeScript 接口即合同,代码生成产出双端胶水,错误前移到编译期;
  • 病理不会清零但阈值抬高:线程视角的诊断思路在新架构下依然有效。

架构讲完了「怎么跑」,下一节讲「何时醒」:从点击图标到首屏亮起的启动全程,以及双端生命周期事件的对齐手册。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U