Facebook 在 2018 年 6 月官方宣布了大规模重构 React Native 的计划及重构路线图。目的是为了让 React Native 更加轻量化、更适应混合开发,接近甚至达到原生的体验。
之前我还写了一篇文章分析了下 Facebook 的设计想法。经过这么久的迭代,最近新架构终于有了很多进展,或者说无限接近正式 release 了,很值得和大家分享分享,这篇文章会向大家更深层次介绍新架构的现状和开发流程。
下面我们会从原理上简单介绍新架构带来的一些变化,下图是新老架构的变化对比:
相信大家也能从中发现一些区别,原有架构 JS 层与 Native 的通讯都过多的依赖 bridge,而且是异步通讯,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,而新架构正是从这点,对 bridge 这层做了大量的改造,使得 UI 和 API 调用,从原有异步方式,调整到可以同步或者异步与 Native 通讯,解决了需要频繁通讯的瓶颈问题。
旧架构设计
在了解新架构前,我们还是先聊下目前的 React Native 框架的主要工作原理,这样也方便大家了解整体架构设计,以及为什么 Facebook 要重构整个框架:
通过上面的分析,不难发现现在的架构是强依赖 nativemodule,也就是大家通常说的 bridge,对于简单的 Native API 调用来说性能还能接受,而对于 UI 来说,每次的操作都是需要通过 bridge 的,包括高度计算、更新等,且 bridge 限制了调用频率、只允许异步操作,导致一些前端的更新很难及时反应到 UI 上,特别是类似于滑动、动画,更新频率较高的操作,所以经常能看到白屏或者卡顿。
新架构设计
旧的架构 JS 层与 Native 的通讯都太依赖 bridge,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,这就是 Facebook 这次重构的主要目标,在新的设计上,React Native 提出了几个新的概念和设计:
上面这些概念其实在架构图上已经体现了,主要用于替换原有的 bridge 设计,下面我们将重点剖析这些模块的原理和作用。
JSI
JSI 在 0.60 后的版本就已经开始支持,它是 Facebook 在 JS 引擎上设计的一个适配架构,允许我们向 JavaScript 运行时注册方法的 JavaScript 接口,这些方法可通过 JavaScript 世界中的全局对象获得,可以完全用 C++ 编写,也可以作为一种与 iOS 上的 Objective C 代码和 Android 中的 Java 代码进行通信的方式。任何当前使用 Bridge 在 JavaScript 和原生端之间进行通信的原生模块都可以通过用 C++ 编写一个简单的层来转换为 JSI 模块。
所以 API 调用流程:JS->JSI->C++->JNI->JAVA,每个 API 更加独立化,不再全部依赖 Native module,但这也带来了另外一个问题,相比以前的设计更复杂了,设计一个 API,开发者需要封装 JS、C++、JNI、Java 等一套接口。当然 Facebook 早已经想到了这个问题,所以在设计 JSI 的时候,就提供了一个 codegen 模块,帮忙大家完成基础代码和环境的搭建,以下我们会简单为大家介绍怎么使用 JSI。
1、Facebook 提供了一个脚手架工程,方便大家创建 Native Module 模块,需提前增加 npx 命令。
npx create-react-native-library react-native-simple-jsi
前面的步骤更多的是在配置一些模块的信息,值得注意的是在选择模块的开发语言时要注意,这边是支持很多种类型的,针对原生端开发我们用 Java&OC 比较多,也可以选择纯 JS 或者 C++ 的类型,大家根据自己的实际情况来选择,完成后需要选择是 UI 模块还是 API 模块,这里我们选择 API(Native Module)来做测试:
以上是完成后的目录结构,大家可以看到这是个完整的 ReactNative App 工程,相应的 API 需要开发者在对应的 Android、iOS 目录中开发。
下面我们看下 C++ Moulde 的模式,相比 Java 模式,多了 cpp 模块,并在 Moudle 中以 Native lib 的方式加载 so:
2、其实到这里我们还是没有创建 JSI 的模块,删掉删掉 example 目录后,运行下面命令,完成后在 Android studio 中导入 example/android,编译后 app 工程,就能打包我们 cpp 目录下的 C++ 文件到 so。
npx react-native init example
cd example
yarn add ../
3、到这里我们完成了 C++ 库的打包,但是不是我们想要的 JSI Module,需要修改 Module 模块,代码如下,从代码中我们可以看到,不再有 reactmethod 标记,而是直接的一些 install 方法,在这个 JSI Module 创建的时候调用注入环境。
public class NewswiperJsiModule extends ReactContextBaseJavaModule {
public static final String _NAME_ = "NewswiperJsi";
public NewswiperJsiModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
@NonNull
public String getName() {
return _NAME_;
}
static {
try {
_// Used to load the 'native-lib' library on application startup._
System._loadLibrary_("cpp");
} catch (Exception ignored) {
}
}
private native void nativeInstall(long jsi);
public void installLib(JavaScriptContextHolder reactContext) {
if (reactContext.get() != 0) {
this.nativeInstall(
reactContext.get()
);
} else {
Log._e_("SimpleJsiModule", "JSI Runtime is not available in debug mode");
}
}
}
public class SimpleJsiModulePackage implements JSIModulePackage {
@Override
public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
reactApplicationContext.getNativeModule(SimpleJsiModule.class).installLib(jsContext);
return Collections.emptyList();
}
}
4、后面就是我们要创建 JSI Object 了,用来直接和 JS 通讯,主要是通过 createFromHostFunction 来创建 JSI 的代理对象,并通过 global().setProperty 注入到 JS 运行环境。
void install(Runtime &jsiRuntime) {
auto multiply = Function::createFromHostFunction(jsiRuntime,
PropNameID::forAscii(jsiRuntime,
"multiply"),
2,
[](Runtime &runtime,
const Value &thisValue,
const Value *arguments,
size_t count) -> Value {
int x = arguments[0].getNumber();
int y = arguments[1].getNumber();
return Value(x * y);
});
jsiRuntime.global().setProperty(jsiRuntime, "multiply", move(multiply));
global.multiply(2,4) // 8
到这里相信大家知道了怎么通过 JSI 完成 JSIMoudle 的搭建了,这也是我们 TurboModule 和 Fabric 设计的核心底层设计。
Fabric
Fabric 是新架构的 UI 框架,和原有 UImanager 框架是类似,前面章节也说明 UIManager 框架的一些问题,特别在渲染性能上的瓶颈,似乎基于原有架构已经很难再有优化,体验上与原生端组件和动画的渲染性能还是差距比较大的,举个比较常见的问题,Flatlist 快速滑动的状态下,会存在很长的白屏时间,交互比较强的动画、手势很难支持,这也是此次架构升级的重点,下面我们也从原理上简单说明下新架构的特点:
1、JS 层新设计了 FabricUIManager,目的是支持 Fabric render 完成组件的更新,它采用了 JSI 的设计,可以和 cpp 层沟通,对应 C++ 层 UIManagerBinding,其实每个操作和 API 调用都有对应创建了不同的 JSI,从这里就彻底解除了原有的全部依赖 UIManager 单个 Native bridge 的问题,同时组件大小的 measure 也摆脱了对 Java、bridge 的依赖,直接在 C++ 层 shadow 完成,提升渲染效率。
export type Spec = {|
+createNode: (
reactTag: number,
viewName: string,
rootTag: RootTag,
props: NodeProps,
instanceHandle: InstanceHandle,
) => Node,
+cloneNode: (node: Node) => Node,
+cloneNodeWithNewChildren: (node: Node) => Node,
+cloneNodeWithNewProps: (node: Node, newProps: NodeProps) => Node,
+cloneNodeWithNewChildrenAndProps: (node: Node, newProps: NodeProps) => Node,
+createChildSet: (rootTag: RootTag) => NodeSet,
+appendChild: (parentNode: Node, child: Node) => Node,
+appendChildToSet: (childSet: NodeSet, child: Node) => void,
+completeRoot: (rootTag: RootTag, childSet: NodeSet) => void,
+measure: (node: Node, callback: MeasureOnSuccessCallback) => void,
+measureInWindow: (
node: Node,
callback: MeasureInWindowOnSuccessCallback,
) => void,
+measureLayout: (
node: Node,
relativeNode: Node,
onFail: () => void,
onSuccess: MeasureLayoutOnSuccessCallback,
) => void,
+configureNextLayoutAnimation: (
config: LayoutAnimationConfig,
callback: () => void, // check what is returned here
// This error isn't currently called anywhere, so the `error` object is really not defined
// $FlowFixMe[unclear-type]
errorCallback: (error: Object) => void,
) => void,
+sendAccessibilityEvent: (node: Node, eventType: string) => void,
|};
const FabricUIManager: ?Spec = global.nativeFabricUIManager;
module.exports = FabricUIManager;
if (methodName == "createNode") {
return jsi::Function::createFromHostFunction(
runtime,
name,
5,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const &thisValue,
jsi::Value const *arguments,
size_t count) noexcept -> jsi::Value {
auto eventTarget =
eventTargetFromValue(runtime, arguments[4], arguments[0]);
if (!eventTarget) {
react_native_assert(false);
return jsi::Value::undefined();
}
return valueFromShadowNode(
runtime,
uiManager->createNode(
tagFromValue(arguments[0]),
stringFromValue(runtime, arguments[1]),
surfaceIdFromValue(runtime, arguments[2]),
RawProps(runtime, arguments[3]),
eventTarget));
});
}
2、有了 JSI 后,以前批量依赖 bridge 的 UI 操作,都可以同步的执行到 c++ 层,而在 c++ 层,新架构完成了一个 shadow 层的搭建,而旧架构是在 java 层实现,以下也重点说明下几个重要的设计。
3、新架构下,开发一个原生组件,需要完成 Java 层的原生组件及 ComponentDescriptor (C++) 开发,难度相较于原有的 viewManager 有所提升,但 ComponentDescriptor 本身很多是 shadow 层代码,比较固定,Facebook 后续也会提供 codegen 工具,帮助大家完成这部分代码的自动生成,简化代码难度。
TurboModule
实际上 0.64 版本已经支持 TurboModule,在分析它的设计原理前,我们先说明下设计这个模块的目的,从上面架构图来看,主要用来替换 NativeModule 的重要一环:
1、NativeModule 会包含很多我们初始化过程中就需要注册的的 API,随着开发迭代,依赖 NativeMoude 的 API 和 package 会越来越多,解析及校验这些 pakcages 的时间会越来越长,最终会影响 TTI 时长
2、另外 Native module 其实大部分都是提供 API 服务,其实是可以采用单例子模式运行的,而不用跟随 bridge 的关闭打开,创建很多次
TurboModule 的设计就是为了解决这些问题,原理上还是采用 JSI 提供的能力,方便 JS 可以直接调用到 c++ 的 host object,下面我们从代码层简单分析原理。
上面代码就是目前项目里面给出的一个例子,通过实现 TurboModule 来完 NativeModule 的开发,其实代码流程和原有的 BaseJavaModule 大致是一样的,不同的是底层的实现:
1、现有版本可以通过 ReactFeatureFlags.useTurboModules 来打开这个模块功能
2、TurboModule 组件是通过 TurboModuleManager.java 来管理的,被注入的 modules 可以分为初始化加载的和非初始化加载的组件
3、同样 JNI/C++ 层也有一层 TurboModuleManager 用来管理注册 java/C++ 的 module,并通过 TurboModuleBinding C++ 层的 proxy moudle 注入到 JS 层,到这里基本就和上面说的基础架构 JSI 接上轨了,JS 中可以通过代理的 __turboModuleProxy 来完成 c++ 层的 module 调用,C++ 层透过 JNI 最终完成对 java 代码的执行,这里 facebook 设计了两种类型的 moudles,longLivedObject 和 非常驻的,设计思路上就和我们上面要解决的问题吻合了。
void TurboModuleBinding::install(
jsi::Runtime &runtime,
const TurboModuleProviderFunctionType &&moduleProvider) {
runtime.global().setProperty(
runtime,
"__turboModuleProxy",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "__turboModuleProxy"),
1,
_// Create a TurboModuleBinding that uses the global_
_// LongLivedObjectCollection_
[binding =
std::make_shared<TurboModuleBinding>(std::move(moduleProvider))](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
return binding->jsProxy(rt, thisVal, args, count);
}));
}
const NativeModules = require('../BatchedBridge/NativeModules');
import type {TurboModule} from './RCTExport';
import invariant from 'invariant';
const turboModuleProxy = global.__turboModuleProxy;
function requireModule<T: TurboModule>(name: string): ?T {
// Bridgeless mode requires TurboModules
if (!global.RN$Bridgeless) {
// Backward compatibility layer during migration.
const legacyModule = NativeModules[name];
if (legacyModule != null) {
return ((legacyModule: $FlowFixMe): T);
}
}
if (turboModuleProxy != null) {
const module: ?T = turboModuleProxy(name);
return module;
}
return null;
}
CodeGen
1、新架构 UI 增加了 C++ 层的 shadow、component 层,而且大部分组件都是基于 JSI,因而开发 UI 组件和 API 的流程更复杂了,要求开发者具有 c++、JNI 的编程能力,为了方便开发者快速开发 Facebook 也提供了 codegen 工具,帮助生成一些自动化的代码。
具体工具参看:https://github.com/facebook/react-native/tree/main/packages/react-native-codegen
2、以下是代码生成的大概流程,因 codegen 目前还没有正式 release,关于如何使用的文档几乎没有,但也有开发者尝试使用生成了一些代码,可以参考 https://github.com/karol-bisztyga/codegen-tool。
笔者也试了,暂时行不通,还是等待 Facebook 正式 release,相信使用起来会很简单。
总结
上面我们从 API、UI 角度重新学习了新架构,JSI、Turbormodule 已经在最新的版本上已经可以体验,而且开发者社区也用 JSI 开发了大量的 API 组件,例如以下的一些比较依赖 C++ 实现的模块:
从最新的代码结构来看,新架构离发布似乎已经进入倒计时了,作为一直潜心学习、研究 React Native 的开发者相信一定和我一样很期待,从 Facebook 官方了解到 Facebook App 已经采用了新的架构,预计今年应该就能正式 release 了,这一次我们可以相信 React Native 应该要正式进入 1.0 版本了吧。
开发、迭代效率、收益都有很大的提升,同样我们也在持续关注 React Native 的新架构动态,相信整体方案、性能会越来越好,也期待快速迁移到新架构。