RN 的新模块系统 Turbo module
本文所有代码如果没有特别标注的话,默认用的都是 v0.76.0 的 RN 代码
TurboModule 是什么
要讲清楚这个问题,我们要先从 bridge 架构的 Native module 系统开始说起,旧架构的 Native module 系统的调用流程大概是这样的:
JS code
│
│ UIManager.dispatchViewManagerCommand(tag, commandID, params)
▼
+----------------------+
| JS UIManager proxy |
| (generated function) |
+----------------------+
│
│ enqueueNativeCall(moduleID, methodID, args)
▼
+----------------------+
| BatchedBridge |
| call queue |
+----------------------+
│
│ batch + flush
▼
+----------------------+
| Bridge payload |
| [moduleIDs] |
| [methodIDs] |
| [params] |
+----------------------+
│
│ pass to native
▼
+----------------------+
| Native bridge side |
+----------------------+
│
├─ lookup module by moduleID
├─ lookup method by methodID
└─ convert args
▼
+----------------------+
| Native UIManager |
+----------------------+
│
├─ resolve reactTag
├─ resolve command
└─ dispatch
▼
+----------------------+
| ViewManager / View |
+----------------------+
│
▼
Actual native UI effect
首先 JS 在调用的时候需要知道有哪些模块,以及对应模块的方法,这些资讯是由宿主侧在初始化阶段注入的
当 JS 调用 Native module 时,JS 还有一层代理(proxy)层将模块方法的调用统一转换成 enqueueNativeCall 方法的调用
当这个方法进入 JS 侧 bridge 的时候,会被打包、序列化后传送给宿主侧的 bridge 由这边的 bridge 进行反序列化、解析后调用对应的原生模块方法;如果这个原生模块的方法存在回调,还需要把这个流程反过来一次,然后找到对应的 callID(JS 调用原生模块方法的编号)拿到对应的 callback 执行
这种调用模式会有什么问题呢?我们用一个表格来看看:
| 旧 Native Module 的架构问题 | 问题描述 | TurboModule 怎么修复 |
|---|---|---|
| 以 Bridge 为中心 | 模块系统依赖跨桥消息模型,能力受限(强制异步、对高频/小细粒度调度不友好等等) | 改为 JSI binding 为中心,使得模块可以像 JS runtime 中的本地对象能力被调用 |
| 接口契约松散 | 原生模块缺乏单一数据源约束,JS/Android/IOS 各自实现,三者依靠文档、约定、测试保持一致,长期容易造成平台不一致、重构风险高、团队协作成本上升等问题 | 用 spec 来约束类型 + Codegen 根据类型约束生成 C++/Android/IOS 侧脚手架文件强化契约,建立单一数据源 |
| 生命周期治理弱 | 旧原生模块系统虽然也支持懒加载,但它的生命周期依赖 bridge 上下文驱动,生命周期管理边界也十分分散 | 由 TurboModuleManager 统一按需创建和管理支持两层缓存:模块实例、JS 方法属性按需加载 |
| UI 模块和普通原生能力混杂 | 旧 Native Module 既承载普通能力模块,也承载像 UIManager 这类 UI 基础设施,导致 “能力模块” 和 “渲染/视图系统” 职责混杂,抽象层次不清晰 | TurboModule 负责普通原生能力;Fabric 负责 UI/视图系统,从架构上完成职责拆分 |
基于这些区别,现在可以回答本章节标题的问题了:TurboModule system 是 RN 新架构中面向非 UI 原生能力的模块系统,它以 JS runtime 为中心,替代框架中以 bridge 为中心的 Native Module system;并通过更清晰的模块边界、按需创建和更强的接口契约支持 RN 架构长期演进
设计方案
新旧模块兼容方案
在进入架构设计前,我们需要先了解一些背景知识:
新的 TurboModule 不仅重写了整个 NativeModule 系统,还更改了之前的调用方式
在 Bridge 时代,NativeModule 的调用是这样子的:
import { NativeModules } from 'react-native';
NativeModules.Vibration.vibrate(500);
但是在 TurboModule 中,调用方式变成了:
import { TurboModuleRegistry } from 'react-native';
const NativeVibration = TurboModuleRegistry.getEnforcing('Vibration')
NativeVibration.vibrate(500)
个人猜测改变调用方式的动机有两个:
- 相比于之前分散的模块,新的方法按模块名从 TurboModule 系统取,更适合“按名称查找”和“按需解析”的新语义,且加强了原生模块之间的内聚性
- 考虑到原生模块的迁移成本,通过
TurboModuleRegistry能更好的适配旧的原生模块调用方式,使得旧模块也能在新架构上继续使用
特别是第二点,RN 为了让新架构也能兼容旧写法的 Native module,设计了如下的兼容流程:
![]()
说明一下:
- 当我们在新架构调用了
Vibration.vibrate(500)方法后,会进入到对应的 spec 文件中 - spec 文件就是 typescript 编写的文件,它声明了原生模块的类型,并且会暴露一个
TurboModuleRegistry.getEnforcing('Vibration')方法的返回值 - 在
getEnforcing方法中,会判断当前的模块是否为 TurboModule - 【接步骤 3】如果是,则通过原生平台的
TurboModuleProvider模块去查找对应实现(对应图中最右边的橘色正方形) - 【接步骤 3】如果不是,则要先判断是否要启用兼容逻辑(判断依据是
useLegacyNativeModuleInterop是否为 true,只有当前 RN 在 bridge 模式或者原生平台提供了LegacyModuleProvider才会返回 true) - 【接步骤 5】如果不启用则直接返回 null,因为没有找到对应原生模块
- 【接步骤 5】如果启用了,就判断原生平台是否提供
LegacyModuleProvider,如果有则走LegacyModuleProvider的逻辑 - 【接步骤 7】如果没有就回退到 bridge 的
enqueueNativeCall解决方案(对应到我们开篇的流程图)
至于 TurboModuleProvider 与 LegacyModuleProvider 分别做了什么,以及 RN 是如何管理模块的生命周期,我们可以来看看下个章节的架构设计~
调用链路
由于 LegacyModuleProvider 与 TurboModuleProvider 基本类似,区别在于 LegacyModuleProvider 需要兼容之前基于 Bridge 的原生模块,所以我们重点看一下新的 TurboModule 调用链路:
![]()
TurboModule 的调用链路中总共有两个主要角色:Js runtime 以及 C++ 中的 TurboModuleBinding
看过本专栏 JSI 文章的读者应该知道,在新架构中的 XXXbinding 一般就是负责把各种能力挂到 global 对象上
整个调用链路分为两个部分:初始化阶段、调用阶段
在 RN 应用初始化过程中,会调用 TurboModuleBinding::install 方法,这个方法会做两件事:
- 往 global 挂
__turboModuleProxy属性,这是一个 C++ 侧的方法,也是 JS 调用 turboModule 的入口 - 如果当前是 bridgeless 模式(也就是使用 JSI),会往 global 挂
nativeModuleProxy属性,这也是一个 C++ 侧的方法,负责兼容老架构的原生模块
至此,初始化阶段就完成了,接下来进入调用阶段
调用的发起点是 JS 侧(对应图中左侧 JS runtime),总共分为两步:
- 获得 TurboModule 对应 JS 对象的引用(对应到图中左侧的
jsRepresentation,为了方便理解我们把它赋值给了NativeVibration这个更加语义化的变量) - 从该引用取得方法然后调用
首先是第一步获取 TurboModule 对应 JS 对象的引用,我们可以通过 global.__turboModuleProxy 取得,这是一个可以通过 JSI 调用的 C++ 方法,最后会调用 TurboModuleBinding::getModule 方法,该方法会做四件事:
- 创建一个 JS 对象
jsRepresentation,用来存放后续所需 TurboModule 的实例 - 去宿主平台的 TurboModuleManager(IOS、Android)取得对应 TurboModule 的实例
- 把 TurboModule 的实例放到
jsRepresentation.__proto__中,这是 TurboModule 系统实现模块按照方法/属性级别缓存的主要设计 - 最后,我们把
jsRepresentation返回给 JS runtime,让 JS 持有这个对象的引用以便后续直接调用其中的方法
以上的步骤只有第一次的时候需要,后续由于 JS 已经持有了 jsRepresentation 的引用,所以 JS 直接通过这个对象访问所需方法即可~
源码解析
在这个小节,我们来看看上个小节的初始化以及调用阶段的具体代码实现
首先是初始化阶段,我们先来看看 TurboModuleBinding::install 是怎么实现的:
// in packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp
// 这个方法会在应用初始化的时候被宿主平台各自实现的 TurboModuleManager 调用
// 目的是为了在 global 对象上挂载原生模块调用所需方法
void TurboModuleBinding::install(
jsi::Runtime &runtime, TurboModuleProviderFunctionType &&moduleProvider,
TurboModuleProviderFunctionType &&legacyModuleProvider,
std::shared_ptr<LongLivedObjectCollection> longLivedObjectCollection) {
// 挂载 __turboModuleProxy
runtime.global().setProperty(
runtime, "__turboModuleProxy",
// 这是一个 C++ 方法
jsi::Function::createFromHostFunction(
runtime, jsi::PropNameID::forAscii(runtime, "__turboModuleProxy"), 1,
// 传入了一个 lymbda,这个 lymbda 会在 JS 访问 global.__turboModuleProxy 时被调用
[binding = TurboModuleBinding(runtime, std::move(moduleProvider),
longLivedObjectCollection)](
jsi::Runtime &rt, const jsi::Value &thisVal,
const jsi::Value *args, size_t count) {
// 检查参数的代码
if (count < 1) {
throw std::invalid_argument(
"__turboModuleProxy must be called with at least 1 argument");
}
std::string moduleName = args[0].getString(rt).utf8(rt);
// 真正做事的方法
return binding.getModule(rt, moduleName);
}));
// 因为 0.76.0 版本还需要兼容之前的 bridge 模式,所以会用 RN$Bridgeless 来标识现在用的是 JSI 架构的代码
if (runtime.global().hasProperty(runtime, "RN$Bridgeless")) {
bool rnTurboInterop = legacyModuleProvider != nullptr;
auto turboModuleBinding =
legacyModuleProvider ? std::make_unique<TurboModuleBinding>(
runtime, std::move(legacyModuleProvider),
longLivedObjectCollection)
: nullptr;
auto nativeModuleProxy = std::make_shared<BridgelessNativeModuleProxy>(
std::move(turboModuleBinding));
defineReadOnlyGlobal(runtime, "RN$TurboInterop",
jsi::Value(rnTurboInterop));
// 主要代码,目的是挂载 nativeModuleProxy 到 global 对象上
defineReadOnlyGlobal(
runtime, "nativeModuleProxy",
jsi::Object::createFromHostObject(runtime, nativeModuleProxy));
}
}
接下来我们来看看 getModule 做了什么:
// in packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp
// 获得具体模块实现的方法,目标是最后返回一个带着模块实现的 JS 对象 jsRepresentation
jsi::Value TurboModuleBinding::getModule(jsi::Runtime &runtime,
const std::string &moduleName) const {
std::shared_ptr<TurboModule> module;
{
SystraceSection s("TurboModuleBinding::moduleProvider", "module",
moduleName);
// 调用双端实现的 TurboModuleProvider 来获取对应的模块实例
module = moduleProvider_(moduleName);
}
if (module) {
// 这里是第一层缓存
// 如果这不是第一次获取该模块实例,我们可以从模块中找到上一次返回的 jsRepresentation
// 这里直接返回即可,无需再创建一次 jsRepresentation 对象
auto &weakJsRepresentation = module->jsRepresentation_;
if (weakJsRepresentation) {
auto jsRepresentation = weakJsRepresentation->lock(runtime);
if (!jsRepresentation.isUndefined()) {
return jsRepresentation;
}
}
// 如果是第一次获取该模块实例,我们就需要先创建一个空的 jsRepresentation 对象
jsi::Object jsRepresentation(runtime);
weakJsRepresentation =
std::make_unique<jsi::WeakObject>(runtime, jsRepresentation);
// ⚠️ 核心代码
// jsRepresentation 解决的是 “实例属性的缓存问题”
// 具体做法是:
// 1. 第一次创建并返回的 jsRepresentation 是一个空对象,但是我们用原型链将其与模块实例关联起来
// 2. 如果我们第一次调用了该模块的方法,会因为当前对象为空而去原型链找,于是就进入了模块原型的 get 方法
// 3. 模块的 get 方法被触发会把对应的属性缓存在 jsRepresentation 的属性中
// 4. 这样一来,后面如果我们再次调用同样的方法,就可以直接在 jsRepresentation 的属性中查找到,不需要再进行原型链查找了
// 具体的实现等我们聊到 TurboModuleProvider 的实现会再进行分析
auto hostObject =
jsi::Object::createFromHostObject(runtime, std::move(module));
jsRepresentation.setProperty(runtime, "__proto__", std::move(hostObject));
// 把刚刚创建的对象返回给 JS
return jsRepresentation;
} else {
// 如果找不到对应的模块,直接返回 null
return jsi::Value::null();
}
}
接下来,我们来看看神秘的 TurboModuleProvider 都做了什么~
TurboModuleProvider 是由平台自行实现,所以会有两套实现,我们先来看看 IOS 的实现:
// in packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm
// 这个方法是 IOS 侧把 TurboModule 系统安装进 JS runtime 的入口
// 在这里 IOS 提供了 turboModuleProvider 以及 legacyModuleProvider 并在最后调用了上述 TurboModuleBinding::install 方法
- (void)installJSBindings:(facebook::jsi::Runtime &)runtime
{
// 创建 turboModuleProvider,可以看到这是一个 lymbda
// 当 getModule 需要查找模块的时候就会调用这个 lymbda
// 留意这里的返回值是一个 TurboModule,这个跟后续的 legacyModuleProvider 是一致的
// 也就是说,新旧原生模块的差异在这里被抹平了
auto turboModuleProvider = [self,
runtime = &runtime](const std::string &name) -> std::shared_ptr<react::TurboModule> {
auto moduleName = name.c_str();
// 性能埋点
TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName);
// 判断当前模块是否初始化了,如果没有的话也要埋个性能埋点
// 这里只记录 objc 实现的原生模块,因为 objc 原生模块的初始化过程比较复杂,这个我们后面会聊
auto moduleWasNotInitialized = ![self moduleIsInitialized:moduleName];
if (moduleWasNotInitialized) {
[self->_bridge.performanceLogger markStartForTag:RCTPLTurboModuleSetup];
}
// 关键代码,真正获取模块的逻辑
// 如果该模块已经初始化了会直接返回实例,否则会执行初始化流程
auto turboModule = [self provideTurboModule:moduleName runtime:runtime];
// 初始化性能埋点
if (moduleWasNotInitialized && [self moduleIsInitialized:moduleName]) {
[self->_bridge.performanceLogger markStopForTag:RCTPLTurboModuleSetup];
}
// 埋点,记录是否获取成功
if (turboModule) {
TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName);
} else {
TurboModulePerfLogger::moduleJSRequireEndingFail(moduleName);
}
// 返回一个模块实例
return turboModule;
};
// 一个开关,如果在 bridgeless 模式下默认是开启的
// 用来开启兼容旧模块的开关,在开启的情况下才会提供 legacyModuleProvider
if (RCTTurboModuleInteropEnabled()) {
// 这里的代码跟上面 turboModuleProvider 基本一致,区别只在于获取模块的方法变了
// 这里变成了 provideLegacyModule
auto legacyModuleProvider = [self](const std::string &name) -> std::shared_ptr<react::TurboModule> {
auto moduleName = name.c_str();
TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName);
// 关键代码
auto turboModule = [self provideLegacyModule:moduleName];
if (turboModule) {
TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName);
} else {
TurboModulePerfLogger::moduleJSRequireEndingFail(moduleName);
}
return turboModule;
};
// 调用带有 legacyModuleProvide 的 TurboModuleBinding::install
TurboModuleBinding::install(runtime, std::move(turboModuleProvider), std::move(legacyModuleProvider));
} else {
// 调用只有 turboModuleProvider 的 TurboModuleBinding::install
TurboModuleBinding::install(runtime, std::move(turboModuleProvider));
}
}
接下来我们就要进入 provideTurboModule 的具体实现(由于 provideLegacyModule 方法逻辑类似,这里就不额外说明了)
不过在看代码之前,我们需要先了解一些背景知识:TurboModule 内部实现到底有几种
-
纯 C++ 实现的 TurboModule:最 “纯” 的新架构模块,这也是最接近新架构设计的原生模块
-
全局导出的 C++ TurboModule:这个跟第一类其实一样,唯一的区别在于它俩的查找方式不同,上一个是通过 delegate 查找、它是靠 register 查找
-
Objc 平台模块:这一类泛指所有基于 objc 实现的平台模块,细分下去可以分为三类:
-
ObjC 实现的 TurboModule:这个指的是由 Objc 实现且实现了
getTurboModule方法的模块 - legacy ObjC NativeModule:这个由 provideLegacyModule 负责处理
- RCTCxxModule:这个是在旧架构中使用 C++ 实现的模块,因为在旧架构中 C++ 模块需要 Objc 来中转,所以它在之前也被当成了 Objc 的模块
-
ObjC 实现的 TurboModule:这个指的是由 Objc 实现且实现了
而 provideTurboModule 查找的顺序也是依据上面的顺序来的,下面我们看看具体代码:
// in packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm
/**
* provideTurboModule 方法接受一个模块名作为参数,然后查找并返回返回对应的 TurboModule 模块实例
* 查找流程如下:
* 1. 查 _turboModuleCache
* 2. 查 delegate 提供的 pure C++ TurboModule
* 3. 查 global exported C++ TurboModule map
* 4. 再查 ObjC module
**/
- (std::shared_ptr<TurboModule>)provideTurboModule:(const char *)moduleName runtime:(jsi::Runtime *)runtime
{
/**
* 第一步:如果这个模块已经创建过有缓存了,直接返回缓存
* _turboModuleCache 是一个 unordered_map,保存 “模块名” 到 “模块实例指针” 的映射
*/
auto turboModuleLookup = _turboModuleCache.find(moduleName);
if (turboModuleLookup != _turboModuleCache.end()) {
TurboModulePerfLogger::moduleJSRequireBeginningCacheHit(moduleName);
TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);
return turboModuleLookup->second;
}
TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);
/**
* 第二步:检查纯 C++ 模块(C++ 模块拥有最高优先级)
*/
if ([_delegate respondsToSelector:@selector(getTurboModule:jsInvoker:)]) {
int32_t moduleId = getUniqueId();
TurboModulePerfLogger::moduleCreateStart(moduleName, moduleId);
// 往 delegate 上找模块的实例
auto turboModule = [_delegate getTurboModule:moduleName jsInvoker:_jsInvoker];
if (turboModule != nullptr) {
// 如果找到了就保存到缓存中
_turboModuleCache.insert({moduleName, turboModule});
TurboModulePerfLogger::moduleCreateEnd(moduleName, moduleId);
// 然后返回实例
return turboModule;
}
TurboModulePerfLogger::moduleCreateFail(moduleName, moduleId);
}
/**
* 第三步:检查全局导出的 C++ 模块
*/
auto &cxxTurboModuleMapProvider = globalExportedCxxTurboModuleMap();
auto it = cxxTurboModuleMapProvider.find(moduleName);
if (it != cxxTurboModuleMapProvider.end()) {
auto turboModule = it->second(_jsInvoker);
_turboModuleCache.insert({moduleName, turboModule});
return turboModule;
}
/**
* 第四步:找平台相关的模块,在 IOS 中就是 objc 模块
*/
// 只有当 TurboModuleInterop 关闭或者当前模块是 TurboModule 的时候才会调用 _provideObjCModule 方法查找
// legacyModule 不在这里处理,而是直接交给了 legacyModuleProvider
// _provideObjCModule 会找到对应的模块,并且返回模块实例
id<RCTBridgeModule> module =
!RCTTurboModuleInteropEnabled() || [self _isTurboModule:moduleName] ? [self _provideObjCModule:moduleName] : nil;
TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName);
// 如果没有找到直接返回空指针
if (!module) {
return nullptr;
}
// 从模块实例找到对应的类
Class moduleClass = [module class];
// 找到模块需要的 queue
dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(module, &kAssociatedMethodQueueKey);
if (methodQueue == nil) {
RCTLogError(@"TurboModule \"%@\" was not associated with a method queue.", moduleClass);
}
// 根据 queue 创建 nativeMethodCallInvoker
std::shared_ptr<NativeMethodCallInvoker> nativeMethodCallInvoker =
std::make_shared<ModuleNativeMethodCallInvoker>(methodQueue);
// 在 bridgeless 模式下没有 bridge,所以忽略
// 这个方法主要是把 TurboModule 的 native method invoker 交给 RCTCxxBridge 包装一下,让 bridge 能感知 TurboModule 的异步 native 调用,从而维持 onBatchComplete 等旧 bridge 行为
if ([_bridge respondsToSelector:@selector(decorateNativeMethodCallInvoker:)]) {
nativeMethodCallInvoker = [_bridge decorateNativeMethodCallInvoker:nativeMethodCallInvoker];
}
// 处理 RCTCxxModule
if ([moduleClass isSubclassOfClass:RCTCxxModule.class]) {
// 直接用一个 TurboCxxModule 类包起来完事
auto turboModule = std::make_shared<TurboCxxModule>([((RCTCxxModule *)module) createModule], _jsInvoker);
// 还是一样放入 cache 中
_turboModuleCache.insert({moduleName, turboModule});
return turboModule;
}
// 最后我们来处理有 getTurboModul 方法的 objc 模块
if ([module respondsToSelector:@selector(getTurboModule:)]) {
ObjCTurboModule::InitParams params = {
.moduleName = moduleName,
.instance = module,
.jsInvoker = _jsInvoker,
.nativeMethodCallInvoker = nativeMethodCallInvoker,
.isSyncModule = methodQueue == RCTJSThread,
.shouldVoidMethodsExecuteSync = (bool)RCTTurboModuleSyncVoidMethodsEnabled(),
};
auto turboModule = [(id<RCTTurboModule>)module getTurboModule:params];
if (turboModule == nullptr) {
RCTLogError(@"TurboModule \"%@\"'s getTurboModule: method returned nil.", moduleClass);
}
_turboModuleCache.insert({moduleName, turboModule});
if ([module respondsToSelector:@selector(installJSIBindingsWithRuntime:)]) {
[(id<RCTTurboModuleWithJSIBindings>)module installJSIBindingsWithRuntime:*runtime];
}
return turboModule;
}
return nullptr;
}
总结一下,IOS 这块代码主要就是根据不同的模块类型一一处理,并且最后统一包裹成 TurboModule 返回
其中 _turboModuleCache 是一个关键的缓存机制,它保证了模块最多只会初始化一次
RN 团队在注释中有写了一个 TODO 想要把模块的生命周期管理下放到由模块自己管理(就是让 _turboModuleCache 不要像现在长时间保存实例缓存),但是我看了下到目前最新的 0.85 版本这个 TODO 还在
接下来我们来看看 Android 的 TurboModuleProvider 做了啥吧~
// in packages/react-native/ReactAndroid/src/main/jni/react/turbomodule/ReactCommon/TurboModuleManager.cpp
void TurboModuleManager::installJSIBindings(
jni::alias_ref<jhybridobject> javaPart,
bool shouldCreateLegacyModules) {
auto cxxPart = javaPart->cthis();
if (cxxPart == nullptr || !cxxPart->jsCallInvoker_) {
return;
}
// 从 runtimeExecutor 拿到 js runtime
cxxPart->runtimeExecutor_([cxxPart,
javaPart = jni::make_global(javaPart),
shouldCreateLegacyModules](jsi::Runtime& runtime) {
// 跟 IOS 一样,也是在这里调用了 install 方法
TurboModuleBinding::install(
runtime,
// TurboModuleProvider
cxxPart->createTurboModuleProvider(javaPart, &runtime),
shouldCreateLegacyModules
// LegacyModuleProvider
? cxxPart->createLegacyModuleProvider(javaPart)
: nullptr);
});
}
接下来我们看看具体实现:
// in packages/react-native/ReactAndroid/src/main/jni/react/turbomodule/ReactCommon/TurboModuleManager.cpp
TurboModuleProviderFunctionType TurboModuleManager::createTurboModuleProvider(
jni::alias_ref<jhybridobject> javaPart,
jsi::Runtime* runtime) {
return [runtime, weakJavaPart = jni::make_weak(javaPart)](
const std::string& name) -> std::shared_ptr<TurboModule> {
auto javaPart = weakJavaPart.lockLocal();
if (!javaPart) {
return nullptr;
}
auto cxxPart = javaPart->cthis();
if (cxxPart == nullptr) {
return nullptr;
}
// 具体实现在这~下面有代码~
return cxxPart->getTurboModule(javaPart, name, *runtime);
};
}
// 这个方法对应 IOS 的 provideTurboModule 方法
std::shared_ptr<TurboModule> TurboModuleManager::getTurboModule(
jni::alias_ref<jhybridobject> javaPart,
const std::string& name,
jsi::Runtime& runtime) {
const char* moduleName = name.c_str();
TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName);
/**
* 第一步:如果这个模块已经创建过有缓存了,直接返回缓存
*/
auto turboModuleLookup = turboModuleCache_.find(name);
if (turboModuleLookup != turboModuleCache_.end()) {
TurboModulePerfLogger::moduleJSRequireBeginningCacheHit(moduleName);
TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);
return turboModuleLookup->second;
}
TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);
/**
* 第二步:检查纯 C++ 模块(C++ 模块拥有最高优先级)
*/
auto cxxDelegate = delegate_->cthis();
auto cxxModule = cxxDelegate->getTurboModule(name, jsCallInvoker_);
if (cxxModule) {
turboModuleCache_.insert({name, cxxModule});
return cxxModule;
}
/**
* 第三步:检查全局导出的 C++ 模块
*/
auto& cxxTurboModuleMapProvider = globalExportedCxxTurboModuleMap();
auto it = cxxTurboModuleMapProvider.find(name);
if (it != cxxTurboModuleMapProvider.end()) {
auto turboModule = it->second(jsCallInvoker_);
turboModuleCache_.insert({name, turboModule});
return turboModule;
}
/**
* 第四步:找平台相关的模块,在 Android 中就是 java 模块
*/
static auto getTurboJavaModule =
javaPart->getClass()
->getMethod<jni::alias_ref<JTurboModule>(const std::string&)>(
"getTurboJavaModule");
auto moduleInstance = getTurboJavaModule(javaPart.get(), name);
if (moduleInstance) {
TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName);
JavaTurboModule::InitParams params = {
.moduleName = name,
.instance = moduleInstance,
.jsInvoker = jsCallInvoker_,
.nativeMethodCallInvoker = nativeMethodCallInvoker_};
auto turboModule = cxxDelegate->getTurboModule(name, params);
if (moduleInstance->isInstanceOf(
JTurboModuleWithJSIBindings::javaClassStatic())) {
static auto getBindingsInstaller =
JTurboModuleWithJSIBindings::javaClassStatic()
->getMethod<BindingsInstallerHolder::javaobject()>(
"getBindingsInstaller");
auto installer = getBindingsInstaller(moduleInstance);
if (installer) {
installer->cthis()->installBindings(runtime);
}
}
turboModuleCache_.insert({name, turboModule});
TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName);
return turboModule;
}
// 处理旧架构中使用 C++ 实现的模块(对应 IOS 的 RCTCxxModule)
static auto getTurboLegacyCxxModule =
javaPart->getClass()
->getMethod<jni::alias_ref<CxxModuleWrapper::javaobject>(
const std::string&)>("getTurboLegacyCxxModule");
auto legacyCxxModule = getTurboLegacyCxxModule(javaPart.get(), name);
if (legacyCxxModule) {
TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName);
auto turboModule = std::make_shared<react::TurboCxxModule>(
legacyCxxModule->cthis()->getModule(), jsCallInvoker_);
turboModuleCache_.insert({name, turboModule});
TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName);
return turboModule;
}
return nullptr;
}
可以看到 Android 的实现跟 IOS 基本没区别
在这个小节的最后,我们来聊一下 TurboModule 的两层缓存是怎么实现的:
- 第一层缓存是在宿主的
turboModuleCache_中这里存放了所有被初始化了的原生模块实例 - 第二层缓存是在 C++ 的
jsRepresentation,这是一个 JS 对象,每个对象对应到一个模块实例,模块实例通过挂在它的原型链上使得其可以访问模块的方法
访问模块的核心方法在 TurboModule.h:
// in packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h
// 在 TurboModuleBinding::getModule 方法中,我们知道它返回了 jsRepresentation 并且把对应模块实例挂上了它的原型链
// 如果在 JS 侧访问某个模块方法但是在 jsRepresentation 中找不到时,会试图往原型链上找,于是就会命中这里的 get 方法
facebook::jsi::Value get(
facebook::jsi::Runtime& runtime,
const facebook::jsi::PropNameID& propName) override {
{
// 在当前 TurboModule 实例上找到对应的属性
auto prop = create(runtime, propName);
// 对于访问过的实例,我们把它放进 jsRepresentation 的属性中,这样下次访问就不用在往原型链上走了
if (jsRepresentation_ && !prop.isUndefined()) {
jsRepresentation_->lock(runtime).asObject(runtime).setProperty(
runtime, propName, prop);
}
return prop;
}
}
可以看到,这一个小小的 get 方法就完成了对模块属性级别的缓存
Codegen
在了解了 TurboModule 的设计方案以及调用链路后,我们接下来要补齐 TurboModule 的最后一块拼图:Codegen
根据 官网博客 的描述,codegen 是一个可选的工具,所以我们首先需要知道的是它的优势是什么,以及什么时候推荐使用
Codegen 的优势
Codegen 的优势可以一句话总结:它用一份强类型 JS/TS spec 生成 Android、iOS、C++ 原生层所需的接口与胶水代码,从而减少样板代码维护成本,并降低 JS 与 Native 之间类型不一致、跨语言调用出错的风险
怎么理解这句话呢?
假设我们想要实现一个原生模块叫 NativeNotifier 它的职责是调用原生的能力,生成一个系统弹窗出来(示例见下图)
因为这个模块调用了 原生的能力 所以我决定用 objc 来实现,下面来讲一下如果不使用 codegen 的话,要怎么做:
第一步,先创建 JS wrapper:
// NativeNotifier.ts
import {TurboModuleRegistry} from 'react-native';
type NativeNotifierType = {
show(message: string): void;
};
export default TurboModuleRegistry.getEnforcing<NativeNotifierType>(
'NativeNotifier',
);
第二步,创建 IOS header:
// NativeNotifier.h
#import <React/RCTBridgeModule.h>
// 用 RCTBridgeModule 是为了让 RN 能发现这个 ObjC module,并读取 RCT_EXPORT_MODULE / RCT_EXPORT_METHOD 产生的 metadata
@interface NativeNotifier : NSObject <RCTBridgeModule>
@end
第三步,创建 iOS 实现:
// NativeNotifier.mm
#import "NativeNotifier.h"
#import <React/RCTUtils.h>
#import <ReactCommon/RCTInteropTurboModule.h>
#import <ReactCommon/RCTTurboModule.h>
#import <UIKit/UIKit.h>
using namespace facebook::react;
@interface NativeNotifier () <RCTTurboModule>
@end
@implementation NativeNotifier
// 暴露模块与方法名
RCT_EXPORT_MODULE(NativeNotifier)
RCT_EXPORT_METHOD(show:(NSString *)message)
{
// 具体逻辑实现,这里省略
}
- (std::shared_ptr<TurboModule>)getTurboModule:
(const ObjCTurboModule::InitParams &)params
{
// 把这个 ObjC module 包装成 TurboModule
return std::make_shared<ObjCInteropTurboModule>(params);
}
@end
第四步,为了让 xcode 编译 NativeNotifier.mm,我们需要把它加入到 Xcode target 中
第五步,在 App 中调用它:
import NativeNotifier from './NativeNotifier';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
// 调用这个模块方法
onPress={() => NativeNotifier.show('Hello from native iOS code')}>
<Text style={styles.buttonText}>Show native message</Text>
</Pressable>
</SafeAreaView>
);
}
如果我们纯手写的话,我们不仅要先知道 TurboModuleRegistry.getEnforcing 的用法,还需要知道 RCTBridgeModule、RCTTurboModule、ObjCInteropTurboModule 的用法与区别,最关键的是,这个流程我们需要在 Android 再来一次
这还是初次开发这种简单模块的情况,如果后续需要迭代修改,或者是模块的复杂度上去了,维护成本巨大,双端的一致性也是个大问题
Codegen 是怎么解决的呢?
第一步,在项目根目录创建 spec 目录以及 JS wrapper
// specs/NativeNotifier.ts
// 这个文件既是 JS 调用入口,也是 Codegen 读取的 spec
// 文件目录名字可以通过在 package.json 自定义,但约定俗成使用 specs
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
// 很重要的类型声明,Codegen 会通过这个类型生成胶水代码
export interface Spec extends TurboModule {
show(message: string): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeNotifier');
第二步,去 package.json 配置相关信息,具体配置参考 官方博客
{
"codegenConfig": {
"name": "DemoSpec", // SpecName
"type": "modules", // types
"jsSrcsDir": "specs", // source_dir
"android": {
"javaPackageName": "com.demo" // java.package.name
},
}
第三步,创建 iOS 实现:
// ios/Demo/NativeNotifier.h
#import <Foundation/Foundation.h>
@interface NativeNotifier : NSObject
@end
// ios/Demo/NativeNotifier.mm
#import "NativeNotifier.h"
#import <React/RCTUtils.h>
#import <DemoSpec/DemoSpec.h>
#import <UIKit/UIKit.h>
@interface NativeNotifier () <NativeNotifierSpec>
@end
@implementation NativeNotifier
RCT_EXPORT_MODULE()
- (void)show:(NSString *)message
{
// 具体逻辑实现,这里省略
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeNotifierSpecJSI>(params);
}
@end
第四步,把 NativeNotifier.mm 加入到 Xcode target 中
第五步,实现 App.tsx 中的调用逻辑
完成了这五步后,在配置正确的情况下,IOS/Android build 流程会触发 Codegen,自动生成双端所需的接口和胶水代码;但具体的原生模块实现,例如 IOS 的 NativeNotifier.mm、Android 的 NativeNotifierModule.kt,仍然需要开发者自己实现并注册
细心的读者会发现,即使用了 Codegen,我们仍然需要:
- 创建 spec
- 配置 package.json
- 实现 iOS native 逻辑
- 加入 Xcode target
- 在 JS 中调用
也就是说,Codegen 并不是让创建 native module 的步骤变少,它真正减少的是隐藏在这些步骤背后的胶水代码:方法表、类型转换、JSI adapter、Android abstract spec、JNI glue,以及双端 API 签名同步的成本
简单画个比较表:
| 对比点 | 不使用 Codegen | 使用 Codegen |
|---|---|---|
| JS/native API 来源 | JS wrapper 和 native method 各写一份(总共三份) | JS/TS spec 是唯一 API 来源(Single source of truth) |
| 类型检查 | 主要靠人工保证 | Codegen 根据 spec 生成 native 类型约束 |
| iOS glue code | 依赖 ObjCInteropTurboModule 运行时解析 RCT_EXPORT_METHOD metadata |
生成 NativeNotifierSpec /NativeNotifierSpecJSI
|
| Android glue code | 需要手动写 module/package,签名一致性靠人工 | 生成 abstract Spec 和 JNI 胶水代码 |
| 方法名/参数数量错误 | 运行时更容易暴露 | 编译期或生成阶段更容易暴露 |
| 后续修改 API | JS、iOS、Android 多处人工同步 | 先改 spec,再让生成代码约束 native 实现 |
| 适用场景 | 迁移旧 ObjC bridge module、快速兼容 | 新架构下新模块,更适合长期维护 |
总结
本文从 TurboModule 的设计开始,讲到了 Codegen 的优势以及为什么要使用 Codegen
诚然 Codegen 可以降低很多开发 TurboModule 的隐形成本,但作为开发者了解 TurboModule 以及背后的 JSI 才是主要的根基
毕竟只有掌握了底层的机制,才能往上搭出像 Nitro module 或 Expo Modules API 这样的上层库/工具