RN 的新渲染器 Fabric

本文所有代码如果没有特别标注的话,默认用的都是 v0.76.0 的 RN 代码

设计目标

Fabric Renderer 是 React Native 新架构中的核心渲染系统RN 官网 称其为旧渲染系统的概念性演进

Fabric 的核心设计原则也很简单,只有一句话:在 C++ 层尽可能多的统一渲染逻辑

为什么呢?我们来看看 RN bridge 架构的渲染系统:

js 复制代码
┌──────┐      ┌────────┐      ┌───────────────────────────────┬──────────────────────────────────┐
│  JS  │ ───> │ Bridge │ ───> │      iOS UIManager            │       Android UIManager          │
└──────┘      └────────┘      └──────────────┬────────────────┴──────────────────┬───────────────┘
                                             │                                   │
                                             v                                   v
                                 ┌─────────────────────┐             ┌─────────────────────┐
                                 │   iOS Shadow Tree   │             │ Android Shadow Tree │
                                 └──────────┬──────────┘             └──────────┬──────────┘
                                            │                                   │
                                            v                                   v
                                 ┌─────────────────────────────────────────────────────────┐
                                 │                       Yoga Layout(C++)                  │
                                 └──────────┬───────────────────────────────────┬──────────┘
                                            │                                   │
                                            v                                   v
                                 ┌─────────────────────┐             ┌─────────────────────┐
                                 │     iOS Commit      │             │   Android Commit    │
                                 └──────────┬──────────┘             └──────────┬──────────┘
                                            │                                   │
                                            v                                   v
                                 ┌─────────────────────┐             ┌─────────────────────┐
                                 │      iOS Mount      │             │   Android Mount     │
                                 └─────────────────────┘             └─────────────────────┘

Bridge 时期的 UIManager 是平台相关的,也就是说,IOS 跟 Android 分别有一套适配自己平台的实现

其内部的 shadow tree 构建、commit、mount 都是平台自己的实现,只有 layout 的部分有统一的 yoga 实现

在专栏前面的文章中我们聊过,RN 架构演进的其中一个动力是 React 18 引入的新特性 Concurrent

  • 可中断渲染
  • 优先级调度
  • 同步 / 异步混合更新

要做好 Concurrent,最重要的是做好这三件事:可中断、多版本、可回滚

这三件事最后指向了唯一结果:Immutable 数据结构(Immutable tree)

而 UIManager 这种指令驱动、不可中断的 mutable 渲染系统注定是无法支撑新架构的目标的

如果我们仔细看看 IOS/Android 的 UIManager,我们不难发现其中只有 mount 的工作是平台相关的,其他部分都可以统一成一套 C++ 实现,使用 C++ 的好处有:

  1. 新架构中的 JSI 已经解决了 JS 与 C++ 的通信问题,使用 C++ 可以更好处理调度任务,避免 "跨语言断层"
  2. C++ 能提供精细内存控制高性能对象创建/销毁,这个对 Immutable tree 来说非常重要
  3. C++ 天生满足跨平台"单一实现",能最大程度满足跨平台一致性

最后,我们可以得到 Fabric 系统的雏形

心智模型

在了解 Fabric 是怎么实现的之前,我们先来聊聊 RN 渲染的流程,建立正确的心智模型~

RN 官方博客 把 RN 的渲染流程分成了三个阶段:

  • 渲染 Render :描述了从 React 创建 React Element Tree 到 RN 基于该树创建 React Shadow Tree 的过程,最后的产物是 React Shadow Tree
  • 提交 Commit :React Shadow Tree 创建后就进入了提交阶段,提交阶段有两个重要任务:
    1. Layout Calculation:用 yoga 引擎计算布局
    2. Tree Promotion (New Tree → Next Tree):把计算好布局的 Shadow tree 标记为 "next tree"
  • 挂载 Mount :当 shadow tree 计算完 layout 后,就进入挂载阶段,这个阶段是平台相关的,会把 shadow tree 转换成真正渲染像素在屏幕上的 Host View Tree

接下来我们来聊聊在三种不同场景下,Fabric 在这三个阶段分别做了什么

初次渲染

假设我们需要渲染这个自定义组件:

jsx 复制代码
function MyComponent() {
  return (
    <View>
      <Text>Hello, World</Text>
    </View>
  );
}

渲染阶段

在这个阶段,最重要的目标就是创建 React Element Tree 与 React Shadow Tree

熟悉 react 的读者应该知道,MyComponent 这个自定义组件在 react 内部大概长:

jsx 复制代码
HostRoot
  └── FunctionComponent(MyComponent)
        └── HostComponent(View)
              └── HostComponent(Text)

在 react 构建 React Element Tree 的同时,针对 HostComponent 这一类型的元素会同步在 C++ 中创建 React Shadow Node,并且用对应 react 元素中的 stateNode 属性指向它

需要留意的是,这个步骤一定是线程安全并同步的,但不一定只会在 JS 线程中发生,这点我们会在后面有详细解释

最后创建的两颗树关系如下:

提交阶段

当渲染阶段完成后,react 会通知 Fabric 进入提交阶段

此时 Fabric 会先计算渲染阶段创建的 Shadow tree 的布局,计算后 Shadow tree 看起来如下:

当布局计算完成后,Fabric 会把最新的 Shadow tree 标记为 "next tree",表示这棵树表示的内容会在下一个 tick 被渲染到 UI 上

挂载阶段

在挂载阶段中,Fabric 主要的目标就是把有布局信息的 Shadow tree 转换为能真实渲染像素的 Host View Tree:

由于涉及到 Host View,挂载阶段的实现是强平台相关的,这代表我们需要在不同的原生平台中准备不同的实现;挂载阶段的所有操作都在 UI 线程中执行

挂载阶段可以被分成三个步骤:

  1. Tree Diffing :在 C++ 中对比 previously rendered tree (当前被渲染的树) 与上述 "next tree" ,对比的结果是一个在 host view 上执行的操作列表(比如 createView, updateView, removeView, deleteView);这个阶段也会顺便做 View Flattening 以减少不必要的 host view 渲染
  2. Tree Promotion (Next Tree → Rendered Tree) :这是第二次 Tree Promotion,这次是为了将 "next tree" 提升成 previously rendered tree
  3. View Mounting:对 host view 应用 tree diffing 产生的操作

需要注意的是,第一个步骤中的 diff 跟 react 的 diff 完全不同:react diff 决定的是"组件树哪里变了";而 mount 阶段的 diff 决定的是原生 UI 应该怎么改

当然,在初次渲染这个场景中 previously rendered tree 是空的,所以 tree diffing 的结果更多在创建 view、设置属性上;在后续的小节中我们会讨论状态变化导致的渲染更新,此时 tree diffing 的重要性就凸显出来了

React 状态更新渲染

假设我们需要更新这个自定义组件:

jsx 复制代码
function MyComponent() {
  return (
    <View>
      <View
        // 假设我们把这里的 red 改成了 yellow
        style={{backgroundColor: 'red', height: 20, width: 20}}
      />
      <View
        style={{backgroundColor: 'blue', height: 20, width: 20}}
      />
    </View>
  );
}

渲染阶段

在这个例子中,我们更改了其中一个 View 元素的属性,但由于 React Element Tree 与 React Shadow Tree 的不可变(immutable)特性,渲染阶段需要做的事情就是:创建带有新属性的全新 React Element Tree 与 React Shadow Tree

不过,这里有个例外:如果我们看内部的第二个 View 元素(背景为 blue)会发现它始终都没有被改变过

对此 RN 有一个优化策略:结构共享 Structural Sharing

这个策略的核心思想是:当一个节点的属性(包括样式)与子节点没有变更,则我们继续使用(不复制)该节点

具体表现如下图所示,节点 4 对应我们北京为 blue 的 View 元素:

创建新树 T' 的步骤如下:

  1. CloneNode(Node 3 , {backgroundColor: 'yellow'}) → Node 3'
  2. CloneNode(Node 2 ) → Node 2'
  3. AppendChild(Node 2' , Node 3')
  4. AppendChild(Node 2' , Node 4):节点 4 直接被复用了
  5. CloneNode(Node 1 ) → Node 1'
  6. AppendChild(Node 1' , Node 2')

提交阶段

提交阶段跟初次渲染时基本一致,唯一的不同点出现在计算布局的时候

如果某个被复用的节点的父节点有布局上的变化,则该节点可能会被重新复制

挂载阶段

挂载阶段也与初次渲染时一致,区别在于 Tree Diffing 与 View Mounting 会多了 update/delete 等操作

C++ 状态更新渲染

最后我们再来说一种独属于 RN 原生场景的更新渲染:C++ 状态更新

C++ 状态(C++ state)指的是 RN 中并没有直接暴露给 JS 的原生组件状态,这些状态被 C++ 以及宿主平台控制

有个比较常用的例子是 ScrollView 其内部有一个 contentOffset 属性控制着当前滚动位置,这个属性的值 JS 可以通过 measure 方法获得,但 JS 不是其数据源(官网叫 source of truth)

C++ 状态更新导致的渲染,跟其他的渲染有两点差异:

  1. 没有渲染阶段,因为 React 不参与这次的更新
  2. 渲染可以发生在任何线程,包括 UI 线程

接下来我们来看看具体的更新过程,假设我们有个树:

js 复制代码
Tree 当前版本(Revision A):

1
 └─ 2
     ├─ 3 (ScrollView, offset=0)
     └─ 4

提交阶段

这里有两种情况,我们分别描述

第一种情况:从触发更新,到成功提交树的过程中版本(Revision)没有变化

假设这里我们更新了 offset = 100,这个过程中只有这个更新发生

js 复制代码
Tree 目标版本(Revision B):
1'
 └─ 2'
     ├─ 3' (offset=100)
     └─ 4

结果就是:节点 1、2、3 都会被 clone,然后新节点 3'offset 被更新为 100

把 Revision B 标记为 "next tree",结束提交阶段

第二种情况:从触发更新,到提交树的过程中,树本身版本变化了

假设时间线是这样的:

text 复制代码
更新offset=100 -> 节点 3 setState -> setState 的更新被率先提交 -> offset 更新提交

此时 offset 更新会遇到很尴尬的情况:它基于的 Revision A 已经不是最新的树了,所以它需要重试(retry)

它会:

  1. 获取当前最新的树(setState 更新完的树)
  2. 基于最新的树重新 clone 一次所需节点并完成状态更新
  3. 重新提交

所以最后它的提交可能会变成:

js 复制代码
Tree 目标版本(Revision B'):
1''
 └─ 2''
     ├─ 3'' (offset=100 + new state)
     └─ 4

其中节点 1''、2''、3'' 跟 Revision B 的三个节点完全不同

重试机制是为了避免不同数据源之间互相碰撞、竞争问题

挂载阶段

挂载阶段的操作就更其他更新/初次渲染操作一致了,这里就不赘述了

渲染的线程模型

在上个小节我们说到,渲染的过程不一定都在 JS 线程中执行,那么具体有哪些线程可能参与这个过程呢?

本节我们借着 RN 的博客 Threading Model 来聊聊这个问题

首先,Fabric 主要运行在这两个线程:

  • UI 线程:也是应用的主线程,这也是唯一能操作宿主元素的线程,所以是绕不开的
  • JS 线程:大部分情况下,JS 运行时以及 Fabric 会运行在这个线程中,但并不绝对

接下来,我们来讨论一下不同情况下渲染不同阶段的线程占用情况,渲染不同阶段在后面会用下图色块代表:

在 JS 线程渲染,UI 线程挂载

这是最常见的渲染方式,其线程占用如下:

在 UI 线程完成全部渲染

最高优先级的渲染方式,对应到本专栏前面文章 RuntimeSchedulerNative 同步触发 更新方式

JS 线程渲染、交互中断、JS 线程继续渲染

这种情况就是 React 可中断、可恢复的价值所在:React 在 render 过程中突然发生了一个低优交互事件,所以它就中断(yield)等待该事件被处理完之后基于已经发生的事件状态继续刚刚的任务

高优事件中断

当应用遇到 Discrete event 时(比如用户按下按钮)会触发 React 的最高优同步处理机制,这个机制会中断所有正在执行中的渲染工作,然后以同步的形式处理这个事件

RN 在接收到此类事件的时候会在 UI 线程处理完所有的 render、layout、commit、mount 操作以响应高优事件

C++ State 更新

最后,是最特殊的 C++ 状态更新!

这个情况回跳过 React 的 render 阶段,直接进入 layout、commit、mount

Fabric 实现

在上一小节中,我们从具体表现的视角看了 Fabric 是如何处理复杂的 render -> mount 流程的

在这一小节,我们会稍微聚焦 Fabric 内部,结合具体代码来看看 Fabric 是如何实现/控制这些流程的

Fabric 的职责范围

首先,我们需要先确定 Fabric 在整个渲染流程中具体负责了哪些东西,输入输出又是什么~

从本文第一张图 Fabric 系统的雏形 中我们可以看到,Fabric 主要是负责:

  • Tree creation
  • Layout
  • Commit

简单来说,Fabric 负责接收 React 的 UI 描述,生成平台无关的 UI 挂载指令(MountingTransaction)

输入

大多数情况下,Fabric 都是通过 JSI 接收 JS 的输入/调用,JS 可以调用的方法都集中在 UIManagerBinding::get

这些方法以及分类如下所示:

cpp 复制代码
/**
 * UIManagerBinding.cpp (RN 0.76)
 * 这些方法本质都是通过 JSI HostObject 暴露给了 global.nativeFabricUIManager 供 JS 调用
 */

// =============================
// 1. 渲染主链路(核心)
// =============================
// 👉 React Renderer 每次 render / commit 都会用到

createNode                      // 创建 ShadowNode(HostComponent 入口)
cloneNodeWithNewChildren        // clone + children 变更
cloneNodeWithNewProps           // clone + props 变更
cloneNodeWithNewChildrenAndProps// clone + children + props
appendChild                     // 构建 ShadowTree 结构
completeRoot                    // JS 完成当前所有任务后调用,提交 root → 触发 commit / mounting

createChildSet                  // 创建 root children 容器(过渡 API)
appendChildToSet                // 往 child set 塞节点(过渡 API)


// =============================
// 2. 事件系统(核心)
// =============================
// 👉 Native → JS 事件回流

registerEventHandler            // 注册 JS 处理函数入口(global handler)

setIsJSResponder                // responder 体系(触摸 / 手势控制)

unstable_getCurrentEventPriority
unstable_DefaultEventPriority
unstable_DiscreteEventPriority  // 事件优先级(Scheduler 相关)


// =============================
// 3. 命令 / 副作用(非声明式)
// =============================
// 👉 imperative 操作(绕过 render pipeline)

dispatchCommand                 // ref command(scrollTo / focus 等)
setNativeProps                  // 直接改 ShadowTree 的属性,DEPRECATED
sendAccessibilityEvent          // 无障碍事件,主要给无障碍系统使用,比如 IOS 的 VoiceOver 或 Android 的 TalkBack
configureNextLayoutAnimation    // 给下一次布局变化预先注册一份动画配置,使这些布局变化在挂载时以动画形式呈现,详见 https://reactnative.dev/docs/layoutanimation


// =============================
// 4. 布局查询(读 ShadowTree)
// =============================
// 👉 读取当前 revision 的 layout(不是直接读 UIView)

getRelativeLayoutMetrics        // 相对布局(left/top/width/height)

// 下面这三个都是 legacy,为了兼容旧 RN 才留着的,推荐使用 getRelativeLayoutMetrics 替代
measureLayout                   // 相对测量
measure                         // 绝对 + pageX/Y
measureInWindow                 // window 坐标测量


// =============================
// 5. DOM / 查询(辅助能力)
// =============================

findNodeAtPoint                 // 根据坐标(x, y)在 ShadowTree 中做命中测试(hit-testing),找到对应的目标节点

// 以下两个方法推荐直接从 NativeDOM 中调用
getBoundingClientRect           // 获取某个节点在屏幕(或窗口)坐标系中的边界信息 DOM 风格 API
compareDocumentPosition         // 判断两个 DOM 节点的位置关系

findShadowNodeByTag_DEPRECATED  // 根据 reactTag 找 shadowNode,DEPRECATED

输出

Fabric 的输出是 MountingTransaction,它是一个由多条 UI 变更指令(Mutation)组成的集合

这些 Mutation 描述了从旧 UI 到新 UI 需要执行的最小变更操作,用于驱动最终的原生 UI 更新

Mutation 的定义是在 ShadowViewMutation.cpp 中,RN 当前定义了 5 种 Mutation:

  • Create
  • Delete
  • Insert
  • Remove
  • Update

感兴趣的读者可以去 packages/react-native/ReactCommon/react/renderer/mounting/ShadowViewMutation.cpp 看看这些 Mutation 长啥样

渲染流程总览

通过上面的总览图,我们可以看出来在 Fiber 其实横跨了渲染流程的三个阶段,它的核心部分从 UIManagerBinding 开始,一直到挂载阶段的 Tree diffing 结束,生成 MountingTransaction 为止

了解了 Fabric 的职责范围后,我们以总览图为基础,通过一些简单的例子深入 Fabric 的实现细节,看看它是如何将 UI 描述转换成 MountingTransaction

渲染流水线

让我们从一个最简单的例子开始,看看 Fabric 是如何帮助 RN 最终把这个元素渲染到屏幕上的:

tsx 复制代码
function ColorChanger() {
  const [color, setColor] = useState('red');
  return (
    // container
    <View style={{ marginTop: 100 }}>
      {/*  color box */}
      <View
        style={{ backgroundColor: color, height: 50, width: 50 }}
      />
      {/* blue box */}
      <View
        style={{ backgroundColor: 'blue', height: 50, width: 50 }}
      />
      {/* button */}
      <Pressable onPress={() => setColor(prev => prev === 'red' ? 'green' : 'red')}>
        <Text>Change color</Text>
      </Pressable>
    </View>
  );
}

这个例子只是为了方便讲解,请忽略 RN 中类似 view flattening 之类的优化

ColorChanger 是一个展示两个颜色块的组件(初次渲染时是一红一蓝),通过下方的 Change color 按钮可以更改第一个色块的颜色

为了方便描述我在每个组件的上方都标注了名字,后面我们通过这些名字来指代对应元素

接下来我们还是分成三个阶段深入代码来聊聊 Fabric 具体做了什么

渲染阶段

【初次渲染】 渲染阶段在初次渲染最主要的任务是创建 ShadowTree,下方是初次渲染时调用 UIManagerBinding 方法的顺序:

js 复制代码
/**
 * ColorChanger 首次渲染(first render)时,
 * nativeFabricUIManager / UIManagerBinding 暴露的方法调用顺序
 *
 * 说明:
 * 1. 这里只列"render/complete 阶段"会触发的 UIManager 方法
 * 2. Pressable 是复合组件,最终会展开成一个 host View;下面把它记作 "button"
 * 3. Text 最终会产生一个 host Text 和一个 host RawText
 */

1. // color box
createNode(
  tag = 2,
  viewName = "RCTView",
  rootTag = <rootTag>,
  props = {
    style: {
      backgroundColor: "red",
      height: 50,
      width: 50,
    },
  },
  ...
);

2. // blue box
createNode(
  tag = 4,
  viewName = "RCTView",
  rootTag = <rootTag>,
  props = {
    style: {
      backgroundColor: "blue",
      height: 50,
      width: 50,
    },
  },
  ...
);

3. // button -> Text child -> raw text: "Change color"
createNode(
  tag = 6,
  viewName = "RCTRawText",
  rootTag = <rootTag>,
  props = {
    text: "Change color",
  },
  ...
);

4. // button -> Text
createNode(
  tag = 8,
  viewName = "RCTText",
  rootTag = <rootTag>,
  props = {
    ...
  },
  ...
);

5. // button -> Text 追加 raw text 子节点
appendChild(
  parentNode = /* button -> Text */ tag: 8,
  childNode = /* raw text */ tag: 6
);

6. // button (Pressable 展开后的 host View)
createNode(
  tag = 10,
  viewName = "RCTView",
  rootTag = <rootTag>,
  props = {
    // Pressable 注入的交互/可访问性/event props
    onPress: ...,
    ...
  },
  ...
);

7. // button 追加 Text 子节点
appendChild(
  parentNode = /* button */ tag: 10,
  childNode = /* button -> Text */ tag: 8
);

8. // container
createNode(
  tag = 12,
  viewName = "RCTView",
  rootTag = <rootTag>,
  props = {
    style: {
      marginTop: 100,
    },
  },
  ...
);

9. // container 追加 color box
appendChild(
  parentNode = /* container */ tag: 12,
  childNode = /* color box */ tag: 2
);

10. // container 追加 blue box
appendChild(
  parentNode = /* container */ tag: 12,
  childNode = /* blue box */ tag: 4
);

11. // container 追加 button
appendChild(
  parentNode = /* container */ tag: 12,
  childNode = /* button */ tag: 10
);

12. // HostRoot 创建 child set
createChildSet();

13. // HostRoot 把 container 放进 child set
appendChildToSet(
  childSet = <childSet>,
  childNode = /* container */ tag: 12
);

14. // render 阶段完成,进入 commit 阶段
completeRoot(
  rootTag = <rootTag>,
  childSet = <childSet>
);

总结一下:

  1. 初次渲染时,会从叶子节点开始调用 createNode 方法(Fabric 会在这个方法内部创建 ShadowNode

  2. 越上层的元素越晚调用 createNode(步骤 8:container 节点是在 color boxblue boxbutton 都创建完后才创建的)

  3. 当上层元素创建完后,会依次调用 appendChild 插入子元素(步骤 9~11)

  4. 当所有元素创建完后,HostRoot(React fiber 的根结点)会创建一个用于收集当前 commit 中所有 root 子节点的临时容器 child set(步骤 12)

  5. child set 创建完后,会把我们创建的 container 放进去(步骤 13)

    • 实际上这一步放的不是 container 因为 RN 框架内部还有一层顶层 View,这里简化说明方便理解
  6. 最后一步,调用 completeRoot 方法,这个方法内部会调用 commit 方法进入提交阶段

【更新渲染】 如果初次渲染结束后,我们点击 button 更新 color box 的颜色,此时 Fabric 的任务就是更新 ShadowTree,下方是更新渲染时调用 UIManagerBinding 方法的顺序:

js 复制代码
/**
 * ColorChanger
 * 点击 button 后:
 *   color box: backgroundColor "red" -> "green"
 *
 * 这里按"主更新链路"列出 nativeFabricUIManager / UIManagerBinding 方法调用顺序
 * (下方调用只看我们 ColorChanger 组件中的元素,由于 RN 框架在内部顶层套了几层 View,那些元素的 clone 为了方便理解直接忽略)
 */

1. // color box
cloneNodeWithNewProps(
  node = /* color box 的旧 ShadowNode */,
  props = {
    style: {
      backgroundColor: "green",
      height: 50,
      width: 50,
    },
  }
);

2. // container
cloneNodeWithNewChildren(
  node = /* container 的旧 ShadowNode */
);
  
3. // 【第一次】container 重新挂 children
appendChild(
  parentNode = /* container 的新 ShadowNode */,
  childNode = /* color box 的新 ShadowNode */
);

4. // 【第二次】container 重新挂 children
appendChild(
  parentNode = /* container 的新 ShadowNode */,
  childNode = /* blue box 的 ShadowNode */
);

5. // 【第三次】container 重新挂 children
appendChild(
  parentNode = /* container 的新 ShadowNode */,
  childNode = /* button 的 ShadowNode */
);

// ⚠️由于 blue box 以及 button 没有变更,所以不需要 clone 它们

6. // 创建新的 child set
createChildSet();

7. // 把 container 放进新 child set
appendChildToSet(
  childSet = <childSet>,
  childNode = /* container 的新 ShadowNode */
);

8. // render 阶段完成,进入 commit 阶段
completeRoot(
  rootTag = <rootTag>,
  childSet = <childSet>
);

总结一下:

  1. 属性改变的节点 color box 会被 Fabric 用 cloneNodeWithNewProps 方法复制一个新的 ShadowNode(步骤 1)
  2. 属性改变节点的父节点 container 也会需要复制,但是用的是 cloneNodeWithNewChildren 方法(步骤 2)
  3. 由于 blue box 以及 button 没有变更,所以不需要 clone 它们;但是需要把它们重新挂到 container 的新 ShadowNode 下(步骤3~5)
  4. 最后我们需要收尾,准备进入 commit 阶段(步骤6~8)
创建 ShadowNode 前的准备工作

在进入具体的 UIManager 方法解析之前,我们需要先讨论在创建 ShadowNode 之前需要做哪些准备工作,有了这些背景知识才能更好的理解 ShadowNode 是如何被 UIManager 创建出来的

接下来我们以 ViewShadowNode 为例,来聊聊如果我们需要创建一个这样的 ShadowNode,需要做哪些准备

准备工作一共分为 5 步:

  1. 定义 ViewShadowNode :从 代码 中可以看到它继承了 ConcreteViewShadowNode 这个通用 View 节点,定义了自己的 NamePropsEventEmitter

  2. 通过 ViewShadowNode 定义 ViewComponentDescriptor :见 代码XXXComponentDescriptor 的目的就是把一些通用能力绑定到 XXXShadowNode 中(比如 createShadowNodecloneShadowNode,通用能力具体实现看 代码

  3. ViewComponentDescriptor 包装成 ComponentDescriptorProvider :见 IOSAndroid 实现,ComponentDescriptorProvider 提供了 handle (一个标识 View 元素的整型 int)、name (标识 View 元素的字符串)、constructorViewComponentDescriptor 的构造函数引用)

  4. ComponentDescriptorProvider 注册进 ComponentDescriptorProviderRegistry :见 IOSAndroid 实现,ProviderRegistry 有两个主要功能:

    1. 提供保存所有 ComponentDescriptorProvider 的空间
    2. 提供 createComponentDescriptorRegistry 方法,该方法会为 ViewComponentDescriptor 创建实例并返回一个 ComponentDescriptorRegistry(这是一个保存所有 XXXComponentDescriptor 的空间,见 代码
  5. 最后,应用初始化的时候调用 createComponentDescriptorRegistry 然后把返回的 ComponentDescriptorRegistry 放进 UIManager 就大功告成了(见 代码 实现)

准备工作有一点绕,但它的最终目标其实就是把每一种 ShadowNode 的 ComponentDescriptor 都放进 ComponentDescriptorRegistry,然后让 UIManager 可以从中拿到对应的 ComponentDescriptor 去创建/复制所需的 ShadowNode

只要抓住这一点,其他的代码都是装饰

聊完准备工作后,接下来我们来看看 UIManager 具体是怎么实现上面 UIManagerBinding 声明的方法吧~

UIManager::createNode
cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp

// 根据 tag 创建 ShadowNode
std::shared_ptr<ShadowNode> UIManager::createNode(
    Tag tag,
    const std::string& name,
    SurfaceId surfaceId,
    RawProps rawProps,
    InstanceHandle::Shared instanceHandle) const {
	// ...省略部分代码

  // 根据元素名称拿到对应的 ComponentDescriptor
  auto& componentDescriptor = componentDescriptorRegistry_->at(name);
  // 兜底逻辑
  auto fallbackDescriptor =
      componentDescriptorRegistry_->getFallbackComponentDescriptor();

  PropsParserContext propsParserContext{surfaceId, *contextContainer_.get()};

  // 根据 ComponentDescriptor 创建 Family(⚠️非常重要!后面会有解释)
  auto family = componentDescriptor.createFamily(
      {tag, surfaceId, std::move(instanceHandle)});
  // 处理 JS 传过来的属性
  const auto props = componentDescriptor.cloneProps(
      propsParserContext, nullptr, std::move(rawProps));
  // 对于有 C++ state 的元素,需要创建一下
  const auto state = componentDescriptor.createInitialState(props, family);

  // 根据 ComponentDescriptor 来创建 ShadowNode
  auto shadowNode = componentDescriptor.createShadowNode(
      ShadowNodeFragment{
          .props = fallbackDescriptor != nullptr &&
                  fallbackDescriptor->getComponentHandle() ==
                      componentDescriptor.getComponentHandle()
              ? componentDescriptor.cloneProps(
                    propsParserContext,
                    props,
                    RawProps(folly::dynamic::object("name", name)))
              : props,
          .children = ShadowNodeFragment::childrenPlaceholder(),
          .state = state,
      },
      family);

  // 钩子,用来通知
  if (delegate_ != nullptr) {
    delegate_->uiManagerDidCreateShadowNode(*shadowNode);
  }
  if (leakChecker_) {
    leakChecker_->uiManagerDidCreateShadowNodeFamily(family);
  }

  // 返回创建的 ShadowNode,最后会被附着在 JS 侧 fiber 的 stateNode 属性上
  return shadowNode;
}

根据上面的代码我们可以知道在创建每一个 ShadowNode 的时候都会创建一个对应的 Family(具体的类叫 ShadowNodeFamily

ShadowNodeFamily 的主要职责就一句话:为同一个组件实例提供稳定身份标识,并承载该实例在多个 ShadowNode 版本之间共享的"家族级数据"

由于 ShadowNode 的不可变性(immutable)导致每次有更新都需要 clone 一个新的 ShadowNode,此时一个能够跨越版本标识同一个组件的身份标识就显得特别重要了

除了身份标识之外,ShadowNodeFamily 还承担着在所有 clone 的 ShadowNode 之间共享数据 的职责,比如 tagsurfaceIdstate

ShadowNodeFamily 的跨版本稳定性还可以让它作为事件系统的稳定锚点 ,这也是为什么它可以直接持有 eventEmitter_

最后,ShadowNodeFamily 还在内部保存了 Family 级别的树关系指针 parent,这保证了在 ShadowNode 不稳定(频繁 clone)的前提下,仍然能维持组件实例级别的稳定祖先关系

UIManager::appendChild
cpp 复制代码
// in UIManager.cpp

// 把已经创建好的 childShadowNode 插入到 parentShadowNode 的子节点中
void UIManager::appendChild(
    const ShadowNode::Shared& parentShadowNode,
    const ShadowNode::Shared& childShadowNode) const {
  // ...省略部分代码

  // 直接从 parentShadowNode 拿 ComponentDescriptor
  auto& componentDescriptor = parentShadowNode->getComponentDescriptor();
  // 会同时更新 ShadowNode 的 children 以及 Family 的 parent 字段
  componentDescriptor.appendChild(parentShadowNode, childShadowNode);
}
UIManager::completeSurface
cpp 复制代码
// in UIManager.cpp

// UIManagerBinding 中 completeRoot 的具体实现
// 目的是把当前 ShadowNode 提交上去
void UIManager::completeSurface(
    SurfaceId surfaceId,
    const ShadowNode::UnsharedListOfShared& rootChildren,
    ShadowTree::CommitOptions commitOptions) {
	// ...省略部分代码

  // 核心代码,visit 传入的第二个参数是一个 lambda
  // 这个 lambda 会在 visit 方法中被调用,并且传入当前 ShadowTree 的引用
  shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) {
    // 这段做了两件事:
    // 1. 把 shadowTree 提交上去计算布局(Layout Calculation)
    // 2. 做 Tree Promotion (New Tree → Next Tree)
    auto result = shadowTree.commit(
        [&](const RootShadowNode& oldRootShadowNode) {
          return std::make_shared<RootShadowNode>(
              oldRootShadowNode,
              ShadowNodeFragment{
                  .props = ShadowNodeFragment::propsPlaceholder(),
                  .children = rootChildren,
              });
        },
        commitOptions);

    // 如果当前的 ShadowTree commit 成功了,我们可以保存当前 ShadowTree 的稳定快照
    // 这样一来 JS 就可以稳定的拿到当前的 measure 信息了
    if (result == ShadowTree::CommitStatus::Succeeded &&
        lazyShadowTreeRevisionConsistencyManager_ != nullptr) {
      // It's safe to update the visible revision of the shadow tree immediately
      // after we commit a specific one.
      lazyShadowTreeRevisionConsistencyManager_->updateCurrentRevision(
          surfaceId, shadowTree.getCurrentRevision().rootShadowNode);
    }
  });
}
UIManager::cloneNode
cpp 复制代码
// in UIManager.cpp

// UIManagerBinding 中 cloneNodeWithNewProps, cloneNodeWithNewChildren, cloneNodeWithNewChildrenAndProps 的具体实现
// 目标是根据新的 children 以及 rawProps 复制一个新的 ShadowNode 出来
std::shared_ptr<ShadowNode> UIManager::cloneNode(
    const ShadowNode& shadowNode,
    const ShadowNode::SharedListOfShared& children,
    RawProps rawProps) const {
	// ...省略部分代码

  PropsParserContext propsParserContext{
      shadowNode.getFamily().getSurfaceId(), *contextContainer_.get()};

  // 直接从要复制的 shadowNode 拿 ComponentDescriptor
  auto& componentDescriptor = shadowNode.getComponentDescriptor();
  // 拿到当前 shadowNode 对应的 family
  auto& family = shadowNode.getFamily();
  // 默认属性,因为 rawProps 只会传 diff 后的新属性
  auto props = ShadowNodeFragment::propsPlaceholder();

  // 把这次传进来的 rawProps 增量地合并到当前节点的 props 上,然后生成一份新的 Props 对象
  if (!rawProps.isEmpty()) {
    if (family.nativeProps_DEPRECATED != nullptr) {
      family.nativeProps_DEPRECATED =
          std::make_unique<folly::dynamic>(mergeDynamicProps(
              *family.nativeProps_DEPRECATED,
              (folly::dynamic)rawProps,
              NullValueStrategy::Ignore));

      props = componentDescriptor.cloneProps(
          propsParserContext,
          shadowNode.getProps(),
          RawProps(*family.nativeProps_DEPRECATED));
    } else {
      props = componentDescriptor.cloneProps(
          propsParserContext, shadowNode.getProps(), std::move(rawProps));
    }
  }

  // 根据新 props 复制一份 shadowNode
  auto clonedShadowNode = componentDescriptor.cloneShadowNode(
      shadowNode,
      {
          .props = props,
          .children = children,
          .runtimeShadowNodeReference = false,
      });

  return clonedShadowNode;
}

提交阶段

提交阶段的入口在 UIManager::completeSurfaceshadowTree.commit 中;它是不区分初次渲染或者状态更新渲染的

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp

CommitStatus ShadowTree::commit(
    const ShadowTreeCommitTransaction& transaction,
    const CommitOptions& commitOptions) const {
  SystraceSection s("ShadowTree::commit");

  [[maybe_unused]] int attempts = 0;

  // 基于乐观并发机制(optimistic concurrency control)的失败重试逻辑
  while (true) {
    attempts++;

    // 核心提交代码,允许在可控范围内不断重试提交
    auto status = tryCommit(transaction, commitOptions);
    if (status != CommitStatus::Failed) {
      return status;
    }

    // 一般不会重试那么多次,这段代码是用来 debug 的
    react_native_assert(attempts < 1024);
  }
}

从上面代码可以看到,ShadowTree::commit 就是个无情的重试机器,它允许在 CommitStatus::Failed 的情况下不断重试真正的提交阶段代码 tryCommit,至于为什么要这么做、什么情况会导致失败请容我在这卖个关子,我们先来看看 tryCommit 到底做了什么:

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp

CommitStatus ShadowTree::tryCommit(
    const ShadowTreeCommitTransaction& transaction,
    const CommitOptions& commitOptions) const {
  // ...省略部分代码

  // telemetry 用来记录本次提交的时间/时序信息
  auto telemetry = TransactionTelemetry{};
  // 记录当前 commit 开始的时间
  telemetry.willCommit();

  // 获取当前 revision 状态:revision 代表当前 ShadowTree 的快照,包含当前的 rootShadowNode、版本号以及对应的提交元数据(如 telemetry)
  CommitMode commitMode;
  auto oldRevision = ShadowTreeRevision{};
  auto newRevision = ShadowTreeRevision{};
  // 这个变量记录了最近一次 "因为 C++ state 变化" 而产生的 Revision number
  ShadowTreeRevision::Number lastRevisionNumberWithNewState;
  {
    // 用一个共享锁读取当前 revision 状态
    std::shared_lock lock(commitMutex_);
    commitMode = commitMode_;
    oldRevision = currentRevision_;
    lastRevisionNumberWithNewState = lastRevisionNumberWithNewState_;
    // ⚠️留意这里读完之后马上就释放锁了,代表后续的代码不受这个锁的保护
    // 换句话说,当前 revision 可能在后续的计算中被修改,这可能导致本次计算结果不可用
    // 这里是一个关键的 tradeoff,用无锁状态下的乐观重试来提高整体并发度
  }

  // 从 Revision 中读到 ShadowNode
  const auto& oldRootShadowNode = oldRevision.rootShadowNode;
  // 这里的 transaction 就是 UIManager::completeSurface 方法中,shadowTree.commit 方法调用传入的 lambda
  // 本质就是根据 oldRootShadowNode 以及新的 children 复制一个新的 ShadowNode 出来
  auto newRootShadowNode = transaction(*oldRevision.rootShadowNode);

  // 如果创建出来的新 ShadowNode 为空或者传进来的 shouldYield 方法返回 true
  // 则直接把本次 commit 取消掉
  if (!newRootShadowNode ||
      (commitOptions.shouldYield && commitOptions.shouldYield())) {
    return CommitStatus::Cancelled;
  }

  // 这一段 if 负责处理 C++ state
  // 它负责处理在拿到 oldRootShadowNod 后,执行 layout 前的 C++ state 变化
  // 如果 state 有变化,需要进行对齐
  if (commitOptions.enableStateReconciliation) {
    if (ReactNativeFeatureFlags::useStateAlignmentMechanism()) {
      // 这是新的对齐方法,会进行细粒度的比较,并且如果可以的话会在原来的 ShadowNode 上应用新 state
      // 最后它会改动 newRootShadowNode 并且应用上所有新的 state(这也是声明 newRootShadowNode 的时候没有 const 的原因)
      progressStateIfNecessary(*newRootShadowNode, *oldRootShadowNode);
    } else {
      // 简单粗暴,如果发现有新 state,直接复制一个新的 ShadowNode
      auto updatedNewRootShadowNode =
          progressState(*newRootShadowNode, *oldRootShadowNode);
      if (updatedNewRootShadowNode) {
        newRootShadowNode =
            std::static_pointer_cast<RootShadowNode>(updatedNewRootShadowNode);
      }
    }
  }

  // 在真正 commit 之前跑 commit hook,这些 hook 可以记录当前树的状态或者进一步修改/返回新的 RootShadowNode
  // 这里的 delegate_ 就是 uiManager
  newRootShadowNode = delegate_.shadowTreeWillCommit(
      *this, oldRootShadowNode, newRootShadowNode);

  // 再次检查是否需要取消
  if (!newRootShadowNode ||
      (commitOptions.shouldYield && commitOptions.shouldYield())) {
    return CommitStatus::Cancelled;
  }

  // 用一个 vector 记录会受 layout 影响的节点
  std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
  affectedLayoutableNodes.reserve(1024);

  // 记录 layout 发生的时间点
  telemetry.willLayout();
  // 把当前 telemetry 记录到一个受 thread_loca 保护的变量,使得在后面 layout 过程中可以随时获取这个 telemetry
  telemetry.setAsThreadLocal();
  // 【核心】执行 layout 计算
  // ⚠️这里的 layout 是在锁外做的,因为 layout 的计算有可能很重,如果为了这个计算加个锁会阻塞别的 commit
  // 所以 RN 选择了在计算完布局的提交窗口才加锁判断是否要将当前计算布局提交,目的还是通过乐观重试尽可能提高并发度
  newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
  // layout 计算结束,把记录 telemetry 的变量释放
  telemetry.unsetAsThreadLocal();
  // 记录 layout 结束时间、受影响的节点数量
  telemetry.didLayout(static_cast<int>(affectedLayoutableNodes.size()));

  // 这里是当前 commit 最后提交窗口,我们需要判断判断是否该提交这次 commit
  {
    // 为了获取到最新的 currentRevision_,这里加了独占锁
    std::unique_lock lock(commitMutex_);

    // 再次检查是否需要取消
    if (commitOptions.shouldYield && commitOptions.shouldYield()) {
      return CommitStatus::Cancelled;
    }

    // 判断是否用新逻辑的 flag
    if (ReactNativeFeatureFlags::
            enableGranularShadowTreeStateReconciliation()) {
      auto lastRevisionNumberWithNewStateChanged =
          lastRevisionNumberWithNewState != lastRevisionNumberWithNewState_;
			// 新逻辑的判断更加细粒度,不只是简单粗暴的比较 Revision number
      // 而是判断 "自从我开始这次提交以来,是否出现了带有新 state 的 revision"
      // 如果有,则需要重新计算
      // ⚠️ 这里的逻辑对应到 "C++ 状态更新渲染" 小节中提交阶段的重试逻辑
      if (commitOptions.enableStateReconciliation &&
          lastRevisionNumberWithNewStateChanged) {
        return CommitStatus::Failed;
      }
    } else {
      // 旧逻辑,直接比较 Revision number,如果当前 Revision number 跟之前获取到的 Revision number 不同
      // 表示 Revision 有变化了,则我们应该抛弃这次 layout 的计算,重新跑一次 tryCommit
      if (currentRevision_.number != oldRevision.number) {
        return CommitStatus::Failed;
      }
    }

    // 如果不需要重试,我们直接更新 newRevisionNumber
    auto newRevisionNumber = currentRevision_.number + 1;

    {
      // 这里又加了一把锁,这把锁是从 EventEmitter 中拿到的
      // 这个锁是为了防止事件分发线程与当前线程同时写 EventEmitter / EventTarget 的相关状态
      std::scoped_lock dispatchLock(EventEmitter::DispatchMutex());
      // 拿到新旧两棵 ShadowTree 的子节点对比
      // 更新 mounted 标记,用来标识 "当前 ShadowNode 是否可以安全参与事件系统"
      // 它主要被用来标记当前节点是否是 "活跃节点",对于不活跃节点我们不应该 dispatch event 给它
      updateMountedFlag(
          currentRevision_.rootShadowNode->getChildren(),
          newRootShadowNode->getChildren());
    }

    // 记录完成 commit 的时间
    telemetry.didCommit();
    // 记录当前的 RevisionNumber
    telemetry.setRevisionNumber(static_cast<int>(newRevisionNumber));

    // 把当前的 Revision 封存起来,这样这个 Revision 就是不可变的了
    // 这个主要是在 debug 的时候的防御机制
    newRootShadowNode->sealRecursive();

    newRevision = ShadowTreeRevision{
        std::move(newRootShadowNode), newRevisionNumber, telemetry};
		// 记录当前最新的 Revision
    currentRevision_ = newRevision;
    // 只有当此次更新是由 C++ state 变化引起的时候才需要更新
    if (!commitOptions.enableStateReconciliation) {
      lastRevisionNumberWithNewState_ = newRevisionNumber;
    }
  }

  // 对于 layout 有变化的元素触发它们的 onLayout 回调
  emitLayoutEvents(affectedLayoutableNodes);

  // CommitMode 有两种模式:
  // Normal 代表正常把 Revision 挂载上去
  // Suspended 代表 Revision 正常计算,但是先不要挂载
  if (commitMode == CommitMode::Normal) {
    // 进入挂载阶段~
    mount(std::move(newRevision), commitOptions.mountSynchronously);
  }

  // 返回提交成功,终止这次的提交循环
  return CommitStatus::Succeeded;
}

总结一下,tryCommit 方法通过尽量减少在并发提交场景下锁持有时间、把昂贵工作放到锁外做,以换取更高吞吐

代价是提交结果可能在最后一刻发现已经基于过期 revision 或过期 state,于是必须在可控状态下主动失败并进行重试

挂载阶段

挂载阶段的进入时机是提交完成后,在上一个小节中 tryCommit 在返回 Succeeded 之前调用的 mount 方法就是挂载阶段的入口

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp

void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously)
    const {
  
  // mountingCoordinator_ 是一个协调器,有三个职责:
  // 1. 缓存最新已提交 revision
  // 2. 计算与已挂载 revision 之间的 diff(Tree Diffing)
  // 3. 生成 mounting transaction
  // 这里的 push 方法只是将当前 revision 用 mountingCoordinator_ 记录下来
  mountingCoordinator_->push(std::move(revision));
  // 调用 delegate_(就是 uiManager)上的 shadowTreeDidFinishTransaction 方法
  delegate_.shadowTreeDidFinishTransaction(
      mountingCoordinator_, mountSynchronously);
}

可以看到 ShadowTree::mount 方法非常简单,只做了两件事:

  1. MountingCoordinator 中记录当前最新提交的 Revision
  2. shadowTreeDidFinishTransaction 这个消息通过 Delegate(委托)传播出去了

Delegate 主要是用于将一些不适合当前类实现的功能委托出去的一种方式,比如在 ShadowTree 这个例子,委托链是这样的:

  1. ShadowTree 的 Delegate 是 UIManager
  2. UIManager 的 Delegate 是 Scheduler(Scheduler 主要负责协调 ShadowTree 的更新以及事件传递)
  3. Scheduler 的 Delegate 有两个:在 IOS 中是 RCTScheduler、在 Android 中是 Binding

所以我们来接着看一下,UIManager 的 shadowTreeDidFinishTransaction 做了什么:

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp

void UIManager::shadowTreeDidFinishTransaction(
    MountingCoordinator::Shared mountingCoordinator,
    bool mountSynchronously) const {
  SystraceSection s("UIManager::shadowTreeDidFinishTransaction");

  if (delegate_ != nullptr) {
    // 透传到了 Scheduler 的 uiManagerDidFinishTransaction
    delegate_->uiManagerDidFinishTransaction(
        std::move(mountingCoordinator), mountSynchronously);
  }
}
cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp

void Scheduler::uiManagerDidFinishTransaction(
    MountingCoordinator::Shared mountingCoordinator,
    bool mountSynchronously) {
  SystraceSection s("Scheduler::uiManagerDidFinishTransaction");

  if (delegate_ != nullptr) {
    // 再往 delegate 传下去
    // 从这里开始,IOS 跟 Android 的具体实现会开始不一样,这个我们后面会详细聊
    delegate_->schedulerDidFinishTransaction(mountingCoordinator);

    // 获取 RuntimeScheduler 的引用
    auto weakRuntimeScheduler =
        contextContainer_->find<std::weak_ptr<RuntimeScheduler>>(
            "RuntimeScheduler");
    auto runtimeScheduler = weakRuntimeScheduler.has_value()
        ? weakRuntimeScheduler.value().lock()
        : nullptr;
    // 如果能拿到 runtimeScheduler 以及 mountSynchronously 是 false
    // 就会进入异步 mount 的流程(指的是走 runtimeScheduler 的事件循环)
    if (runtimeScheduler && !mountSynchronously) {
      auto surfaceId = mountingCoordinator->getSurfaceId();

      // 调用 scheduleRenderingUpdate 方法,它会往 pendingRenderingUpdates_ 里面注册这里写的 lymbda
      // 作用就是模拟事件循环中的 "绘制机会"
      // 具体可以看看本专栏上一篇关于 runtimeScheduler 文章,会详细讲解这个机制是怎样的,以及 pendingRenderingUpdates_ 是做什么的
      runtimeScheduler->scheduleRenderingUpdate(
          surfaceId,
          [delegate = delegate_,
           mountingCoordinator = std::move(mountingCoordinator)]() {
            // 这里的实现其实跟下面的 else 是一致的,只是执行时机略有差异
            delegate->schedulerShouldRenderTransactions(mountingCoordinator);
          });
    } else {
      // 否则直接同步 mount,不管事件循环那一套东西了
      delegate_->schedulerShouldRenderTransactions(mountingCoordinator);
    }
  }
}

关于这段代码有两个可以补充的点:

第一点是关于 mountSynchronously 什么时候为 true、什么时候应该为 false

关于这点 ShadowTree.h 中有给出注释解释,只有当此次提交来源是 React 的时候,才应该为 false

因为这个变量本质是:我需不需要给 React 再留一个"在 paint 前补跑 effect 和更新"的窗口

如果需要,我用 runtimeScheduler 来执行这个 mount、否则我就直接调用了

第二点是关于 schedulerDidFinishTransaction 后续的双端差异问题

首先,Scheduler 这个类负责协调 ShadowTree 的更新,它使用两个方法来控制挂载阶段的流程:

  • schedulerDidFinishTransaction:用来通知它的 Delegate "有新 transaction 了"
  • schedulerShouldRenderTransactions:用来通知它的 Delegate "现在去真正渲染它"

Scheduler 只负责在对应的时机调用 Delegate 的这两个方法,其他的部分是由平台对应的 Delegate 实现

为了防止大家过早迷失在代码细节里,我在这里先列出两个平台的实现差异~

IOS Android
消费模型 拉模型(Pull model) 推模型(Push model)
思路 由最终负责更新界面的执行层在准备好时主动去 "拉取 "最新 MountingTransaction 并立即应用 上游先把 MountingTransaction 整理好并推入宿主调度链,再由宿主稍后批量/排队地应用到真实界面上
具体操作 1. 在 schedulerDidFinishTransaction 的时候什么都不做 2. 在 schedulerShouldRenderTransactions 的时候在主线程拉取所有 transaction 然后直接消费 1. 在 schedulerDidFinishTransaction 的时候先拉取所有 transaction 然后保存起来 2. 在 schedulerShouldRenderTransactions 的时候把保存起来的 transaction 推送给宿主侧,由宿主决定什么时候要消费
IOS 的挂载流程

首先来看看 IOS 是如何处理的

实现 schedulerDidFinishTransaction 方法的是 SchedulerDelegateProxy 类:

objc 复制代码
// in packages/react-native/React/Fabric/RCTScheduler.mm

class SchedulerDelegateProxy : public SchedulerDelegate {
 public:
  t(void *scheduler) : scheduler_(scheduler) {}

  // schedulerDidFinishTransaction 具体实现
  void schedulerDidFinishTransaction(const MountingCoordinator::Shared &mountingCoordinator) override
  {
    RCTScheduler *scheduler = (__bridge RCTScheduler *)scheduler_;
    // 这里由出现了一个 delegate:拿到 RCTScheduler 类的 delegate,并调用它的 schedulerDidFinishTransaction
    // 这个 delegate 就是 RCTSurfacePresenter
    // RCTSurfacePresenter 是 iOS 侧负责统一管理多个 React Native surface、连接调度层与挂载层,并把渲染结果协调到实际原生界面上的宿主控制器
    [scheduler.delegate schedulerDidFinishTransaction:mountingCoordinator];
  }
}

RCTSurfacePresenter 类中,这个方法的实现是 no-op(也就是不做任何操作):

objc 复制代码
// in packages/react-native/React/Fabric/RCTSurfacePresenter.mm

- (void)schedulerDidFinishTransaction:(MountingCoordinator::Shared)mountingCoordinator
{
  // no-op, we will flush the transaction from schedulerShouldRenderTransactions
}

可以看到,在 IOS 把消费 transaction 的任务全部交给了 schedulerShouldRenderTransactions 方法来执行,之所以要留下 schedulerDidFinishTransaction 我个人猜测主要为了对齐 Android 的语义,因为 Android 需要这个方法来承载挂载阶段逻辑

接下来我们来看看 schedulerShouldRenderTransactions 方法做了什么:

objc 复制代码
// in packages/react-native/React/Fabric/RCTScheduler.mm

class SchedulerDelegateProxy : public SchedulerDelegate {
 public:
  t(void *scheduler) : scheduler_(scheduler) {}

  // schedulerShouldRenderTransactions 具体实现
  void schedulerShouldRenderTransactions(const MountingCoordinator::Shared &mountingCoordinator) override
  {
    RCTScheduler *scheduler = (__bridge RCTScheduler *)scheduler_;
    // 是不是似曾相似?这个方法也通过委托的方式调用了 RCTSurfacePresenter 的同名方法
    [scheduler.delegate schedulerShouldRenderTransactions:mountingCoordinator];
  }
}

RCTSurfacePresenter 中给出了具体的实现:

objc 复制代码
// in packages/react-native/React/Fabric/RCTSurfacePresenter.mm

- (void)schedulerShouldRenderTransactions:(MountingCoordinator::Shared)mountingCoordinator
{
  // 具体的实现~
  [_mountingManager scheduleTransaction:mountingCoordinator];
}

接下来看看 scheduleTransaction 方法具体做了什么:

objc 复制代码
// in packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm

- (void)scheduleTransaction:(MountingCoordinator::Shared)mountingCoordinator
{
  // 如果当前在主线程(UI thread)
  if (RCTIsMainQueue()) {
    // 则直接调用挂载方法
    [self initiateTransaction:*mountingCoordinator];
    return;
  }

  // 否则我们需要把挂载方法放到主线程中执行
  RCTExecuteOnMainQueue(^{
    RCTAssertMainQueue();
    [self initiateTransaction:*mountingCoordinator];
  });
}

// 上述 scheduleTransaction 方法最终会在主线程调用这个方法
- (void)initiateTransaction:(const MountingCoordinator &)mountingCoordinator
{
  SystraceSection s("-[RCTMountingManager initiateTransaction:]");
  RCTAssertMainQueue();

  // 如果现在有挂载的任务,我们先标记后挂起当前任务
  if (_transactionInFlight) {
    _followUpTransactionRequired = YES;
    return;
  }

  // 做一个循环,如果当前挂载任务结束后还有未执行的新挂载任务,则捡起来继续执行
  do {
    _followUpTransactionRequired = NO;
    _transactionInFlight = YES;
    // 核心方法,用来处理 Transaction
    [self performTransaction:mountingCoordinator];
    _transactionInFlight = NO;
  } while (_followUpTransactionRequired);
}

// 用来执行 Transaction 的方法
- (void)performTransaction:(const MountingCoordinator &)mountingCoordinator
{
  SystraceSection s("-[RCTMountingManager performTransaction:]");
  RCTAssertMainQueue();

  // 获取当前需要挂载的 surface
  auto surfaceId = mountingCoordinator.getSurfaceId();

  // 拉取最新的 Transaction
  // 可以看到这里传入了三个 lambda,分别是 willMount、doMount、didMount 的时候需要调用的方法
  mountingCoordinator.getTelemetryController().pullTransaction(
      [&](const MountingTransaction &transaction, const SurfaceTelemetry &surfaceTelemetry) {
        [self.delegate mountingManager:self willMountComponentsWithRootTag:surfaceId];
        _observerCoordinator.notifyObserversMountingTransactionWillMount(transaction, surfaceTelemetry);
      },
      [&](const MountingTransaction &transaction, const SurfaceTelemetry &surfaceTelemetry) {
        RCTPerformMountInstructions(
            transaction.getMutations(), _componentViewRegistry, _observerCoordinator, surfaceId);
      },
      [&](const MountingTransaction &transaction, const SurfaceTelemetry &surfaceTelemetry) {
        _observerCoordinator.notifyObserversMountingTransactionDidMount(transaction, surfaceTelemetry);
        [self.delegate mountingManager:self didMountComponentsWithRootTag:surfaceId];
      });
}

以上方法具体调用的都是 TelemetryController 类的方法,我们来看看具体代码:

cpp 复制代码
// packages/react-native/ReactCommon/react/renderer/mounting/TelemetryController.cpp

bool TelemetryController::pullTransaction(
    const MountingTransactionCallback& willMount,
    const MountingTransactionCallback& doMount,
    const MountingTransactionCallback& didMount) const {
  // 这个方法会向 MountingCoordinator 请求执行 tree diffing
  // 最后返回所有的 transaction
  // 具体实现我们下面聊
  auto optional = mountingCoordinator_.pullTransaction();
  // 如果没有需要执行的 transaction 则直接返回
  if (!optional.has_value()) {
    return false;
  }

  auto transaction = std::move(*optional);

  // telemetry 是 transaction 级别的,只代表一次 transaction
  auto& telemetry = transaction.getTelemetry();
  auto numberOfMutations = static_cast<int>(transaction.getMutations().size());

  // compoundTelemetry_ 记录了当前 surface 的所有 telemetry
  // 在 IOS 当前语境下,这个方法只会在主线程被串行调用
  // 这个锁更像是一种防止 data race 的保守型写法
  mutex_.lock();
  auto compoundTelemetry = compoundTelemetry_;
  mutex_.unlock();

  // 调用上述方法注册的 willMount lambda
  // 主要就是把 "即将 mount" 的事件广播给 delegate 和 observers
  willMount(transaction, compoundTelemetry);

  telemetry.willMount();
  // 调用上述方法注册的 doMount lambda
  // 真正 mount 的操作
  // 具体实现在 https://github.com/facebook/react-native/blob/v0.76.0/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm#L44-L144 感兴趣的读者可以去看看
  // 简单来说就是会遍历 transaction,然后根据类型选择看要调用哪一个 UIView 的方法创建/更新/插入/删除 View 元素
  doMount(transaction, compoundTelemetry);
  telemetry.didMount();

  // 最后把当前 telemetry 记录到 compoundTelemetry 中
  compoundTelemetry.incorporate(telemetry, numberOfMutations);

  // 最后再调用上述方法注册的 didMount lambda
  // 主要就是把 "已经 mount" 的事件广播给 delegate 和 observers
  didMount(transaction, compoundTelemetry);

  mutex_.lock();
  // 拿把小锁修改一下 compoundTelemetry_
  compoundTelemetry_ = compoundTelemetry;
  mutex_.unlock();

  // 最后返回 true 表示已经执行了所有 transaction
  return true;
}

到这里,我们基本把 IOS 挂载的主流程讲完了,在进入 Android 的挂载流程之前,我们最后来看看 mountingCoordinator_.pullTransaction 方法到底是怎么做 Tree diffing 的吧~

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp

// 这个方法的主要职责是对比 baseRevision_(上一次的 revision)与 lastRevision_(最新的 revision)
// 然后计算一条新的 MountingTransaction 出来
// willPerformAsynchronously 参数默认是 false,这个参数是可选的
// 参数是专门给 Android 准备的(后面会聊)表示这次 transaction 是否会异步执行
std::optional<MountingTransaction> MountingCoordinator::pullTransaction(
    bool willPerformAsynchronously) const {
  SystraceSection section("MountingCoordinator::pullTransaction");

  // 先上个锁,保护 baseRevision_、lastRevision_ 等共享状态
  std::scoped_lock lock(mutex_);
	// 准备一个空的 transaction
  auto transaction = std::optional<MountingTransaction>{};

  // 1. 处理常规情况
  if (lastRevision_.has_value()) {
    // 如果当前 revision 不为空,我们需要执行 tree diffing 后更新 transaction
    // 否则我们就不在常规情况处理 transaction
    number_++;
    auto telemetry = lastRevision_->telemetry;
    telemetry.willDiff();

    // tree diffing 逻辑
    // 本质就是对比两个 Revision,然后生成一个个 Mutation(一条具体的操作)
    // 具体实现在 https://github.com/facebook/react-native/blob/v0.76.0/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp#L1530-L1566 感兴趣的读者可以去看看
    auto mutations = calculateShadowViewMutations(
        *baseRevision_.rootShadowNode, *lastRevision_->rootShadowNode);

    telemetry.didDiff();

    // 把 Mutation 包装成 transaction(一批操作的打包结果)
    transaction = MountingTransaction{
        surfaceId_, number_, std::move(mutations), telemetry};
  }

  // 2. 处理覆盖情况
  // 这个功能主要是给一些 delegate 修改或者拦截 transaction 的机会
  // 一个典型的例子就是 LayoutAnimation(https://reactnative.dev/docs/layoutanimation)
  // 它可以在给 View 在移动/变化过程加个动画,让用户体感更加丝滑
  for (const auto& delegate : mountingOverrideDelegates_) {
    auto mountingOverrideDelegate = delegate.lock();
    // delegate 可以自己定义 shouldOverridePullTransaction 决定是否覆盖这次 transaction
    auto shouldOverridePullTransaction = mountingOverrideDelegate &&
        mountingOverrideDelegate->shouldOverridePullTransaction();

    if (shouldOverridePullTransaction) {
      SystraceSection section2("MountingCoordinator::overridePullTransaction");

      auto mutations = ShadowViewMutation::List{};
      auto telemetry = TransactionTelemetry{};

      if (transaction.has_value()) {
        // 如果这次有 transaction,就把这些信息传给 delegate
        mutations = transaction->getMutations();
        telemetry = transaction->getTelemetry();
      } else {
        // 否则就需要 "构造" 一个了
        number_++;
        telemetry.willLayout();
        telemetry.didLayout();
        telemetry.willCommit();
        telemetry.didCommit();
        telemetry.willDiff();
        telemetry.didDiff();
      }

      // 调用 delegate 自己实现的 pullTransaction 方法,并以这一版为准
      transaction = mountingOverrideDelegate->pullTransaction(
          surfaceId_, number_, telemetry, std::move(mutations));
    }
  }

	// ...省略部分调试用代码

  if (lastRevision_.has_value()) {
    // 更新 baseRevision_
    baseRevision_ = std::move(*lastRevision_);
    lastRevision_.reset();

    hasPendingTransactionsOverride_ = willPerformAsynchronously;
  }
  // 返回最后的 transaction
  return transaction;
}

至此 IOS 的挂载逻辑已经梳理完毕了,稍稍总结一下:

IOS 只在 schedulerShouldRenderTransactions 这个真正需要渲染的时候,才会主动拉取所有的 transaction,并一次性挂载它们

这个流程被称之为 Pull model

Android 的挂载流程

Android 也是通过 Scheduler 这个类负责协调 ShadowTree 的更新,所以跟 IOS 一样,我们先来看看 Android 在 schedulerDidFinishTransaction 方法中做了什么:

cpp 复制代码
// in packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp

void Binding::schedulerDidFinishTransaction(
    const MountingCoordinator::Shared& mountingCoordinator) {

  // 可以看到,对比在 IOS 中的 no-op,Android 在一开始就不一样了
  // Android 在这一步就 pullTransaction 了,这意味着 Android 的 tree diffing 会早于 IOS
  auto mountingTransaction = mountingCoordinator->pullTransaction(
      // 这里一般是 true,代表 Android 这次 pull 的 Transaction 不会被同步执行
    	// 而是会先被保存起来在未来某个时候异步执行
      ReactNativeFeatureFlags::
          fixMountingCoordinatorReportedPendingTransactionsOnAndroid());
  if (!mountingTransaction.has_value()) {
    return;
  }

  // 加个锁保护一下 pendingTransactions_
  std::unique_lock<std::mutex> lock(pendingTransactionsMutex_);
  // 找到当前 Surface 对应的 pendingTransaction
  auto pendingTransaction = std::find_if(
      pendingTransactions_.begin(),
      pendingTransactions_.end(),
      [&](const auto& transaction) {
        return transaction.getSurfaceId() ==
            mountingTransaction->getSurfaceId();
      });

  if (pendingTransaction != pendingTransactions_.end()) {
  // 如果当前 surface 已经有 pendingTransaction:把当前拿到的 Transaction 放入其中
    pendingTransaction->mergeWith(std::move(*mountingTransaction));
  } else {
    // 否则放一个新的 pendingTransaction 进去
    pendingTransactions_.push_back(std::move(*mountingTransaction));
  }
}

可以看到,从第一步开始 Android 就不一样了,接下来我们来看看 schedulerShouldRenderTransactions 方法:

cpp 复制代码
// in packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp

void Binding::schedulerShouldRenderTransactions(
    const MountingCoordinator::Shared& mountingCoordinator) {
  // 先拿一下 MountingManager,它是 Android 平台侧真正执行 mount 的对象
  auto mountingManager =
      getMountingManager("schedulerShouldRenderTransactions");
  if (!mountingManager) {
    return;
  }

  // 这里是一个功能的 flag,这个 flag 在目前最新版本的 RN 已经被去掉并默认走 true 的流程
  // 这里主要是为了修复一个场景的问题:在 else 中的逻辑会给整个 executeMount 加锁
	// 如果 executeMount 中又提交了 commit,甚至又执行了 schedulerShouldRenderTransactions,可能导致死锁
  // 因为在 0.76 版本这个修复还没有被充分验证,所以 RN 团队先加了个锁
  if (ReactNativeFeatureFlags::
          allowRecursiveCommitsWithSynchronousMountOnAndroid()) {
    std::vector<MountingTransaction> pendingTransactions;

    {
      // 只在获取 pendingTransactions_ 的时候加锁,并且把它保存起来
      std::unique_lock<std::mutex> lock(pendingTransactionsMutex_);
      pendingTransactions_.swap(pendingTransactions);
    }

    // 执行 mount 的逻辑,这里不加锁所以就算在这个时候提交的 commit 也不需要等待
    // 可以尽可能的对齐 IOS 的挂载逻辑(对应 RCTMountingManager 中 initiateTransaction 方法的循环)
    for (auto& transaction : pendingTransactions) {
      mountingManager->executeMount(transaction);
    }
  } else {
    std::unique_lock<std::mutex> lock(pendingTransactionsMutex_);
    for (auto& transaction : pendingTransactions_) {
      mountingManager->executeMount(transaction);
    }
    pendingTransactions_.clear();
  }
}

Android 在 schedulerDidFinishTransaction 的时候就默默的做 tree diffing,然后把生成的 transaction 收集起来,等到 schedulerShouldRenderTransactions 的时候再一次性交给 FabricMountingManagerexecuteMount 方法

至于为什么 Android 要这么做,请容我先买个关子,我们先来看看 executeMount 到底做了什么,最后再一起解释:

cpp 复制代码
// in packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp

/**
 * executeMount 函数的目标是:
 *  - 把 C++ 的 MountingTransaction 转成 Android Java 侧能消费的一批 mount instructions
 *  - 然后通过 JNI 调 scheduleMountItem(...) 方法
 **/
void FabricMountingManager::executeMount(
    const MountingTransaction& transaction) {
  SystraceSection section("FabricMountingManager::executeMount");

  // 🚩第一部分:准备工作
  std::scoped_lock lock(commitMutex_);
  auto finishTransactionStartTime = telemetryTimePointNow();
  auto env = jni::Environment::current();
  // 拿到 transaction 基础信息
  auto telemetry = transaction.getTelemetry();
  auto surfaceId = transaction.getSurfaceId();
  auto& mutations = transaction.getMutations();
  auto revisionNumber = telemetry.getRevisionNumber();

  // 准备不同种类的 MountItem 容器
  std::vector<CppMountItem> cppCommonMountItems; // Create / Insert / Remove(从父节点移除)
  std::vector<CppMountItem> cppDeleteMountItems; // Delete(回收节点)
  std::vector<CppMountItem> cppUpdatePropsMountItems; // 更新 props
  std::vector<CppMountItem> cppUpdateStateMountItems; // 更新 state
  std::vector<CppMountItem> cppUpdatePaddingMountItems; // 更新 padding
  std::vector<CppMountItem> cppUpdateLayoutMountItems; // 更新 layout
  std::vector<CppMountItem> cppUpdateOverflowInsetMountItems; // 更新 OverflowInset(一个布局指标,用来描述某个 View 的真实内容或可交互边界超出了自身 layout frame 多少,从而让原生平台在不改变实际布局尺寸的前提下正确处理命中测试和边界相关行为)
  std::vector<CppMountItem> cppUpdateEventEmitterMountItems; // 更新 Event Emitter

  // 🚩第二部分:收集、分类 Mutation
  {
    std::lock_guard allocatedViewsLock(allocatedViewsMutex_);

    // 找当前 surface 有哪些 tag 已经分配过原生 view
    auto allocatedViewsIterator = allocatedViewRegistry_.find(surfaceId);
    auto defaultAllocatedViews = std::unordered_set<Tag>{};
    // 找不到就用默认的空集合
    const auto& allocatedViewTags =
        allocatedViewsIterator != allocatedViewRegistry_.end()
        ? allocatedViewsIterator->second
        : defaultAllocatedViews;
    if (allocatedViewsIterator == allocatedViewRegistry_.end()) {
      LOG(ERROR) << "Executing commit after surface was stopped!";
    }

    // 遍历所有的 mutations 操作(对应 IOS TelemetryController::pullTransaction 的 doMount 方法)
    for (const auto& mutation : mutations) {
      const auto& parentShadowView = mutation.parentShadowView;
      const auto& oldChildShadowView = mutation.oldChildShadowView;
      const auto& newChildShadowView = mutation.newChildShadowView;
      auto& mutationType = mutation.type;
      auto& index = mutation.index;

      bool isVirtual = mutation.mutatedViewIsVirtual();
      // 根据不同的 mutationType 把 mutation 分配到不同的 MountItem 中
      // 具体的实现感兴趣的读者可以自己看看代码,受限篇幅这里就不赘述了
      switch (mutationType) {
        case ShadowViewMutation::Create: {
          // ...省略部分实现
        }
        case ShadowViewMutation::Remove: {
          // ...省略部分实现
        }
        case ShadowViewMutation::Delete: {
          // ...省略部分实现
        }
        case ShadowViewMutation::Update: {
          // ...省略部分实现
        }
        case ShadowViewMutation::Insert: {
          // ...省略部分实现
        }
        default: {
          break;
        }
      }
    }

    // 更新一下 allocatedViewRegistry_:
    // - 如果有新增的 View,加进去
    // - 如果有删除的 View,删除它们
    if (allocatedViewsIterator != allocatedViewRegistry_.end()) {
      auto& views = allocatedViewsIterator->second;
      for (const auto& mutation : mutations) {
        switch (mutation.type) {
          case ShadowViewMutation::Create:
            views.insert(mutation.newChildShadowView.tag);
            break;
          case ShadowViewMutation::Delete:
            views.erase(mutation.oldChildShadowView.tag);
            break;
          default:
            break;
        }
      }
    }
  }

  // 🚩第三部分:准备与 Java 通信
  
  // 因为 Android 与 IOS 不一样,Object-C 与 C++ 语言墙比较薄,双方可以低成本持有对方变量(内存)
  // 但是 Java 需要通过 JNI 与 C++ 通信
  // 这里先计算需要多少 JNI 的 buffer,以免后续频繁扩缩容导致的额外开销
  int batchMountItemIntsSize = 0;
  int batchMountItemObjectsSize = 0;
  computeBufferSizes(
      batchMountItemIntsSize,
      batchMountItemObjectsSize,
      cppCommonMountItems,
      cppDeleteMountItems,
      cppUpdatePropsMountItems,
      cppUpdateStateMountItems,
      cppUpdatePaddingMountItems,
      cppUpdateLayoutMountItems,
      cppUpdateOverflowInsetMountItems,
      cppUpdateEventEmitterMountItems);

  // 取得 Java 方法 createIntBufferBatchMountItem 的句柄
  // 主要功能是把要发给 Java 的数据打包成一个 Batch
  static auto createMountItemsIntBufferBatchContainer =
      JFabricUIManager::javaClassStatic()
          ->getMethod<jni::alias_ref<JMountItem>(
              jint, jintArray, jni::jtypeArray<jobject>, jint)>(
              "createIntBufferBatchMountItem");
	// 取得 Java 方法 scheduleMountItem 的句柄
  // 主要功能是把上面方法打包的这批 batch 工作提交给 Java 侧调度执行
  static auto scheduleMountItem = JFabricUIManager::javaClassStatic()
                                      ->getMethod<void(
                                          JMountItem::javaobject,
                                          jint,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jlong,
                                          jint)>("scheduleMountItem");

  // 如果没有任何 MountItem
  if (batchMountItemIntsSize == 0) {
    auto finishTransactionEndTime = telemetryTimePointNow();
    // 还是需要调用 scheduleMountItem,只不过需要传 nullptr
    // 这样 Java 侧才能收到这次 transaction 的 telemetry 数据
    scheduleMountItem(
        javaUIManager_,
        nullptr,
        telemetry.getRevisionNumber(),
        telemetryTimePointToMilliseconds(telemetry.getCommitStartTime()),
        telemetryTimePointToMilliseconds(telemetry.getDiffStartTime()),
        telemetryTimePointToMilliseconds(telemetry.getDiffEndTime()),
        telemetryTimePointToMilliseconds(telemetry.getLayoutStartTime()),
        telemetryTimePointToMilliseconds(telemetry.getLayoutEndTime()),
        telemetryTimePointToMilliseconds(finishTransactionStartTime),
        telemetryTimePointToMilliseconds(finishTransactionEndTime),
        telemetry.getAffectedLayoutNodesCount());
    return;
  }

  // 分配 JNI buffer
  jintArray intBufferArray = env->NewIntArray(batchMountItemIntsSize);
  auto objBufferArray =
      jni::JArrayClass<jobject>::newArray(batchMountItemObjectsSize);

  // 准备写入 intBufferArray 与 objBufferArray
  int intBufferPosition = 0;
  int objBufferPosition = 0;
  int prevMountItemType = -1;
  jint temp[8];
  
  // 以下代码都是将各种 MountItems 放进 intBufferArray 与 objBufferArray 的逻辑
  // 放入的顺序非常重要,因为 Android 会依据顺序一一执行(比如 Delete 一定要在 Create/Insert/Remove 之前否则节点被回收了就没法操作了)
  for (int i = 0; i < cppCommonMountItems.size(); i++) {
    // 将 CommonMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }
  if (!cppUpdatePropsMountItems.empty()) {
    // 将 UpdatePropsMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }
  if (!cppUpdateStateMountItems.empty()) {
    // 将 UpdateStateMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }
  if (!cppUpdatePaddingMountItems.empty()) {
    // 将 UpdatePaddingMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }
  if (!cppUpdateLayoutMountItems.empty()) {
    // 将 UpdateLayoutMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }
  if (!cppUpdateOverflowInsetMountItems.empty()) {
    // 将 UpdateOverflowInsetMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }
  if (!cppUpdateEventEmitterMountItems.empty()) {
    // 将 UpdateEventEmitterMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }
  if (!cppDeleteMountItems.empty()) {
    // 将 DeleteMountItems 内容经过解析后放进 intBufferArray 与 objBufferArray
    
    // ...省略部分代码
  }

  // 把 batchMountItemIntsSize、batchMountItemObjectsSize 打包成一个 batch
  auto batch = createMountItemsIntBufferBatchContainer(
      javaUIManager_,
      surfaceId,
      batchMountItemIntsSize == 0 ? nullptr : intBufferArray,
      batchMountItemObjectsSize == 0 ? nullptr : objBufferArray.get(),
      revisionNumber);

  auto finishTransactionEndTime = telemetryTimePointNow();
  
  // 🚩第四部分:正式与 Java 通信

  // 把打包好的 batch 提交给 Java 侧 FabricUIManager 调度执行
  scheduleMountItem(
      javaUIManager_,
      batch.get(),
      telemetry.getRevisionNumber(),
      telemetryTimePointToMilliseconds(telemetry.getCommitStartTime()),
      telemetryTimePointToMilliseconds(telemetry.getDiffStartTime()),
      telemetryTimePointToMilliseconds(telemetry.getDiffEndTime()),
      telemetryTimePointToMilliseconds(telemetry.getLayoutStartTime()),
      telemetryTimePointToMilliseconds(telemetry.getLayoutEndTime()),
      telemetryTimePointToMilliseconds(finishTransactionStartTime),
      telemetryTimePointToMilliseconds(finishTransactionEndTime),
      telemetry.getAffectedLayoutNodesCount());

  // 释放 JNI 局部引用 slot,允许 intBufferArray 这个 Java 对象更早变得可回收
  env->DeleteLocalRef(intBufferArray);
}

总结一下,executeMount 方法做了两件事:

  1. mutation 根据 mutationType 分类,然后将这些分类打包成一个 batch
  2. 把 batch 提交给 Java 侧的 FabricUIManager 调度执行

接下来来看看 FabricUIManagerscheduleMountItem 做了什么:

java 复制代码
// in packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

private void scheduleMountItem(
      @Nullable final MountItem mountItem,
      int commitNumber,
      long commitStartTime,
      long diffStartTime,
      long diffEndTime,
      long layoutStartTime,
      long layoutEndTime,
      long finishTransactionStartTime,
      long finishTransactionEndTime,
      int affectedLayoutNodesCount) {
    // 记录时间,用来计算相关耗时
    long scheduleMountItemStartTime = SystemClock.uptimeMillis();
  	// 检查 mountItem(就是我们刚刚说的 batch)的类型
    boolean isBatchMountItem = mountItem instanceof BatchMountItem;
    boolean shouldSchedule =
        (isBatchMountItem && !((BatchMountItem) mountItem).isBatchEmpty())
            || (!isBatchMountItem && mountItem != null);

    // 向所有监听者发送 "mount items 已经被 schedule 了" 的通知
    for (UIManagerListener listener : mListeners) {
      listener.didScheduleMountItems(this);
    }

  	// 更新一组 Java 侧的性能统计字段
    if (isBatchMountItem) {
      mCommitStartTime = commitStartTime;
      mLayoutTime = layoutEndTime - layoutStartTime;
      mFinishTransactionCPPTime = finishTransactionEndTime - finishTransactionStartTime;
      mFinishTransactionTime = scheduleMountItemStartTime - finishTransactionStartTime;
      mDispatchViewUpdatesTime = SystemClock.uptimeMillis();
    }

    if (shouldSchedule) {
      // 如果当前不在 UI 线程,就先保存起来,后面再调度执行
      mMountItemDispatcher.addMountItem(mountItem);
      Runnable runnable =
          new GuardedRunnable(mReactApplicationContext) {
            @Override
            public void runGuarded() {
              mMountItemDispatcher.tryDispatchMountItems();
            }
          };
      if (UiThreadUtil.isOnUiThread()) {
        // 如果在 UI 线程则直接执行(对应到我们在渲染的线程模型讲的 "在 UI 线程完成全部渲染" 场景)
        runnable.run();
      }
    }

    // ...省略一堆用于性能分析、调试和 profiling 的记录逻辑
  }

总结一下,scheduleMountItem 就做了两件事:

  1. 检查 mountItem 是否是正确的类型且是否为空
  2. 如果不为空判断一下当前在不在 UI 线程,在的话就直接尝试挂载;否则先把当前 mountItem 保存起来等待后续调度

这些被保存起来的 mountItem 会由 Android 的帧回调机制触发:

java 复制代码
// in packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

// 继承可以让这个类有重载特定帧回调方法的能力
private class DispatchUIFrameCallback extends GuardedFrameCallback {
		// ...省略部分代码

    @Override
    @UiThread
    @ThreadConfined(UI)
  	// doFrameGuarded 方法被 RN 重载了,目的是为了在 Android 帧回调的时候能够触发指定的逻辑
  	// 这里的执行必定在 UI 线程
    public void doFrameGuarded(long frameTimeNanos) {
      // ...省略部分代码

      try {
        // 先处理一些 pre-mount 的任务,比如提前创建/预分配原生 View
        mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos);
        // 真正 mount 的操作
  			// 最后会调用这里的方法:https://github.com/facebook/react-native/blob/v0.76.0/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.java#L74-L169 感兴趣的读者可以去看看
  			// 简单来说就是会遍历 transaction,然后根据类型选择看要调用哪一个 View 相关的方法创建/更新/插入/删除 View 元素
        mMountItemDispatcher.tryDispatchMountItems();
      } catch (Exception ex) {
        // ...省略部分错误处理代码
      } finally {
        // 重新注册下一帧的回调
        schedule();
      }

      // ...省略部分代码
    }
  }

至此 Android 的挂载逻辑已经梳理完毕了,稍稍总结一下:

Android 会在 schedulerDidFinishTransaction 方法被调用的时候就执行 tree diffing,然后把产生的 transaction 先暂存起来

后续在 schedulerShouldRenderTransactions 这个真正需要渲染的时候,会将这些 transaction 封装成一个 batch 后交给宿主侧,宿主会根据情况调度这些 transaction

这个流程被称之为 Push model,因为是否挂载的主动权把握在宿主手上,Fabric 只负责把 transaction 推给宿主,其他调度/执行由宿主负责

总结

本篇文章从 RN 新架构的渲染心智模型讲起,逐渐深入到具体的 Fabric 的代码实现,最后引出了 IOS、Android 双端实现的最大差异点:Mount 阶段

为什么会有这些差异呢?

首先根据代码仓库中的注释,Android 的推模型只是一个过渡,将来计划要慢慢向 IOS 的拉模型靠拢的

一个相对可靠的解释是,由于一些历史原因,Android 挂载层不像 IOS 只需要知道 "最终的树长什么样",它在某些时期还依赖 commit 之间的 payload / raw props 变化信息;这会直接影响为什么 Android 不能像 iOS 那样简单把 mount 收敛到一个最终消费点

如果想知道详细背景的读者可以看看这个 PR

参考文献

Render, Commit, and Mount

Threading Model

相关推荐
码徒2 小时前
2026 前端技术十大趋势:84% 的开发者已经在用 AI 写代码了
前端·agent·ai编程
在西安放羊的牛油果2 小时前
Connect 源码深度解析
前端·架构·代码规范
JasonYin2 小时前
多级关联列表预览
前端
Moment2 小时前
AI全栈入门指南:使用 NestJs 创建第一个后端项目
前端·javascript·后端
巫山老妖2 小时前
🧪 AI+测试:当AI遇上软件测试,效率提升10倍不是梦!
前端
蜡台2 小时前
Vue3 props ref router 数据通讯传输等使用记录
前端·javascript·vue.js·vue3·router·ref
Cobyte2 小时前
从 JavaScript 的角度理解 Python 语法
前端
travel_wsy2 小时前
vue Pinia 状态管理库
前端·pinia
巫山老妖2 小时前
📐 Embedding向量化:AI如何「理解」语义?万物皆可向量!
前端