线程模型
渲染器使用三个不同的线程:
- UI 线程(主线程):唯一可以操作宿主视图的线程。
- JavaScript 线程:这是执行 React 渲染阶段的地方。
- 后台线程:专门用于布局的线程。
系统有自己的布局实现方式,它并不理解你刚刚编写的 Flexbox 代码。因此,RN 首先必须将您的 Flexbox 编码布局转换为系统可以理解的布局系统。
继续来看,在转换之前,我们需要将此布局计算部分转交到另一个线程,以便我们可以保持 JavaScript 线程继续执行。因此,RN使用了影子线程,它本质上是在构建了JS线程中编码的布局树。在这个线程中,RN 使用了一个名为 Yoga 的布局引擎,它将基于 Flexbox 的布局将其转换为系统可以理解的布局。最后需要UI线程也叫主线程渲染在终端上。
官方fabric介绍
对比老板bridge通信的优点
1.增加同步以及优先级控制,减少bridge异步带来的抖动问题
2.减少通信传参成本,React 使用序列化 JSON 在 JavaScript 和宿主平台之间传递数据。新的渲染器用 JSI(JavaScript Interface)直接获取 JavaScript 数据。
3.配合codegen类型检查减少类型检查等成本加快速度的同时减少出错
4.性能优化,使比如试图拍平成为可能
5.更快的启动速度,因为默认宿主组件是懒加载的
6.将渲染逻辑从 Native(Android/iOS) 侧统一到 C++ 侧。这带来的好处是类似的逻辑无需在 Android 和 iOS 两侧各维护一份,同时也为将来接入更多的 Native 平台做好了准备。
官方介绍渲染,提交与挂载过程
渲染阶段
- 渲染(Render):在 JavaScript 中,React 执行那些产品逻辑代码创建 React 元素树(React Element Trees)。然后在 C++ 中,用 React 元素树创建 React 影子树(React Shadow Tree)。
- 提交(Commit):在 React 影子树完全创建后,渲染器会触发一次提交。这会将 React 元素树和新创建的 React 影子树的提升为“下一棵要挂载的树”。 这个过程中也包括了布局信息计算。
- 挂载(Mount):React 影子树有了布局计算结果后,它会被转化为一个宿主视图树(Host View Tree)。
阶段一渲染,在 React 影子树创建完成后,渲染器触发了一次 React 元素树的提交。
- 创建 React 影子节点、创建两个影子节点的父子关系的操作是同步的,也是线程安全的。该操作的执行是从 React(JavaScript)到渲染器(C++)的,大部分情况下是在 JavaScript 线程上执行的。(译注:后面线程模型有解释)
- React 元素树和元素树中的元素并不是一直存在的,它只一个当前视图的描述,而最终是由 React “fiber” 来实现的。每一个 “fiber” 都代表一个宿主组件,存着一个 C++ 指针,指向 React 影子节点。这些都是因为有了 JSI 才有可能实现的。学习更多关于 “fibers” 的资料参考这篇文档。
- React 影子树是不可变的。为了更新任意的 React 影子节点,渲染器会创建了一棵新的 React 影子树。为了让状态更新更高效,渲染器提供了 clone 操作。更多细节可参考后面的 React 状态更新部分。
阶段二提交,布局计算(yoga)和树的提升(下一棵树会在 UI 线程下一个“tick”进行挂载)。
- 这些操作都是在后台线程中异步执行的。
- 绝大多数布局计算都是 C++ 中执行,只有某些组件,比如 Text、TextInput 组件等等,的布局计算是在宿主平台执行的。文字的大小和位置在每个宿主平台都是特别的,需要在宿主平台层进行计算。为此,Yoga 布局引擎调用了宿主平台的函数来计算这些组件的布局。
阶段三,挂载
站在更高的抽象层次上,React Native 渲染器为每个 React 影子节点创建了对应的宿主视图,并且将它们挂载在屏幕上。在上面的例子中,渲染器为<View> 创建了android.view.ViewGroup 实例
- 树对比(Tree Diffing): 这个步骤完全用的是 C++ 计算的,会对比“已经渲染的树”(previously rendered tree)和”下一棵树”之间的差异。计算的结果是一系列宿主平台上的原子变更操作,比如
createView,updateView,removeView,deleteView等等。在这个步骤中,还会将 React 影子树拍平,来避免不必要的宿主视图创建。关于视图拍平的算法细节可以在后文找到。
ps:
视图拍平(View Flattening)是 React Native 渲染器避免布局嵌套太深的优化手段。
- **树提升,从下一棵树到已渲染树(Tree Promotion,Next Tree → Rendered Tree):**在这个步骤中,会自动将“下一棵树”提升为“先前渲染的树”,因此在下一个挂载阶段,树的对比计算用的是正确的树。
- **视图挂载(View Mounting):**这个步骤会在对应的原生视图上执行原子变更操作,该步骤是发生在原生平台的 UI 线程的。
- 挂载阶段的所有操作都是在 UI 线程同步执行的。如果提交阶段是在后台线程执行,那么在挂载阶段会在 UI 线程的下一个“tick”执行。另外,如果提交阶段是在 UI 线程执行的,那么挂载阶段也是在 UI 线程执行。
- 挂载阶段的调度和执行很大程度取决于宿主平台。例如,当前 Android 和 iOS 挂载层的渲染架构是不一样的。
- 在初始化渲染时,“先前渲染的树”是空的。因此,树对比(tree diffing)步骤只会生成一系列仅包含创建视图、设置属性、添加视图的变更操作。而在接下来的 React 状态更新场景中,树对比的性能至关重要。
- 在当前生产环境的测试中,在视图拍平之前,React 影子树通常由大约 600-1000 个 React 影子节点组成。在视图拍平之后,树的节点数量会减少到大约 200 个。在 iPad 或桌面应用程序上,这个节点数量可能要乘个 10。
状态更新
渲染:更新部分需要从根到更新的部分,不变的部分可以直接共享来节省内存
提交:
- **布局计算(Layout Calculation):**状态更新时的布局计算,和初始化渲染的布局计算类似。一个重要的不同之处是布局计算可能会导致共享的 React 影子节点被复制。这是因为,如果共享的 React 影子节点的父节点引起了布局改变,共享的 React 影子节点的布局也可能发生改变。
- 树提升(Tree Promotion ,New Tree → Next Tree): 和初始化渲染的树提升类似。
- 树对比(Tree Diffing): 这个步骤会计算“先前渲染的树”(T)和“下一棵树”(T' )的区别。计算的结果是原生视图的变更操作。
-
- 在上面的例子中,这些操作包括:
UpdateView(**Node 3'**, {backgroundColor: '“yellow“})
- 在上面的例子中,这些操作包括:
挂载:类似
rn与react native对比理解
其他补充
TurboModules:新的TurboModules方法允许JavaScript代码只在真正需要的时候才加载每个模块,并对其持有直接引用,这意味着不再需要在旧桥上使用批量JSON消息进行通信。
jsi:可以理解成适配器
可以看到 JSI 是一个用于在 C++ 应用中嵌入 JavaScript 引擎的轻量 API。Fabric 用它进行 C++ 部分和 React 部分的通信。
这部分可以分为两点来理解:
- 在 C++ 应用中嵌入 JavaScript 引擎的轻量 API。
- 用于 C++ 和 JavaScript 的通信(因为 React 部分在 JavaScript 侧)。
从前vs现在
之前使用 Bridge 通信的一个主要原因就是 C++ 中的函数没办法完整映射到 JavaScript 中,让 JavaScript 直接调用,所以只能选择以序列化字符串的形式通过 Bridge 传输。而 JSI 做的事情就是将 C++ 中的常用类型一一映射到 JavaScript 中,这样带来的好处是在 C++ 中定义的对象和函数,就可以做到映射到 Javacript 中。这时再挂载在 JavaScript 侧的 global 对象上,那么就可以在 JavaScript 侧随时调用。
参考文章: