有段时间没更新博客了,之前计划由浅到深、从应用到原理,更新一些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相关区别,下方会进行详细展开。
下方是官方给出的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与Native Modules进行了对比,关于Turbos Modules相关的内容下方会详细展开。
本篇wiki中的示例是基于RN官方的“RCTSampleTurboModule”来展开分析,该示例中使用Turbo Modules在Native侧定义导出一系列的方法,然后在JS侧进行调用。其中有异步方法,也有同步方法,下方是核心代码所在位置以及运行效果。
首先通过官方示例来分析Turbo Modules的使用方式,在官方示例中创建了一个SampleTurboModule,在SampleTurboModule中导出了一系列的Native方法供JS使用,其功能与Native Modules所做的事情一致,但是其实现方式上有着本质区别,下方是相关调用过程。
Turbo 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);
下方使用C++编写的NativeSampleTurboModuleSpecJSI即基于JSI为SampleTurboModule提供的具体实现类。该类继承自ObjCTurboModule,而ObjCTurboModule继承自TurboModule类,而TurboModule类继承自HostObject类。该类是CodeGen自动生成。
相关代码截图
而上述的JSI_EXPORT本质上是__attribute__((visibility("default")))的宏定义,该属性用于设置动态链接库中类的可见性。
#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,并返回相关值。
上述方法中四个参数如下所示:
该方法实现中,调用了turboModule的invokeObjCMethod方法。invokeObjCMethod中传入了getString方法的SEL,其中就会执行Objective-C中对应的getString方法,并把返回值返回出去。在invokeObjCMethod方法中,首先获取当前Module的名称和方法,然后开始打点。红框中是关键代码,如果是Promise,则创建对应的回调,否则直接调用performMethodInvocation执行相关方法。
上述代码中的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的注册过程如下:
注册器初始化后,可以在JS侧调用相关__turboModuleProxy来获取对象了,具体流程如下:
平台 | iOS | Android |
---|---|---|
流程 |
在JS侧会调用TurboModuleRegistry.getEnforcing方法来加载自定义的Turbo Modules,具体代码如下所示:
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 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++实例写入缓存并返回。
上一部分对Turbo Modules的创建过程进行了重点介绍,该部分注重介绍Turbo Modules对象的销毁过程(以iOS侧为例):
Turbo Modules的生命周期也是与RCTBridge绑定的,当RCTBridge对象被释放时,会发通知清除当前创建的Turbo Modules实例。在官方示例的AppDelete及RCTRootView创建时都会创建RCTBridge对象,也就是说Turbo Modules的生命周期是与RCTRootView的生命周期一致。具体分析如下:
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生成代码的工具,亲测可用。(官方链接)
参考:
除了上述FB的rn codegen,而微软也开源了一款rn-tscodegin(Github地址),目的是根据TypeScript的接口,来生成Turbo Modules。在RN工程中亲测可用。
官方相关文档在介绍Turbo Modules的优化点时,没有介绍其在通信过程中的优化点。本部分作为扩充,通过相关示例来探究Turbo Modules的通信过程中所做的事情。首先是线程切换上,其次是异步调用过程中的耗时探究。具体如下所示。
同步方法:Turbo Modules同步方法的调用过程不存在线程切换问题,依旧是在JS线程。
异步方法:在CallBack或者Promise方法执行时,会走到下方的方法中,该方法调用了dispatch_async,如果methodQueue,是主线程对应的队列,那么就会切换到主线程中。
iOS调试验证截图
同步调用
异步调用:
在iOS侧上述的methodQueue是在RCTBridgeModule代理的methodQueue方法提供,该方法会在桥定义时进行实现。方法中如果返回的是主队列,那么就会切换到主线程。如果是创建的新队列,则会创建一个新的线程。
Android侧的线程切换过程与iOS侧大同小异,篇幅有限,就不做过多赘述。下方是安卓侧线程切换相关流程。
同步调用无论在Native Modules和Turbo Modules中,执行过程都非常快(1~5ms),而异步桥的调用过程会慢一些(50 ~ 150ms之间)。本部分着重探究异步桥的调用耗时。
首先对Turbo Modules与Native Modules的异步桥调用进行了测试和分析,下方是相关数据,对应结果如下:
测试机型:
测试场景:
测试次数:
平台 | 模块 | 总耗时(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 |
callback过程,即一次Native to JS的执行过程。通过调试可发现,最终是jsInvoke.invokeAsyn方法中的callback.call()方法耗时比较严重,该调用占据了整个Turbo Modules异步调用过程中的约95%的耗时。通过工具调试定位,具体执行方法的耗时落在了Hermes引擎中的相关方法的执行上(Native Modules也有同样的问题)。
具体是Hermes引擎的哪些操作比较耗时?如何对其进行优化?最终能优化多少?在JSC和V8引擎上Turbo Modules表现如何?欲知后事如何,请听下回分解。
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有明显优势。但是在异步桥调用的过程中,优势并不明显,而且有较大优化空间。具体总结如下: