怎么理解React Native的新架构?
共 11961字,需浏览 24分钟
·
2021-08-31 23:06
Facebook 在 2018 年 6 月官方宣布了大规模重构 React Native 的计划及重构路线图。目的是为了让 React Native 更加轻量化、更适应混合开发,接近甚至达到原生的体验。
之前我还写了一篇文章分析了下 Facebook 的设计想法。经过这么久的迭代,最近新架构终于有了很多进展,或者说无限接近正式 release 了,很值得和大家分享分享,这篇文章会向大家更深层次介绍新架构的现状和开发流程。
下面我们会从原理上简单介绍新架构带来的一些变化,下图是新老架构的变化对比:
相信大家也能从中发现一些区别,原有架构 JS 层与 Native 的通讯都过多的依赖 bridge,而且是异步通讯,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,而新架构正是从这点,对 bridge 这层做了大量的改造,使得 UI 和 API 调用,从原有异步方式,调整到可以同步或者异步与 Native 通讯,解决了需要频繁通讯的瓶颈问题。
在了解新架构前,我们还是先聊下目前的 React Native 框架的主要工作原理,这样也方便大家了解整体架构设计,以及为什么 Facebook 要重构整个框架:
ReactNative 是采用前端的方式及 UI 渲染了原生的组件,他同时提供了 API 和 UI 组件,也方便开发者自己设计、扩展自己的 API,提供了 ReactContextBaseJavaModule、ViewGroupManager,其中 ReactNative 的 UI 是通过 UIManger 来管理的,其实在 Android 端就是 UIManagerModule,原理上也是一个 BaseJavaModule,和 API 共享一个 native module。
ReactNative 页面所有的 API 和 UI 组件都是通过 ReactPackageManger 来管理的,引擎初始化 instanceManager 过程中会读取注入的 package,并根据名称生成对应的 NativeModule 和 Views,这里还仅仅是 Java 层的,实际在 C++ 层会对应生成 JNativeModule。
切换到以上架构图的部分来看,Native Module 的作用就是打通了前端到原生端的 API 调用,前端代码运行在 JSC 的环境中,采用 C++ 实现,为了打通到 native 调用,需要在运行前注入到 global 环境中,前端通过 global 对象来操作 proxy Native Module,继而执行了 JNativeModule。
前端代码 render 生成 UI diff 树后,通过 ReactNativeRenderer 来完成对原生端的 UIManager 的调用,以下是具体的 API,主要作用是通知原生端创建、更新 View、批量管理组件、measure 高度、宽度等。
通过上述一系列的 API 操作后,会在原生端生成 shadow tree,用来管理各个 node 的关系,这点和前端是一一对应的,然后待整体 UI 刷新后,更新这些 UI 组件到 ReactRootView。
通过上面的分析,不难发现现在的架构是强依赖 nativemodule,也就是大家通常说的 bridge,对于简单的 Native API 调用来说性能还能接受,而对于 UI 来说,每次的操作都是需要通过 bridge 的,包括高度计算、更新等,且 bridge 限制了调用频率、只允许异步操作,导致一些前端的更新很难及时反应到 UI 上,特别是类似于滑动、动画,更新频率较高的操作,所以经常能看到白屏或者卡顿。
旧的架构 JS 层与 Native 的通讯都太依赖 bridge,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,这就是 Facebook 这次重构的主要目标,在新的设计上,React Native 提出了几个新的概念和设计:
JSI(JavaScript interface):这是本次架构重构的核心重点,也正是因为这层的调整,将原有重度依赖的 native bridge 架构解耦,实现了自由通讯。
Fabric:依赖 JSI 的设计,并将旧架构下的 shadow tree 层移到 C++ 层,这样可以透过 JSI,实现前端组件对 UI 组件的一对一控制,摆脱了旧架构下对于 UI 的异步、批量操作。
TuborModule:新的原生 API 架构,替换了原有的 Java module 架构,数据结构上除了支持基础类型外,开始支持 JSI 对象,让前端和客户端的 API 形成一对一的调用
社区化:在不断迭代中,Facebook 团队发现,开源社区提供的组件和 API 越来越多,而且很多组件设计和架构上比 React Native 要好,而且官方组件因为资源问题,投入度并不够,对于一些社区问题的反馈,响应和解决问题也不太及时。社区化后,大量的系统组件会开放到社区中,交个开发者维护,例如现在的 webview 组件。
上面这些概念其实在架构图上已经体现了,主要用于替换原有的 bridge 设计,下面我们将重点剖析这些模块的原理和作用。
JSI 在 0.60 后的版本就已经开始支持,它是 Facebook 在 JS 引擎上设计的一个适配架构,允许我们向 JavaScript 运行时注册方法的 JavaScript 接口,这些方法可通过 JavaScript 世界中的全局对象获得,可以完全用 C++ 编写,也可以作为一种与 iOS 上的 Objective C 代码和 Android 中的 Java 代码进行通信的方式。任何当前使用 Bridge 在 JavaScript 和原生端之间进行通信的原生模块都可以通过用 C++ 编写一个简单的层来转换为 JSI 模块。
标准化的 JS 引擎接口,React Native 可以替换 v8、Hermes 等引擎。
它是架起 JS 和原生 java 或者 Objc 的桥梁,类似于老的 JSBridge 架构的作用,但是不同的是采用的是内存共享、代理类的方式,JS 所有的运行环境都是在 JSRuntime 环境下的,为了实现和 native 端直接通讯,我们需要有一层 C++ 层实现的 JSI::HostObject,该数据结构支持 propName, 同时支持从 JS 传参。
原有 JS 与 Native 的数据沟通,更多的是采用 JSON 和基础类型数据,但有了 JSI 后,数据类型更丰富,支持 JSI Object。
所以 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 是新架构的 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 层实现,以下也重点说明下几个重要的设计。
FabricUIManager (JS,Java) ,JS 端和原生端 UI 管理模块。
UIManager/UIManagerBinding(C++),C++ 中用来管理 UI 的模块,并通过 binding JNI 的方式通过 FabricUIManager(Java) 管理原生端组件
ComponentDescriptor (C++) ,原生端组件的唯一描述及组件属性定义,并注册在 CoreComponentsRegistry 模块中
Platform-specific
Component Impl (Java,ObjC++),原生端组件 Surface,通过 FabricUIManager 来管理
3、新架构下,开发一个原生组件,需要完成 Java 层的原生组件及 ComponentDescriptor (C++) 开发,难度相较于原有的 viewManager 有所提升,但 ComponentDescriptor 本身很多是 shadow 层代码,比较固定,Facebook 后续也会提供 codegen 工具,帮助大家完成这部分代码的自动生成,简化代码难度。
实际上 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;
}
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++ 实现的模块:
https://github.com/mrousavy/react-native-vision-camera
https://github.com/mrousavy/react-native-mmkv
https://github.com/mrousavy/react-native-multithreading
https://github.com/software-mansion/react-native-reanimated
https://github.com/BabylonJS/BabylonReactNative
https://github.com/craftzdog/react-native-quick-base64
https://github.com/craftzdog/react-native-quick-md5
https://github.com/greentriangle/react-native-leveldb
https://github.com/expo/expo/tree/master/packages/expo-gl
https://github.com/ospfranco/react-native-quick-sqlite
https://github.com/ammarahm-ed/react-native-mmkv-storage
从最新的代码结构来看,新架构离发布似乎已经进入倒计时了,作为一直潜心学习、研究 React Native 的开发者相信一定和我一样很期待,从 Facebook 官方了解到 Facebook App 已经采用了新的架构,预计今年应该就能正式 release 了,这一次我们可以相信 React Native 应该要正式进入 1.0 版本了吧。
开发、迭代效率、收益都有很大的提升,同样我们也在持续关注 React Native 的新架构动态,相信整体方案、性能会越来越好,也期待快速迁移到新架构。