有段时间没更新博客了,之前计划由浅到深、从应用到原理,更新一些RN的相关博客。之前陆续的更新了6篇RN应用的相关博客(传送门),后边因时间问题没有继续更新。主要是平时空余时间都用来帮着带娃了,不过还是要挤挤时间来总结下,目标是完成由浅到深、由应用到原理的RN系列博客。本篇算是属于原理部分的博客,不过不在之前计划中。本篇是本人在公司内部某事业群大前端月刊中发布的一篇纯技术分享的博客,是基于Facebook的RNTester工程进行的TurboModule的源码分析,因为不涉及公司内部的敏感代码及相关信息,而且在公司内部发布受众有限,所以就以个人名义同步到自己的博客中,与大家分享及交流。文中所述内容仅代表个人观点,如有偏颇或不恰当之处还望指正。
一、简介
Turbo Modules是升级版的Native Modules,是基于JSI开发的一套JS与Native交互的轻量级框架,用来解决在使用Native Modules时遇到的问题。本篇博客主要对Turbo Modules和Native Modules进行了对比,并对Turbo Modules的实现进行了探究。除了介绍官方给出的优化点外,还通过具体示例对Turbo Modules与Native Modules的通信耗时进行了对比分析。 后续会以iOS视角,结合源码补充JSI、Fabric等RN新架构中的实现原理。
下方是新旧架构种,NativeModule与TurboModule相关区别,下方会进行详细展开。
二、为什么要推出Turbo Modules
1、Native Modules的缺点
下方是官方给出的Native Modules缺点,同时也是推出Turbo Modules的原因。
序号 | 总结 | 介绍 |
---|---|---|
1 | Native Modules不支持懒加载 | 在一个包中指定Native Modules有着更早的初始化时机。React Native的启动时间随着Native Modules的数量增加而增加,即使其中一些Native Modules从未使用过也会被创建。Native Modules还不能使用开源的LazyReactPackage进行懒加载,因为LazyReactPackage中ReactModuleSpecProcessor不能与Gradle一起运行,目前该问题尚未解决。 |
2 | Native Modules检查JS与Native方法一致性较为困难 | 暂无简单的方法可以检查JavaScript调用的Native Modules是否在Native中被定义了。并且在热更新时,暂无简单的方式来检查新版中JS代码在调用Native Modules方法时入参是否正确。 |
3 | Native Modules以单例形式存在,其生命周期与桥关联 | Native Modules是以单例的形式存在,其生命周期与桥生命周期相关。该问题在Native与RN混编的APP中尤为明显,因为RN桥可能会多次启动和关闭。 |
4 | Native Modules的方法列表是在运行时进行扫描(多余的运行时操作) | 在启动过程中,Native Modules通常被定义在多个包中。在运行时去遍历,最终给出桥接的Native Modules列表而这些操作是完全不需要在运行时执行。 |
5 | Native Modules使用运行时的反射来实现的,完全可以放到编译期来做 | 一个Native Module的方法和常量推断是在运行时通过反射来实现的。这些操作完全可以放到编译期。 |
参考:官方Turbo Modules介绍
2、Native Modules VS Turbo Modules
下方对Turbo Modules与Native Modules进行了对比,关于Turbos Modules相关的内容下方会详细展开。
三、Turbos Module关键特性探究
本篇wiki中的示例是基于RN官方的“RCTSampleTurboModule”来展开分析,该示例中使用Turbo Modules在Native侧定义导出一系列的方法,然后在JS侧进行调用。其中有异步方法,也有同步方法,下方是核心代码所在位置以及运行效果。
1、Turbo Modules执行过程概览
首先通过官方示例来分析Turbo Modules的使用方式,在官方示例中创建了一个SampleTurboModule,在SampleTurboModule中导出了一系列的Native方法供JS使用,其功能与Native Modules所做的事情一致,但是其实现方式上有着本质区别,下方是相关调用过程。
Turbo Modules在使用过程中的调用流程:
- JS侧:首先在JS侧可以通过import的形式来引入相关Turbo Modules,而在Turbo Modules声明时,会创建JS侧的方法接口,该接口中声明了一些Turbo Modules桥接的方法。我们可以通过该接口定义,使用CodeGen来生成JSI侧相关的调用方法,以及OC/Java侧的方法接口,从而达到接口一致性的目的。
- JSI&引擎层:自定义Turbo Modules需要实现JSI相关方法,可以将JSI相关方法与OC/Java方法进行映射,而这一步相关的方法也是由CodeGen自动生成。
- Native侧:在上层代码(OC/Java)中,可以基于生成的接口来实现相关的桥方法,在JS侧最终调用时,会执行该方法。
- (1)、JS侧对SampleTurboModule的使用 在JS侧声明SampleTurboModule时,会创建对应的自定义JS接口。使用时可以根据接口提供的方法来确定使用场景。而JSI层及OC/Java层对应的自定义Turbo Modules代码,可以通过该接口生成对应的代码及相关协议。稍后在CodeGen中会详细介绍到。而本部分主要介绍模块的注册及使用。 模块注册:在JS侧通过TurboModuleRegistry.getEnforcing方法对RCTSampleTurboModule模块进行导出,并且声明了一个Spec接口,其中包括了SampleTurboModule中声明的相关方法。具体代码如下: 导入后,其使用方式与Native Modules使用一致,具体如下所示:
1 /**
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 * @format
9 */
10
11 import type {UnsafeObject} from '../../Types/CodegenTypes';
12 import type {RootTag, TurboModule} from '../RCTExport';
13 import * as TurboModuleRegistry from '../TurboModuleRegistry';
14
15 export interface Spec extends TurboModule {
16 // Exported methods.
17 getConstants: () => {|
18 const1: boolean,
19 const2: number,
20 const3: string,
21 |};
22 voidFunc: () => void;
23 getBool: (arg: boolean) => boolean;
24 getNumber: (arg: number) => number;
25 getString: (arg: string) => string;
26 getArray: (arg: Array<any>) => Array<any>;
27 getObject: (arg: Object) => Object;
28 getUnsafeObject: (arg: UnsafeObject) => UnsafeObject;
29 getRootTag: (arg: RootTag) => RootTag;
30 getValue: (x: number, y: string, z: Object) => Object;
31 getValueWithCallback: (callback: (value: string) => void) => void;
32 getValueWithPromise: (error: boolean) => Promise<string>;
33 }
34
35 export default (TurboModuleRegistry.getEnforcing<Spec>(
36 'RCTSampleTurboModule',
37 ): Spec);
(2)、NativeSampleTurboModuleSpecJSI具体实现(C 实现 - 基于JSI提供JS可调用的方法)
下方使用C 编写的NativeSampleTurboModuleSpecJSI即基于JSI为SampleTurboModule提供的具体实现类。该类继承自ObjCTurboModule,而ObjCTurboModule继承自TurboModule类,而TurboModule类继承自HostObject类。该类是CodeGen自动生成。
相关代码截图
而上述的JSI_EXPORT本质上是__attribute__((visibility("default")))的宏定义,该属性用于设置动态链接库中类的可见性。
代码语言:javascript复制#define JSI_EXPORT __attribute__((visibility("default")))
下方是NativeSampleTurboModuleSpecJSI类的构造函数中的具体实现,其中主要功能是使用methodMap将JS中的方法与JSI对应的方法实现进行关联。而methodMap的key是JS侧使用的方法名,value则是MethodMetadata对象,及JSI中声明的方法。
上述在.h文件中进行了类的声明,下方是.mm文件中的具体实现,以getString方法的具体实现为例。下方定义了一个名为__hostFunction_NativeSampleTurboModuleSpecJSI_getString的C 方法。该方法有一个类型为facebook::jsi::Value的返回值(Value是JS相关数据类型在JSI中的一个映射,JSI中关于Value的解释:Represents any JS Value (undefined, null, boolean, number, symbol, string, or object). Movable, or explicitly copyable )。
在JS中每次通过SampleTurboModule调用getString方法,都会执行下面的方法,实际作用是调用Native侧实现的getString,并返回相关值。
上述方法中四个参数如下所示:
- &rt:第一个是当前JS的运行环境,Demo中使用的Hermes引擎,所以该参数为当前Hermes的运行时对象。
- &turboModule:第二个参数是turboModule对象,即该示例自定义的SampleTurboModule对应的对象,也就是该方法属于哪个Turbo Modules实例,如下所示。
- *args:第三个参数就比较常规了,就是getString方法的入参。
- count:则是该方法有几个参数,此处的getString只有一个参数,那么Count = 1。
该方法实现中,调用了turboModule的invokeObjCMethod方法。invokeObjCMethod中传入了getString方法的SEL,其中就会执行Objective-C中对应的getString方法,并把返回值返回出去。在invokeObjCMethod方法中,首先获取当前Module的名称和方法,然后开始打点。红框中是关键代码,如果是Promise,则创建对应的回调,否则直接调用performMethodInvocation执行相关方法。
- (3)、Native桥接口(协议)声明 -- NativeSampleTurboModuleSpec CodeGen还会自动生成NativeSampleTurboModuleSpec的协议,该协议遵循了RCTBridgeModule和RCTTurboModule(后边会详细介绍),该接口中声明的方法都是在JS侧要调用的方法,也就是桥方法。
- (4)、RCTSampleTurboModule的创建(Native侧方法的具体实现) 基于NativeSampleTurboModuleSpec协议来创建自定义的Turbo Modules类RCTSampleTurboModule。在下方类声明中的注释信息中可以看出,该类100%与Native Modules系统进行兼容。在RCTSampleTurboModule类声明时中遵循了RCTBridgeModule,在类的@implementation中实现了该协议中的相关方法,以及使用了RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD、RCT_EXPORT_METHOD的方式进行方法导出,目的在于兼容Native Modules。
上述代码中的RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD、RCT_EXPORT_METHOD等方法完全是为了兼容Native Modules,如果在你的APP中没必要兼容Native Modules,在仅仅使用Turbo Modules的情况下,完全可以把上述Export方法换成正常的Objective-C的实现。
除了上述的兼容方法外,在Turbo Modules的使用中比较关键的就是getTurboModule方法。该方法是RCTTurboModule协议中声明的方法,目的在于获取自定义的Turbo Modules对象。getTurboModule的方法实现比较简单,就是调用了一个C 的库函数来对NativeSampleTurboModuleSpecJSI类进行实例化。
下方是Turbo Modules注册及实例初始化的相关流程。Turbo Modules的注册过程确切说是TurboModuleRegister初始化过程,并不会创建相关Turbo Modules对象。
Turbo Modules的注册过程如下:
- TurboModuleManager对象创建:首先在桥创建时,会调用RCTCxxBridge.start方法,在该方法中会创建TurboModuleManager对象。
- TurboModuleBinding初始化:然后在TurboModuleManager中会调用installJSBindingWithRuntimeExecutor方法,来调用TurboModuleBinding的install方法进行初始化,并且将turboModuleProvider与TurboModuleBinding进行关联。
- JS侧注入__turboModuleProxy方法:在TurboModuleBinding的Install中,动态的为JS侧全局global添加了一个__turboModuleProxy方法,JS侧可以调用__turboModuleProxy方法来获取对应的Turbo Modules实例。
注册器初始化后,可以在JS侧调用相关__turboModuleProxy来获取对象了,具体流程如下:
- JS侧调用方式:首先在JS侧import TurboModule,然后会调用JS侧TurboModuleRegistry的getEnforcing方法,最终通过全局变量global执行__turboModuleProxy。
- 获取Turbo Modules实例:最终在Native侧会执行provideTurboModule方法,该方法中主要分为三步----获取缓存 -> 创建C 模块实例 -> 创建ObjC/Java模块实例,并将实例存入缓存。下方在介绍Turbo Modules的懒加载时会详细介绍。
- JS侧获取实例并调用相关方法:经过上述过程,在JS侧可以获取相关Turbo Modules对象然后调用相关方法。
平台 | iOS | Android |
---|---|---|
流程 |
在JS侧会调用TurboModuleRegistry.getEnforcing方法来加载自定义的Turbo Modules,具体代码如下所示:
代码语言:javascript复制1 export default (TurboModuleRegistry.getEnforcing<Spec>(
2 'SampleTurboModule',
3 ): Spec);
而上述调用最终会执行到requireModule(),在下述方法中首先通过Bridgeless来判断是否支持Turbo Modules,如果不支持则返回同名的Native Modules。相反就会调用turboModuleProxy,而此处的__turboModuleProxy方法是通过global ( NodeJS.Global 类型,全局变量)获取的。
而__turboModuleProxy方法则是在Native侧通过运行时往JS的global上绑定的一个方法。而__turboModuleProxy方法则是通过JSI的形式注册关联到JS侧的,最终会调用到Native侧jsProxy方法,从调用栈上可看出在Native侧的调用链为 jsProxy -> getModules -> provideTurboModule,provideTurboModule方法会返回对应的Module实例,如下图所示:
以iOS侧为例(Android实现方案类似,就不做过多赘述),provideTurboModule(入参为module name)方法中主要有三步:
- 第一步查找缓存:首先查找缓存,如果之前创建过对应的Turbo Modules对象,则直接返回。
- 第二步创建并返回对应的C modules:优先返回对应C Module。
- 第三步创建并返回平台指定的Module:最后是创建并返回平台指定侧Module,此处是iOS系统,使用的ObjC,所以返回的是ObjCTurboModule,如果是安卓则返回JavaTurboModule。
3、Turbo Modules懒加载机制
当第一次对Turbo Module进行import调用时,上面的TurboModuleRegistry.getEnforcing方法才会执行,进而才会创建对应的Turbo Module实例对象并进行缓存。如果没有对模块进行import,那么对应的模块将永远不会初始化。
JS侧首先读取本地缓存,因为OC可以直接跟C 交互。在读取缓存与创建C 对象时Java和OC有一些差异,OC可以直接创建C 实例,而Java必须通过C 创建,所以这里使用“Native侧”统一表示。当缓存读取失败时,会创建一个纯C 实例(pure-C Native Modules),在这里Android侧代码中没有给出实现,iOS侧有自己的实现,如果这里创建成功,会写入缓存并且返回给JS侧。当pure-C 实例没有成功创建,就会创建JavaTurboModule/ObjcModule实例,因为Java实例不能直接被JS调用,因此Android侧会额外创建一个C 实例包裹这个Java实例,然后将这个C 实例写入缓存并返回。
4、Turbo Modules的创建与销毁
上一部分对Turbo Modules的创建过程进行了重点介绍,该部分注重介绍Turbo Modules对象的销毁过程(以iOS侧为例):
- Turbo Modules实例创建:在JS侧调用Turbo Modules时会创建相关实例(懒加载),并且将创建好的实例存入CacheMap。
- RCTBridge的创建:在RN示例中RCTRootView创建时,会创建RCTBridge相关实例。
- RCTBridge的销毁:当RCTRootView销毁时,则会释放RCTBridge实例。在RCTBridge释放后,会发送桥销毁的通知。
- ModuleCacheMap清空:RCTTurboModuleManager对象收到通知后,会清空ModuleCacheMap。
Turbo Modules的生命周期也是与RCTBridge绑定的,当RCTBridge对象被释放时,会发通知清除当前创建的Turbo Modules实例。在官方示例的AppDelete及RCTRootView创建时都会创建RCTBridge对象,也就是说Turbo Modules的生命周期是与RCTRootView的生命周期一致。具体分析如下:
- 在TurboModulesManager的,_invalidateModules方法中会对缓存进行清除,而_invalidateModules在收到bridge销毁后的通知时调用。
- RCTBridge在官方示例的AppDelete、RCTRootView、RCTSurfaceHostingProxyRootView(为了兼容RCTRootView,便于往Fabric中的RCTSurface上迁移)中都有初始化,所以当RCTRootView释放时其对应的RCTBridge对象也会被释放,此刻就会发通知然后清除缓存。而在AppDelete中的didFinishLaunching方法中,创建了RCTBridge对象,并将RCTBridge实例已参数的形式传入了RCTRootView的构造方法中。所以在单bundle单页面的情况下,每次退出页面都会都模块缓存Map进行清空。
- 经过代码分析,开发过程中的Command R也会对Turbo Modules的缓存进行清空。
5、接口一致性保障
(1)、Facebook官方工具(暂未正式公开对外使用)
CodeGen是一个开发工具,作用是静态类型检查器(Flow或TypeScript),目的是以自动化的形式来保证JS侧与Native侧的兼容性。用来解决之前检查JS侧接口与Native侧接口一致性比较困难的问题。
The React Native team is also doubling down on the presence of a static type checker (either Flow or TypeScript) in the code. In particular, they are working on a tool called CodeGen to "automate" the compatibility between JS and the native side. By using the typed JavaScript as the source of truth, this generator can define the interface files needed by Fabric and TurboModules (elements of the new architecture that will be showcased in the third post) to send messages across the realms with confidence. This automation will speed up the communication too, as it’s not necessary to validate the data every time.
目前没有找到官方关于介绍CodeGen使用的相关文档,github上有人分享基于react-native-codegen生成代码的工具,亲测可用。(官方链接)
参考:
- https://formidable.com/blog/2019/react-codegen-part-1/
- https://github.com/react-native-community/discussions-and-proposals/issues/92
- https://github.com/facebook/react-native/tree/master/packages/react-native-codegen
- https://npm.runkit.com/react-native-codegen
(2)、微软开源的react-native-tscodegen(可用)
除了上述FB的rn codegen,而微软也开源了一款rn-tscodegin(Github地址),目的是根据TypeScript的接口,来生成Turbo Modules。在RN工程中亲测可用。
四、Turbo Modules通信性能分析
官方相关文档在介绍Turbo Modules的优化点时,没有介绍其在通信过程中的优化点。本部分作为扩充,通过相关示例来探究Turbo Modules的通信过程中所做的事情。首先是线程切换上,其次是异步调用过程中的耗时探究。具体如下所示。
1、方法执行过程中的线程切换
- 同步调用:在Turbo Modules的同步方法调过程中没有线程切换,都是在JS线程中完成的相关操作。所以如果在同步调用的方法中执行耗时操作势必会造成JS线程阻塞,从而会影响其他JS线程的操作。
- 异步调用:而异步调用会有相关的线程切换,会将JS线程切换到主线程或者异步方法调用时指定的线程中,然后在相关线程中执行异步方法。执行回调时又会切换到JS线程中。经过相关Demo验证,一次线程切换的操作耗时可忽略不计。
(1)、iOS侧切换线程的过程
同步方法:Turbo Modules同步方法的调用过程不存在线程切换问题,依旧是在JS线程。
异步方法:在CallBack或者Promise方法执行时,会走到下方的方法中,该方法调用了dispatch_async,如果methodQueue,是主线程对应的队列,那么就会切换到主线程中。
iOS调试验证截图
同步调用
异步调用:
在iOS侧上述的methodQueue是在RCTBridgeModule代理的methodQueue方法提供,该方法会在桥定义时进行实现。方法中如果返回的是主队列,那么就会切换到主线程。如果是创建的新队列,则会创建一个新的线程。
(2)、Android侧切换线程的过程
Android侧的线程切换过程与iOS侧大同小异,篇幅有限,就不做过多赘述。下方是安卓侧线程切换相关流程。
2、异步调用耗时分析
同步调用无论在Native Modules和Turbo Modules中,执行过程都非常快(1~5ms),而异步桥的调用过程会慢一些(50 ~ 150ms之间)。本部分着重探究异步桥的调用耗时。
首先对Turbo Modules与Native Modules的异步桥调用进行了测试和分析,下方是相关数据,对应结果如下:
- 整体耗时:经过相关测试,双端Turbo Modules的异步通信耗时与Native Modules的通信耗时优势并不明显,两者不相上下。当然官方没有明确给出Turbo Modules在桥调用过程中速度更快,而说Turbo Modules在加载过程中会有一些优化,并且基于JSI实现的,JS可直接通过JSI调用OC方法。整体耗时变化不明显,也算符合预期。
- JS to Native:在JS调用Native相关方法是,Turbo Modules因为是通过C 代码之间调用Java/OC对应的方法,执行结果比较快,无论是Android还是iOS,都在1ms左右。而Native Modules因为通过消息队列进行调用,性能会差一些,安卓在50ms左右,iOS在 20ms左右。
- Native to JS:经过测试发现Turbo Module在Native to JS的过程中要比Native Module慢几十毫秒,这点有点出乎意料。稍后会进行分析具体是什么地方耗时。
测试机型:
- Android:Nokia X7 Android 9.0 4G 64G
- iOS:iPhone 8 iOS12 2G 64G
测试场景:
- 异步桥调用
测试次数:
- 手动点击100次,相关数据取平均值(Android & iOS的统计口径及测试代码逻辑保持一致)
平台 | 模块 | 总耗时(ms) | JS to Native(ms) | Native to JS(ms) |
---|---|---|---|---|
Android | Turbo Modules | 118.79 | 1.56 | 117.23 |
Native Modules | 126.05 | 46.1 | 79.93 | |
iOS | Turbo Modules | 83.62 | 1.05 | 82.61 |
Native Modules | 89.8 | 22.16 | 67.64 |
3、异步调用过程中CallBack的耗时分析
callback过程,即一次Native to JS的执行过程。通过调试可发现,最终是jsInvoke.invokeAsyn方法中的callback.call()方法耗时比较严重,该调用占据了整个Turbo Modules异步调用过程中的约95%的耗时。通过工具调试定位,具体执行方法的耗时落在了Hermes引擎中的相关方法的执行上(Native Modules也有同样的问题)。
具体是Hermes引擎的哪些操作比较耗时?如何对其进行优化?最终能优化多少?在JSC和V8引擎上Turbo Modules表现如何?欲知后事如何,请听下回分解。
代码语言:javascript复制strongWrapper->jsInvoker().invokeAsync([weakWrapper, responses, blockGuard]() {
double start = [[NSDate new] timeIntervalSince1970] * 1000;
auto strongWrapper2 = weakWrapper.lock();
if (!strongWrapper2) {
return;
}
std::vector<jsi::Value> args = convertNSArrayToStdVector(strongWrapper2->runtime(), responses);
strongWrapper2->callback().call(strongWrapper2->runtime(), (const jsi::Value *)args.data(), args.size());
strongWrapper2->destroy();
// Delete the CallbackWrapper when the block gets dealloced without being invoked.
(void)blockGuard;
double end = [[NSDate new] timeIntervalSince1970] * 1000;
NSLog(@"Native Block End = %f",end - start );
});
从下方的分析过程中不难发现,一次CallBack过程中的操作耗时75ms,其中有73ms在Hermes引擎执行中。在调试过程中因为没有加载Hermes源码,具体耗时方法暂未定位,后续会继续探索并尝试给出相关优化方案, 具体调试过程:
五、总结
Turbo Modules的实现基于JSI,较Native Modules有明显优势。但是在异步桥调用的过程中,优势并不明显,而且有较大优化空间。具体总结如下:
- Turbo Modules加载过程更优:Turbo Modules支持懒加载,所以在加载过程及生命周期上比Native Modules有明显优势。
- Turbo Modules解决了接口一致性:Turbo Modules使用过程中,可通过JS侧的接口生成C 中间层的JSI代码,并且生成对应的OC/Java的接口。可以基于接口来实现Native方法,从而达到了JS - Native两侧的接口一致性。而Native Modules没有相关机制来保证JS与Native侧的接口一致性,所以往往会造成JS侧调用的Native方法被删除掉,进而造成Crash的情况。
- Turbo Modules的方法加载效率更高:Turbo Modules的方法加载是编译器就确定了,会经过JSI往OC/Java上映射相关的桥方法,在调用时直接执行。而Native Modules则是在运行时执行的,多余的运行时操作,影响性能。
- Turbo Modules的JS to Native更快:无论是同步调用还是异步调用,JS to Native 调用过程中,Turbo Modules因为可以通过JSI直接调用OC/Java方法,所以其执行过程比。
- Turbo Modules的Native to JS调用较Native Modules慢:实测,Turbo Modules在CallBack过程执行过程比Native Modules执行过程要长一些,表现不佳,具体原因分析中。声明: