Skip to content

RN 原理篇:渲染原理

一 前言

在前面的 React 运行时章节中讲到了, 在 JS 线程最终的产物是一个由 fiber 元素构建的虚拟 DOM ,并且通过绘制指令通过桥的方式,传递到 Native。那么本章节讲的就是由指令处理到视图的绘制。

因为渲染过程中,RN 的新老架构的实现有区别。

二 老架构渲染原理

在 React Fiber 构建过程中,如果发现有新元素,那么会通过 createView 创建新的元素节点,如果想要更新元素,那么会通过 updateView 更新元素。如下所示:

js
ReactNativePrivateInterface.UIManager.createView(
    tag, // reactTag
    viewConfig.uiViewClassName, // viewName
    rootContainerInstance, // rootTag
    updatePayload // props
);

在 Native 中,会形成 Shadow Tree ,Shadow Tree 是由 Shadow Node 构成。Shadow Tree 是由 Native 生成和 Fiber Tree 一一对应,它可以映射成 Native 控件,计算 Native 控件布局。接下来我们以 createView 为切入点,看一下 Native 会走哪些流程。

在调用 createView 之后,会通过 UIManager 把事件通过桥的方式,传递给 Native 层。我们还是以安卓为例子,最终会调用 UIManagerModule 类的 createView 方法。

java
 @ReactMethod
  public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
    mUIImplementation.createView(tag, className, rootViewTag, props);
  }

UIManagerModule 为 UI 操作的桥,它接收 JS 传递过来的绘制指令,转发给 UIImplementation 类。UIImplementation 为渲染流程的总控制类,负责整个渲染流程。

java
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
    /* 创建 shadow Node , 进而构建出 Shadow Tree */
    ReactShadowNode cssNode = createShadowNode(className);
    /* 将 shadow Node 添加到注册表 */
    mShadowNodeRegistry.addNode(cssNode);
    /* 设置 Shadow node 样式 */
    cssNode.updateProperties(styles);
    /* 处理 Shadow node */
    handleCreateView(cssNode, rootViewTag, styles);
}
protected void handleCreateView() {
    mNativeViewHierarchyOptimizer.handleCreateView(cssNode, cssNode.getThemedContext(), styles);
}
/* 创建一个 shadow Node  */
protected ReactShadowNode createShadowNode(String className) {
    ViewManager viewManager = mViewManagers.get(className);
    return viewManager.createShadowNodeInstance(mReactContext);
}

在 UIImplementation 的 createView 中含有的信息量比较大。先来看一下其中涉及到的核心组成部分。

  • ShadowNodeRegistry 为注册表,可以存放生成的 Node 节点。
  • NativeViewHierarchyOptimizer 负责优化 Shadow Tree 转换成原生的 UI 控件。
  • viewManager :管理对应的原生控件。

createView 方法中,会通过 createShadowNode 创建 shadow Node, 进而构建出 Shadow Tree,并将 shadow Node 插入到注册表中。然后通过 updateProperties 设置样式,最终调用 handleCreateView,进入到 UI 渲染流程中。

shadow Node 可以理解成一种 RN 应用的中间产物,一方面映射 fiber 节点,另一方面计算 Native UI 组件的布局,映射成 Native 组件。

Android 映射关系:

React 组件Shadow NodeNative 组件
<View>LayoutShadowNodeReactViewGroup
<Image>LayoutShadowNodeReactImageView
<Text>ReactTextShadowNodeReactTextView

iOS 映射关系:

React 组件Shadow NodeNative 组件
<View>RCTShadowViewRCTView
<Image>RCTImageShadowViewRTCImageView
<Text>RCTTextShadowViewRTCTextView

接着上面往下说,最终流程到了 NativeViewHierarchyOptimizer 的 handleCreateView 方法中。来看看这个方法主要做了哪些事情?

java
public void handleCreateView(){
    node.setIsLayoutOnly(isLayoutOnly);
    mUIViewOperationQueue.enqueueCreateView()
}

handleCreateView 做了一个非常主要的操作,就是通过 UIViewOperationQueue 类创建一个 UIViewOperation ,见面知意,这个是一个渲染绘制操作指令,Native 会执行这些指令。

  • UIViewOperation 类:这个类存放 Native UI 操作指令队列,里面的指令会以先进先出的顺序执行,最终形成 Native UI。
java
public void enqueueCreateView(){
     mCreateViewCount++;
      mNonBatchedOperations.addLast(
          new CreateViewOperation(themedContext, viewReactTag, viewClassName, initialProps));
}

如上通过 CreateViewOperation 创建一个 Operation 添加到 mNonBatchedOperations 队列中,等待执行。

我们来描述一下整体安卓流程:在 Native 接受到 React 调和过程中 commit 的阶段的指令之后,Native 会通过这些指令构建构建出一个 Shadow Tree, Shadow Tree 会计算布局信息。在这个过程中,会通过 UIViewOperation 生成操作指令,这些指令包括绘制的数据信息,最终会放到操作队列中,Native 会依次执行这些 Operation,完成 UI 构建,视图的还原。

如下所示:

整个流程如下所示:

如上就是 Android 创建元素初始化渲染流程。iOS 对于 Shadow Tree 也是如出一撤,不同的点是 iOS 在构建 Shadow Tree 的时候同时进行创建渲染视图。而 Android 生成一个 Operation, 然后在最后统一执行渲染视图队列,统一更新视图。

如上就是创建流程。在 RN 应用中,如果发生更新,那么会触发不同的指令,比如更新元素会发送 updateView 指令,插入元素会执行 setChildren 指令。

对于元素的更新会走 updateView 流程, 和 createView 流程类似,在 updateView 流程中,首先就是 UIManagerModule 的 updateView 信息,传递给 UIImplementation ,并且调用 updateView 方法。

java
public void updateView(int tag, String className, ReadableMap props) {
    /* 从注册表获取 shadow Node */
    ReactShadowNode cssNode = mShadowNodeRegistry.getNode(tag);
    if (props != null) {
      /* 更新 shadow Node 的样式属性 */  
      cssNode.updateProperties(styles);
      /* 处理更新逻辑 */
      handleUpdateView(cssNode, className, styles);
    }
}

UIImplementation 的 updateView 逻辑,首先从注册表中通过 getNode 获取 shadow Node, 然后如果 props 存在,然后通过 updateProperties 更新 shadow Node 的样式属性,最后调用 handleUpdateView 处理更新逻辑。

最后同样会通过 NativeViewHierarchyOptimizer 的 handleUpdateView 创建 Operation,并且更新视图。

java
public void handleUpdateView() {
   mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
}

整个流程如下所示:

探索形成 shadow Tree 流程

在运行时章节中讲到了如下例子:

js
import { View, Text } from 'react-native'
function App(){
    return <View>
       <Text>小程序:《大前端跨端开发指南》</Text>
       <View>
          <Text>作者:我不是外星人</Text>
       </View>
    </View>
}

如上例子,最终形成的 fiber 结构如下所示:

在之前的章节中,讲解到,会先深度遍地子 fiber ,然后创建 fiber ,并且插入到父元素节点中。那么绘制流程是这样的。

首先会递归遍历到第一个 Text 的文本内容 小程序:《大前端跨端开发指南》,会创建 tag=3 的文本元素节点,并发送 createView 指令,类似指令如下:

js
JS -> N: UIManager.createView([3,'RCTRawText',{ content:'小程序:《大前端跨端开发指南》' }])

在 RN 中对于纯文本节点,会发送 RCTRawText 指令。接下来就是创建 tag =5 的 Text 元素节点。

js
JS -> N: UIManager.createView([5,'RCTText',{}])

接下来需要把 tag = 3 的节点插入到 tag = 5 的节点中,类似如下的指令:

js
JS -> N: UIManager.setChildren(5,[3])

接下来,因为 Text 有 sibling 兄弟节点,那么会遍历到 View 节点,接下来递归到文本节点 作者:我不是外星人 创建 tag = 7 文本节点。

js
JS -> N: UIManager.createView([7,'RCTRawText',{ content:'作者:我不是外星人' }])

接下来会创建 Text 元素节点。

js
JS -> N: UIManager.createView([9,'RCTText',{}])

然后把文本元素插入到 Text 元素节点中,如下所示:

js
JS -> N: UIManager.setChildren(9,[7])

因为 Text 元素没有 sibling 兄弟节点,那么会 return 到父级节点 View ,然后创建 View 节点。

js
JS -> N: UIManager.createView([11,'RCTView',{}])

