RN 的初版架构——RN 应用的启动流程

7 阅读8分钟

这篇文章是本专栏关于 RN 旧架构的最后一篇文章

在这篇文章中,我希望能从系统设计的视角,跟读者一起过一遍 RN 是如何启动一个原生的 APP 并让它基于 JS 顺利运行的

这篇文章不会像之前的文章一样有具体代码的展示,但是会完整的描述 RN 中不同模块的职责以及模块相互之间的关系

心智模型

在开始过 RN 应用的启动流程之前,我们先用上帝视角复习一次 RN 的架构

+-----------------------------+        +------------------------------+
|        Native World         |        |      JavaScript World        |
|                             |        |                              |
|  Native Host Environment    |<------>|   JS Runtime Environment     |
|  (OS Process, UI Thread)    | Bridge |   (JS Thread, Event Loop)    |
|                             |        |                              |
+-----------------------------+        +------------------------------+
                |
                v
       Native Rendering System

从上帝视角来看,RN 的架构可以被分成两个世界:

  • 原生世界:负责提供应用运行的环境
  • JS 世界:负责提供应用运行的逻辑

维系这两个世界的只有一条 Bridge,而这两个世界的最终目标,就是让 Native Rendering System 渲染出用户见到的画面


接下来,我们稍微降低一点高度,看看两个世界中分别有哪些关键角色(模块)

+--------------------------------------------------------------+
|                    Native Host Environment                   |
|                                                              |
|  +-------------------+        +---------------------------+  |
|  | UI Thread         |        | Scheduling / Queues       |  |
|  | (Render, Commit)  |        | (Async Coordination)      |  |
|  +-------------------+        +---------------------------+  |
|            ^                              |                  |
|            |                              v                  |
|     Native Rendering <---- UI Representation System          |
|                                                              |
+--------------------------^-----------------------------------+
                           |
                        Bridge
                           |
+--------------------------v-----------------------------------+
|               JavaScript Runtime Environment                 |
|                                                              |
|  +-------------------+        +---------------------------+  |
|  | JS Thread         |        | Event Loop                |  |
|  | (Logic, React)    |        | (Timers, Tasks)           |  |
|  +-------------------+        +---------------------------+  |
|                                                              |
+--------------------------------------------------------------+

我们先来看看 JS 运行时环境中的关键模块:

  • JS Thread 中负责运行 react 与 JS 的逻辑,并且负责将消息通过 Bridge 发给原生宿主环境
  • Event Loop 负责维护计时器以及调度 JS 中的任务(就是俗称的 JS Event Loop)

接下来看看原生宿主环境:

Scheduling / Queues 模块是消息进入当前环境的第一个模块,该模块负责控制这些消息该在什么时候在哪里被运行

在应用启动过程,一个关键步骤就是根据 JS 的指令渲染应用页面,页面相关的消息会被发送给 UI Representation System 进行处理

UI Representation System 的职责就是将 JS 传过来的消息转换成平台无关的 UI 描述(包括结构,布局,属性等,也就是大家常说的 Shadow Tree),核心目的就是描述 UI 应该长什么样子

Native Rendering 则是负责根据 UI 的描述构建原生 View 并将其 commit 给主线程 UI Thread 绘制,其核心目的就是决定 UI 实际的绘制方式

启动流程

在了解了 RN 框架的心智模型后,我们要开始启动流程了

按照时间线来梳理,启动流程大概分为以下几个阶段:

[ OS Start ]
     |
     v
[ Native Process ]
     |
     v
[ RN Runtime Setup ]
     |
     v
[ JS App Init ]
     |
     v
[ UI Description ]
     |
     v
[ Native Rendering ]
     |
     v
[ First Screen Visible ]

接下来我们根据不同的阶段一个一个聊

阶段一:应用进程启动(Native Process start)

在这个阶段包含了三个步骤,核心目的就是把应用主线程准备好

+-------------------+
| App Process start |
+-------------------+
     |
     v
+------------------+
| Native Host Env  |
+------------------+
     |
     v
+------------------+
| UI Thread Ready  |
+------------------+

阶段二:设置 RN 运行时(RN Runtime Setup)

当应用主线程准备好后,我们会开始进入到 RN 的逻辑中

其中最主要的三件事就是:

  1. 创建 JS 运行环境(JS thread)
  2. 初始化 Native modules(包括对页面渲染最重要的 UIManager)
  3. 创建 Bridge 并连接 JS 环境与 Native 环境
         +-------------------------------+
         |      Native Host Environment  |
         +-------------------------------+
          |                          | 
          |=== creates ===           |=== owns / creates ===         
          v                          |
+----------------------+             v
| JS Runtime Env       |         +---------------------------+
| (JS Thread)          |         | Native Module Registry    |
+----------------------+         +---------------------------+
          ^                         ^                      |    
          |                         |=== wires ===         |=== instantiates ===      
          |=== wires ===            |                      v               
          v                         v               +---------------------------+ 
        +-------------------------------+           | Native Modules            |
        |           Bridge              |           | (Native-side instances)   |
        +-------------------------------+           | - UIManager               |
                                                    | - DeviceInfo              |
                                                    | - Networking              |                       
                                                    | - AsyncStorage            |                  
                                                    +---------------------------+   

阶段三:JS App 初始化(JS App Init)

