RN:React Native原理以及新架构JSI、Fabric等概念

说明

RN需要一个JS的运行环境, 在IOS上直接使用内置的javascriptcore, 在Android 则使用webkit.org官方开源的jsc.so。 此外还集成了其他开源组件,如fresco图片组件,okhttp网络组件等。

RN 会把应用的JS代码(包括依赖的framework)编译成一个js文件(一般命名为index.android.bundle), , RN的整体框架目标就是为了解释运行这个js 脚本文件,如果是js 扩展的API, 则直接通过bridge调用native方法; 如果是UI界面, 则映射到virtual DOM这个虚拟的JS数据结构中,通过bridge 传递到native , 然后根据数据属性设置各个对应的真实native的View。

重要术语:

JavaScriptCore(JSC):JavaScript引擎,用于执行JS代码。
Yoga:UI引擎,用于计算元素在用户屏幕上展示的位置。
JSI:JavaScript Interface

架构分析

RN:React Native原理以及新架构JSI、Fabric等概念_第1张图片

老架构

RN:React Native原理以及新架构JSI、Fabric等概念_第2张图片
RN:React Native原理以及新架构JSI、Fabric等概念_第3张图片

Bridge三个特点:

  • 异步。这些消息队列是异步的,无法保证处理事件。
  • 序列化。通过JSON格式来传递消息,每次都要经历序列化和反序列化,开销很大。
  • 批处理。对Native调用进行排队,批量处理。

异步设计的好处是不阻塞,这种设计在大部分情况下性能满足需求,但是在某些情况下就会出问题,比如瀑布流滚动。

当瀑布流向下滑动的时候,需要发请求给服务端拿数据进行下一步渲染。

滚动事件发生在UI thread,然后通过Bridge发给JS thread。JS thread 监听到消息后发请求,服务端返回数据,再通过Bridge返回给Native进行渲染。由于都是异步,就会出现空白模块,导致性能问题。

目前 RN 使用 Bridge Module 来让 JS 和 Native 线程进行通信,每次利用 Bridge 发送数据时,都需要转换为 JSON, 而收到数据时也需要进行解码。

这就意味着 JavaScript 和 Native 直接是隔离的,也就是 JS 线程不能直接调用 Native 线程上的方法。

新架构

FB团队在2018年提出了新架构:
在全新架构中,Bridge 将被一个名为 JavaScript Interface 的模块所代替,它是一个轻量级的通用层,用 C++ 编写,JavaScript Engine 可以使用它直接执行或者调用 native。

通用层代表着:JSI 让 JavaScript 接口将与 Engine 分离,这意味着新架构支持 RN 直接使用其他 JavaScript 引擎,比如 Chakra、v8、Hermes 等等。。
当前的架构使用的是JavaScriptCore引擎,bridge只兼容该引擎。而JSI并非如此,它将JavaScript接口与引擎解耦,这意味着新架构可以使用其他JavaScript引擎,如Chakra,v8,Hermes等,因此它是“通用”的。

RN:React Native原理以及新架构JSI、Fabric等概念_第4张图片
RN:React Native原理以及新架构JSI、Fabric等概念_第5张图片

主要有JSI、Fabric、TurboModules、CodeGen、LeanCode组成。

JSI

JSI是Javascript Interface的缩写,一个用C++写成的轻量级框架,它作用就是通过JSI,JS对象可以直接获得C++对象(Host Objects)引用,并调用对应方法。

  • JSI 将支持其他 JS 引擎;
  • JSI 允许线程之间的同步相互执行,不需要 JSON 序列号等耗费性能的操作;
  • JSI 是用 C++ 编写,以后如果针对电视、手表等其他系统,也可以很方便地移植;

Fabric

Fabric 是新的渲染系统,它将取代当前的 UI Manager。
UI Manager:
当 App 运行时,React 会执行你的代码并在 JS 中创建一个 ReactElementTree ,基于这棵树渲染器会在 C++ 中创建一个 ReactShadowTree。UI Manager 会使用 Shadow Tree 来计算 UI 元素的位置,而一旦 Layout 完成,Shadow Tree 就会被转换为由 Native Elements 组成的 HostViewTree(例如:RN 里的 会变成 Android 中的 ViewGroup 和 iOS 中的 UIView)。

