React Native架构对比
React Native概览
对于React Native我们首先需要澄清两点:
- React Native app使用native view,不是web view
- javascript代码没有被编译成native代码
开发者还是使用js按照React的方式编写代码,React Native将js代码编写的逻辑和Native连接,渲染成Native的UI界面,保持快速响应和平顺的操作,较高的FPS。
任何React Native项目都会包含一个 ios
目录和一个android
目录,这是app的入口。当用户打开app时,用户的手机会找到对应平台的目录,对应的native入口代码会开启一个JS的虚拟机,运行经过Metro
打包的bundle.js。bundle.js运行的JS线程会通过bridge
和native进行交互。
React Native老架构
如上图,RN老架构中现在主要有3个线程:
- JS thread。JS代码执行线程,负责逻辑层面的处理。Metro(打包工具)将React源码打包成一个单一JS文件(就是图中JSBundle)。然后传给JS引擎执行,现在ios和android统一用的是JSC。
- UI Thread(Main Thread/Native thread)。这个线程主要负责原生渲染(Native UI)和调用原生能力(Native Modules)比如蓝牙等。
- Shadow Thread。 这个线程主要是创建Shadow Tree来模拟React结构树。Shadow Tree可以类似虚拟dom。RN使用Flexbox布局,但是原生是不支持,所以Yoga就是用来将Flexbox布局转换为原生平台的布局方式。
JavaScriptCore
JavaScriptCore是一个JS引擎,IOS系统自带了该引擎。Android没有自带,所以React Native会将JSC和app打包在一起,这会增加一点app的体积。另外有一点需要注意,JSC是用户的设备运行js时使用的引擎,但当我们debug时是通过Chrome运行的JS,Chrome通过内置的V8引擎运行JS,通过Websocket和Native的代码进行交互。JSC和V8引擎是不同的js运行环境,所以可能会在Debug时遇到bug,但是在设备上不会出现。
Bridge
RN Bridge是用Java/C++
编写的,它允许JS线程和Native线程通过一个自定义的通信传输协议来通信。
JS线程会决定什么应该渲染在屏幕上(React组件)。比如,它告诉Native线程,"我需要你渲染一个按钮在这里,谢谢”,这一消息通过bridge来发送。这一消息会被序列化为JSON
。消息中除了“渲染按钮”这一信息外,还需要额外的信息,比如按钮的颜色,位置,布局等。这些信息通过shadow thread(Yoga)
计算产生。shadow thread
线程和JS线程同时启动,它计算完View的布局信息,和js的发送的信息一起通过bridge发送给native线程。
任何用户的UI操作行为都发生在Native线程。比如点击按钮等行为都会序列化后通过bridge发动给JS线程。
此外,Bridge两侧的通信是异步的,一侧发送了消息后,会异步的等待对方侧处理完。
大部分时候Bridge的工作方式没问题,但有时候Bridge可能会遇到“交通堵塞”。比如当页面中有大量元素且用户快速滚动时,可能会出现白屏。这是由于Native的滚动事件会经过Bridge传递给JS线程,JS线程将新的布局信息传递给Shadow线程计算,然后将计算后的信息经过Bridge传递给native。
UI Manager
老架构中的UI管理模块,叫做UI Mageger。当你的应用程序运行时,React 会执行你的代码并在 JavaScript 中创建一个 ReactElementTree。基于这棵树,渲染器在 C++ 中创建一个 ReactShadowTree。 布局引擎使用此阴影树来计算主屏幕的 UI 元素的位置。一旦 Layout 计算的结果可用,影子树就会被转换为 HostViewTree,它由 Native Elements 组成。 (例如:ReactNative 元素会被分别翻译成 Android 中的 ViewGroup 和 iOS 中的 UIView)
由于上述提到,UI渲染的数据也需要通过Bridge,所以也有Bridge通信问题。此外,还会存在多余数据的复制。
例如:如果 ReactElementTree 节点恰好是一个
<Image/>
,那么 ReactShadowTree 的后续节点也将是一个image
,但是这些数据必须被复制并分别存储在两个节点中。
React Native新架构
由于上述老架构中的问题,React Native开发了新的架构,在React Native 0.68中,新架构正式发布。新架构中出现几个新术语:JSI,FABRIC,TURO MODULE和CODE GEN。
JSI
新架构中Bridge会被一个叫做JavaScript Interface(JSI)的模块取代。JSI是一个用C++
编写的轻量的通用层,JS引擎可通过它直接调用Native侧的代码。
老架构中Bridge只能和特定的JS引擎(JSC)一起工作,新架构中JSI和JS引擎进行了解耦,意味着可以使用其他JS引擎如,Chakra, v8, Hermes。
有了JSI,Native的方法可以通过C++
暴露给JS,JS可以持有这些C++
对象的引用从而直接调用Native方法。这和Web有点相似,JS可以持有指向任何DOM
的引用。
const container = document.createElement(‘div’);
如上代码,container
是一个JS变量,它持有一个指向DOM
元素的引用,该DOM
是可能是C++初始化的。如果我们调用任何container
上的方法,都会依次调用DOM
上的方法。JSI
以类似的方式运行。
举个例子看C++
如何暴露给JS
,向V8
引擎中注入一个Print
方法:
- 声明函数
void Print(const v8::FunctionCallbackInfo<v8::Value>& args);
- 定义函数
// The callback that is invoked by v8 whenever the JavaScript 'print'
// function is called. Prints its arguments on stdout separated by
// spaces and ending with a newline.
void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
bool first = true;
for (int i = 0; i < args.Length(); i++) {
v8::HandleScope handle_scope(args.GetIsolate());
if (first) {
first = false;
} else {
printf(" ");
}
v8::String::Utf8Value str(args[i]);
const char* cstr = ToCString(str);
printf("%s", cstr);
}
printf("\n");
fflush(stdout);
}
- 注入函数
// Creates a new execution environment containing the built-in
// functions.
v8::Local<v8::Context> CreateShellContext(v8::Isolate* isolate) {
// Create a template for the global object.
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);
// Bind the global 'print' function to the C++ Print callback.
global->Set(
v8::String::NewFromUtf8(isolate, "print", v8::NewStringType::kNormal)
.ToLocalChecked(),
v8::FunctionTemplate::New(isolate, Print));
return v8::Context::New(isolate, NULL, global);
}
v8引擎
中方法和变量的注入,实际上是将需要注入的方法和变量设置到global
当中,这样就完成了向引擎注入的操作,就可以在 Js 运行环境中,调用我们注入的方法和变量了。
总而言之,JSI 将支持其他 JavaScript 引擎的使用,它将允许线程之间的完全互操作性,JavaScript 代码可以直接从 JS 线程与本机端进行通信。这将消除对 JSON 消息进行序列化的需要,并将解决Bridge上的拥塞和异步问题。 JSI 的另一大优势是它是用 C++ 编写的。借助 C++ 的强大功能,React Native 可以针对智能电视、手表等大量系统。
Fabric
“Fabric is React Native’s new rendering system, a conceptual evolution of the legacy render system”
如上文的JSI部分所说,JavaScript接口将直接向JavaScript公开原生方法,其中也包括UI方法。因此,JS 和 UI线程可以同步。这将提高列表、导航、手势处理等的性能。
使用新的渲染系统,用户交互(如滚动、手势等)可以优先在主线程或本机线程中同步执行。而API请求等其他任务将异步执行。
此外,新的 Shadow Tree 将是不可变的,它将在 JS 和 UI 线程之间共享,以允许来自两端的直接交互。在老架构中,React Native必须维护两个层次结构/DOM 节点。但由于影子树现在将在领域之间共享,它也将有助于减少内存消耗。
Turbo Modules
在当前架构中,JavaScript 使用的所有原生模块(例如蓝牙、地理位置、文件存储等)都必须在应用程序打开之前进行初始化。这意味着,即使用户不需要特定模块,它仍然必须在启动时进行初始化。 Turbo Modules 基本上是对这些旧的 Native 模块的增强。如本文前面提到的,现在JavaScript将能够持有对这些模块的引用,这将允许 JS 代码仅在需要时加载每个模块。这将显着缩短 ReactNative 应用程序的启动时间。
CodeGen
JavaScript是一种动态类型语言,而JSI是用C++编写的,它是一种静态类型语言。因此,需要确保两者之间的顺畅通信。这就是为什么新架构还将包括一个名为 CodeGen 的静态类型检查器。CodeGen使用JavaScript组件的Props作为类型声明来源来生成C++ structs。当发生类型不匹配时会触发编译错误。
新架构总结
这里的主要亮点是:
- Bridge将被JSI取代
- 能够将JavaScriptCore替换成其他JS引擎
- 所有线程之间的完全互操作性
- 类似Web的渲染系统
- 时间敏感的任务可以同步执行
- Turbo Modules的Native模块可以延迟加载
- JS侧和Native侧之间的静态类型检测
参考:
转载自:https://juejin.cn/post/7159484908786843662