当 JS 环境被创建完成后,会接收到来自 Native 的 AppRegistry.runApplication 方法调用(具体流程可以参考本专栏的 UI 布局与绘制

这个方法最终会在 JS 侧创建出平台无关(platform agnostic)的虚拟 DOM 树

+----------------------+
| JS Runtime Env       |
+----------------------+
          |
          v
+----------------------+
| React App Init       |
+----------------------+
          |
          v
+----------------------+
| Logical UI Tree      |
| (Platform-agnostic)  |
+----------------------+

阶段四:创建 Native 平台 UI 描述(UI Description)

当 JS 侧创建出虚拟 DOM 后,它会通过一系列约定好的方法通过 Bridge 调用 Native 侧 UIManager 模块的方法

在这个阶段中,UIManager 会在 Native 侧创建虚拟树(或者称为 Shadow tree)并在随后交给 yoga 计算布局

+----------------------+
| Logical UI Tree (JS) |
+----------------------+
          |
          | serialized updates
          v
+----------------------+
| Bridge               |
+----------------------+
          |
          v
+----------------------+
| UI Representation    |
| System               |
+----------------------+
          |
          v
+----------------------+
| Layout Calculation   |
+----------------------+

阶段五:原生渲染(Native rendering)

在这个阶段 RN 会在 Native 侧收集 view 的变化(新增、更新、移除等)并且在主线程(UI thread)中创建/更新原生 view

然后 RN 会在这些原生 view 上应用 yoga 的布局结果,并进入提交阶段

当提交与绘制阶段完成后,我们就能在屏幕中看到原生渲染的 RN 页面(First paint)啦~🥳

+----------------------+ 
| Layout Output        |
+----------------------+
          |
          v
+----------------------+
| Native Rendering     |
| System               |
+----------------------+
          |
          v
+----------------------+
| UI Thread Commit     |
+----------------------+
          |
          v
[ 🎉 First Screen Visible ]

设计理由

看完 RN 架构的读者可能会好奇为什么要设计成如此复杂的一个结构

我想在关于 RN 旧架构的最后一篇文章中聊聊我个人理解的设计理由:

首先从 RN 团队的角度考虑,他们手上已经有了 react 这个框架的支持,他们需要的是一个能够利用好 react 框架优势的跨端框架

其次从原生平台的角度考虑,想要顺利渲染出 UI,要么想办法 hack 进当前的渲染机制;要么用一套全新的渲染机制替换掉原生平台当前的渲染方案


而手握 react 的 RN 团队,其实并不需要重新发明一套 UI 描述语言。他们已经拥有了一套成熟的、声明式的 UI 抽象模型,也就是 react 的组件树和状态更新机制。真正的问题并不是如何描述 UI,而是如何让这套描述在不同平台上安全、稳定地变成真实的 UI

问题的转变决定了 RN 的第一个核心选择:由 JS 负责描述意图,而非参与具体实现

所以在 RN 中 JS 并不会直接创建 view、计算布局或参与 UI 绘制,它只负责描述 “我想要一个什么样的 UI”,而具体该 UI 如何实现,则是交给了各自的原生平台


一旦确定了双方的分工,随之而来的问题就是两者的边界要如何划分?RN 选择了最保守同时确定性最高的方案:完全隔离+异步通信

于是 Bridge 就在这个背景下出现了。Bridge 不是一个性能优化手段,它只是一个基于隔离策略的产物,它的职责就是把 JS 的意图(逻辑)安全且可控的传递到 Native 侧


Bridge 的存在解决了 JS 与 Native 互相通信的问题,但是当消息跨过 Bridge 之后,依然会面临许多问题,比如:

  • 任务要在哪个线程执行?UI 线程还是后台线程?
  • 任务要什么时候执行?立刻执行还是合并后执行?

所以在 Bridge 之下,我们还需要一个线程与调度系统来解决这些问题,它保证了 JS 的高频更新不会直接压垮 Native 的 UI 线程、也保证了 UI 的提交发生在一个安全且可预测的时刻


所有的这些设计最后都指向了一个核心目的:原生渲染安全性

在 RN 中,真正触碰 UIKit/Android View 的代码始终运行在受控的 UI 线程中,且只接受准备好的、经过调度的更新指令

无论 JS 是否阻塞、是否抛出异常、是否出现逻辑错误,原生的渲染系统始终保持稳定,且完全受 Native 侧控制

最后,整条逻辑链路梳理如下:

[ Cross-Platform Requirement ]
              |
              v
[ JavaScript describes intent ]
              |
              v
[ JS / Native Isolation ]
              |
              v
[ Asynchronous Bridge ]
              |
              v
[ Threaded Scheduling ]
              |
              v
[ Native Rendering Safety ]

从上述的链路来看,RN 并非刻意设计的如此复杂,而是在跨平台需求React 能力原生平台约束之间做出的权衡

总结

本文从 RN 应用的启动流程出发,介绍了 RN 旧架构的心智模型与设计理由。从架构的视角看,旧架构可以说在现有的限制下已经优化的相当完善了,但是随着时间的变化,更丰富的手势、更多的互动动画,以及更流畅的交互需求(从 60hz~120hz 甚至更高)也对这个架构提出了更多挑战

基于 Bridge 的架构瓶颈也一点点的暴露出来,比如:

  • 更多的通信使得序列化/反序列化成本急剧攀升
  • 所有的通信都必须经过 Bridge 给 Bridge 以及线程调度系统带来巨大压力
  • 高频通信(比如动画、手势、layout 信息)与低频通信(比如日志、配置、设备信息)共享一条信息通道,没有优先级之分

这些瓶颈往往会倒逼着开发者去针对框架本身写应对代码,而不能把精力集中于自身的产品中

当越来越多的代码被用来绕过框架限制而非完善产品时,就是一个明确的框架升级信号了

于是,基于 JSI 的 RN 新框架被提上了开发日程,本专栏后面的文章也将跟读者一起学习 RN 的新架构,敬请期待~