而之前线程之间的通信都发生在 Bridge 上,这就意味着需要在传输和数据复制上耗费时间。通过JSON格式来传递消息,每次都要经历序列化和反序列化。
而得益于前面的 JSI, JS 可以直接调用 Native 方法,其实就包括了 UI 方法,所以 JS 和 UI 线程可以同步执行从而提高列表、跳转、手势处理等的性能。

Turbo Modules

在之前的架构中 JS 使用的所有 Native Modules(例如蓝牙、地理位置、文件存储等)都必须在应用程序打开之前进行初始化,这意味着即使用户不需要某些模块,但是它仍然必须在启动时进行初始化。

Turbo Modules 基本上是对这些旧的 Native 模块的增强,正如在前面介绍的那样,现在 JS 将能够持有这些模块的引用,所以 JS 代码可以仅在需要时才加载对应模块,这样可以将显着缩短 RN 应用的启动时间。

Codegen

Fabric和Turbo Modules听起来很有前途,但是JavaScript是一门动态语言,而JSI是用C++写的,C++是一门静态语言,因此需要保证两者间的顺利通信。

这就是新架构还包括一个名为CodeGen的静态类型检查器的原因。

CodeGen使用类型确定后的JavaScript来为Turbo Modules和Fabric定义供他们使用的接口元素,并且它会在构建时生成更多的native代码,而非运行时。

skia

众所周知,Flutter 跨平台的性能提升和解耦来自于直接使用 Skia 渲染而非系统控件,而如今 RN 也有类似的支持。

react-native-skia 需要 react-native@>=0.66 的支持,而目前它上面的操作都还是十分原始的 canvas 行为,例如通过 Circle 绘制圆形,通过 blendMode 配置重叠模式等。

可以预见目前的 react-native-skia 还有不少问题需要解决,但是它让 RN 可以更高效地使用丰富的 Canvas 能力,对于 RN 的未来而言不免是一次不错的尝试。

主要亮点总结

  • Bridge会被JSI取代
  • 可以用其他引擎替代JavaScriptCore
  • 所有线程间可以完全互相操作
  • Web式的渲染系统
  • 对时间敏感的任务可以同步执行
  • Turbo Modules实现模块懒加载
  • JS端和Native端的静态类型检查

RN的三个线程

1、Main Thread

主线程又称 UI thread,主要负责 UI 的渲染以及用户行为的监听等等,是 App 启动时首先创建的线程

2、JavaScript Thread

通过 JavaScript Core 或者是 Hermes 引擎,JavaScript 代码的解析和执行是由 JS 线程负责的。和浏览器环境不同的是, JS 代码解析和执行在独立 JS 线程,而不是和 UI 渲染共用一个线程。

3、Shadow Thread

前两个线程都比较好理解,那 Shadow 线程是做什么的呢?要回答这个问题,首先我们需要理解 React 的原理。

我们都知道在浏览器环境 React DOM 会维护一个 Virtual DOM,Virtual DOM 实际上是在内存中的一个树形结构(Tree-Like)的 JS Object,用来映射真实的 DOM Node Tree。每次 State 更新后,React 会生成一个新的 Virtual DOM,通过和旧的 virtual DOM 做对比(Diffing),从而仅仅将需要变更的部分同步到真实 DOM(Reconciliation),从而减少 Dom Tree 的变更,从而减少浏览器的绘制工作。

与之类似的是,在 React Native,React 也会维护一个类似的 “Virtual DOM”,也会有 Diffing 的过程。但是不同于浏览器环境中 React DOM 直接调用浏览器 API 来完成真正的 DOM 更新操作,Native 环境下 React Native 是通过 Bridge ,将需要变更的指令(Commands)以字符串的方式发送到 Native Side,而对应在 Native 这边负责处理这些指令的进程就是 Shadow Thread。

Shadow Thread 通过维护一个 Shadow Tree 来计算 “Virtual DOM” 在 Native 页面的实际布局,然后通过 Bridge 异步通知 Main Thread 渲染 UI。

Shadow Tree 可以理解为是 “Virtual DOM” 在 Native 的映射,拥有和 Virtual DOM 相同的树形层级关系

RN是如何渲染的

将 React 代码渲染到宿主平台,我们称为渲染流水线,可大致分为三个阶段:

  • 渲染(Render):在 JavaScript 中,React 执行那些产品逻辑代码创建 React 元素树(React Element Trees)。然后在 C++ 中,用 React 元素树创建 React 影子树(React Shadow Tree)。
  • 提交(Commit):在 React 影子树完全创建后,渲染器会触发一次提交。这会将 React 元素树和新创建的 React 影子树的提升为“下一棵要挂载的树”。 这个过程中也包括了布局信息计算。
  • 挂载(Mount):React 影子树有了布局计算结果后,它会被转化为一个宿主视图树(Host View Tree)。