接下来把 Text 元素插入到 View 元素中。

js
JS -> N: UIManager.setChildren(11,[9])

因为 View 元素没有 sibling 兄弟节点,那么会通过 return 返回到最外层的 View 元素上。并切创建 View 节点。

js
JS -> N: UIManager.createView([13,'RCTView',{}])

有了最外层的 View 节点,接下来把 tag = 9 的 Text 节点,和 tag = 11 的 View 节点插入到最外层 View 里面。指令如下:

js
JS -> N:UIManager.setChildren(13,[5,11])

经过上面的指令, 通过 createView 和 setChildren 创建并插入元素。最终形成 Shadow Tree 如下所示:

三 Fabric 架构渲染原理

在 JSI 的新架构背景下,对于采用了新的渲染架构模型,它就是—Fabric,在老架构中, Shadow Tree 是由 Native 构建,在新的架构中,Shadow Tree 的构建是在 C++ 侧完成的。

在老的架构中,对于渲染指令是通过桥方式实现的,本质上是通过 MessageQueue 的方式,进行信息传递。在新版本的架构中,基于 JSI 可以让 JS 和 C++ 相互感知,那么可以直接在 JS 中操纵构建并更新 Shadow Node。

还是拿创建元素为例子,在 Fabric 架构中,创建元素用的是 createNode 。

js
 var node = createNode(
    tag, // reactTag
    viewConfig.uiViewClassName, // viewName
    rootContainerInstance, // rootTag
    updatePayload, // props
    internalInstanceHandle // internalInstanceHandle
);

createNode 是 global.nativeFabricUIManager 模块提供的方法,该模块是由 C++ 注入到 global 全局状态上的。具体的方式如下所示:

c++
std::shared_ptr<UIManagerBinding> UIManagerBinding::getBinding(
    jsi::Runtime &runtime) {
  /* 声明模块名称 */      
  auto uiManagerModuleName = "nativeFabricUIManager";
  /* 建立双向感知 */
  auto uiManagerValue =
      runtime.global().getProperty(runtime, uiManagerModuleName);
  auto uiManagerObject = uiManagerValue.asObject(runtime);
  return uiManagerObject.getHostObject<UIManagerBinding>(runtime);
}

在 UIManagerBinding 中,通过 getBinding 向 gobal 对象上绑定nativeFabricUIManager。这样在 createNode 的时候,就会触发对应的 get 方法。

c++
jsi::Value UIManagerBinding::get() {
    auto methodName = name.utf8(runtime);
      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));
        });
  }
}

调用 createNode 方法,本质上调用的是 uiManager 的 createNode 方法。 uiManager 为 C++ 层的 Ui 层管理类。

c++
ShadowNode::Shared UIManager::createNode(){
    /* 创建 Shadow Node */
    auto shadowNode = componentDescriptor.createShadowNode()
    return createShadowNode
}

如上就是 ShadowNode 创建的流程,这样在 C++ 会同步完成 Shadow Tree 的构建,和完成布局的计算。这样提高了节点有效复用,提升了性能。

在 C++ 侧形成的 Shadow Tree , 这部分可以被 iOS 和 Android 复用,并且渲染页面。

Fabric 架构和现有架构的差异:

  • 在现有架构中,JS 层通过桥方式把渲染指令传递给 Native 侧,Shadow Tree 构建是在 Native 侧构建的,然后 Native 会根据 Shadow Tree 映射成真正的视图。当元素更新的时候,会把新的 props 传递给 Native 侧,Native 会根据 tag 找到对应的 Shadow Node , 然后进行真正的更新。

  • 在 Fabric 架构中,摒弃了桥通信,利用的 JSI 的 JS 和 C++ 的相互感知,Shadow Tree 由 C++ 侧直接完成构建,然后在由 Native 复用,完成渲染流程。但是在 Fabric 中,Shadow Node 是不可变的,如果发生更新,需要在原有 Shadow Node 上通过新的 props 克隆一个新的 Shadow Node, 并逐级向上克隆出新的节点。节点更新之后,生成一颗新的 Shadow Tree 。

四 总结

本章节介绍 RN 新老架构的渲染流程,描绘了 fiber 树是怎么一步步构建成视图的,读者无需太在意实现细节,最重要的是串联整个渲染流程。

前端知识体系 · wcrane