likes
comments
collection
share

React Native架构对比

作者站长头像
站长
· 阅读数 37

React Native概览

对于React Native我们首先需要澄清两点:

  1. React Native app使用native view,不是web view
  2. 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老架构

React Native架构对比

如上图,RN老架构中现在主要有3个线程:

  1. JS thread。JS代码执行线程,负责逻辑层面的处理。Metro(打包工具)将React源码打包成一个单一JS文件(就是图中JSBundle)。然后传给JS引擎执行,现在ios和android统一用的是JSC
  2. UI Thread(Main Thread/Native thread)。这个线程主要负责原生渲染(Native UI)和调用原生能力(Native Modules)比如蓝牙等。
  3. 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两侧的通信是异步的,一侧发送了消息后,会异步的等待对方侧处理完。

React Native架构对比

大部分时候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)

React Native架构对比 由于上述提到,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方法:

  1. 声明函数
void Print(const v8::FunctionCallbackInfo<v8::Value>& args);
  1. 定义函数
// 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);
}
  1. 注入函数
// 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 运行环境中,调用我们注入的方法和变量了。

React Native架构对比

总而言之,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。当发生类型不匹配时会触发编译错误。

新架构总结

React Native架构对比

这里的主要亮点是:

  • Bridge将被JSI取代
  • 能够将JavaScriptCore替换成其他JS引擎
  • 所有线程之间的完全互操作性
  • 类似Web的渲染系统
  • 时间敏感的任务可以同步执行
  • Turbo Modules的Native模块可以延迟加载
  • JS侧和Native侧之间的静态类型检测

参考:

juejin.cn/post/709988…

juejin.cn/post/684490…

reactnative.cn/docs/0.68/r…

medium.com/coox-tech/d…

betterprogramming.pub/react-nativ…

www.geeksforgeeks.org/what-is-a-b…