渲染流水线存在三种不同场景:

  • 初始化渲染
  • React 状态更新
  • React Native 渲染器的状态更新

场景1:初始化渲染

例如要渲染该组件

function MyComponent() {
  return (
    <View>
      <Text>Hello, World</Text>
    </View>
  );
}

// 

1、render phase

  • JS 线程运行,Virtual DOM Tree 被创建
  • JS 线程通知 Shadow Thread 创建 Shadow Tree
    RN:React Native原理以及新架构JSI、Fabric等概念_第6张图片
    2、commit phase
  • 布局计算(Layout Calculation):通过 Yoga 布局引擎来计算每个 React 影子节点的位置和大小
  • 树提升,从新树到下一棵树(Tree Promotion,New Tree → Next Tree):这次提升代表着这个树是最新的,可以在下一次cpu轮转的时候挂载

RN:React Native原理以及新架构JSI、Fabric等概念_第7张图片3、mount phase
视图挂载(View Mounting):这个步骤会在对应的原生视图上执行原子变更操作,该步骤是发生在原生平台的 UI 线程的。
RN:React Native原理以及新架构JSI、Fabric等概念_第8张图片

场景2:React 状态更新

https://www.react-native.cn/docs/render-pipeline
根据上面的渲染得到了下面的树
RN:React Native原理以及新架构JSI、Fabric等概念_第9张图片
我们准备把3红色的背景变为黄色:
1、render phase
RN:React Native原理以及新架构JSI、Fabric等概念_第10张图片
3要变化,所以复制了1,2,3。没有复制 4
2、commit phase

  • 布局计算(Layout Calculation):通过 Yoga 布局引擎来计算每个 React 影子节点的位置和大小
  • 树提升,从新树到下一棵树(Tree Promotion,New Tree → Next Tree):这次提升代表着这个树是最新的,可以在下一次cpu轮转的时候挂载
  • 树对比(Tree Diffing): 这个步骤会计算“先前渲染的树”(T)和“下一棵树”(T’)的区别。计算的结果是原生视图的变更操作。
    在上面的例子中,这些操作包括:UpdateView(**'Node 3'**, {backgroundColor: 'yellow'})

3、mount phase
视图挂载(View Mounting):同场景1
RN:React Native原理以及新架构JSI、Fabric等概念_第11张图片

场景3:React Native 渲染器的状态更新

C++ 组件可以拥有状态,如果C++状态改变了怎么渲染呢,这种场景很少,我们在此不做深入了解,想了解的话看链接。
https://www.react-native.cn/docs/render-pipeline

从入口代码开始分析

入口文件:

import { AppRegistry } from 'react-native';
AppRegistry.registerComponent('wiseDerma', () => App);

这样,js 就在 app 端注册好了,接着就等待Native事件驱动渲染JS端定义的APP组件。

通信机制

JS调用java

java部分:
只需要在函数上加上ReactMethod注解就可以了
RN:React Native原理以及新架构JSI、Fabric等概念_第12张图片
js部分:
JS调用java 使用通过扩展模块require(‘NativeModules’)获取native模块,然后直接调用native公开的方法

var { NativeModules } = require('react-native');
NativeModules.initialize

JS 调用require(‘NativeModules’)实际上是获取MessageQueue里面的一个native模块列表的属性。

java调用JS

Native端调用Web端,这个比较简单,JavaScript作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段JS代码,所以可以将拼接的JavaScript代码字符串,传入JS解析器执行就可以,JS解析器在这里就是webView。

mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
            //这里的value即为对应JS方法的返回值
        }
});

参考资料

https://cloud.tencent.com/developer/article/1751479
https://cloud.tencent.com/developer/article/1036325
https://zhuanlan.zhihu.com/p/281238593
https://mp.weixin.qq.com/s?__biz=MzI1NTg3NzcwNQ==&mid=2247485511&idx=1&sn=a0d97607924513c9d794e8d4fe21072f&utm_source=tuicool&utm_medium=referral
https://mp.weixin.qq.com/s/6q5r9CMmIL3O5yyHRTU5FA
简书-2022 年 React Native 的全新架构更新
yoga
官方文档架构介绍
React Native 渲染原理

你可能感兴趣的:(#,app,&,react,native,react,native,架构,fabric)