一、前言
7 月 14 日,React Native 核心团队的 Joshua Gross 在 Twitter 说,RN 的新架构已经在 Facebook 内部落地了,并且99%的代码已经开源。其实,早在 2018 年 6 月,Facebook 官方就宣布了大规模重构 React Native 的计划及重构路线图,目的是为了让 React Native 更加轻量化、更适应混合开发,接近甚至达到原生的体验。
这次的架构升级对于 React Native 意义重大,按照官方的说法,升级后的RN性能将得到大幅提升,主要是解决诟病已久的性能问题,下图是React Native的一个版本发布说明。
为了让 RN 更轻量化、更适应混合开发,接近甚至达到原生的体验,React Native的优化措施包括以下几个方面:
- 改变线程模型,UI 更新不再同时需要在三个不同的线程上触发执行,而是可以在任意线程上同步调用 JavaScript 进行优先更新,同时将低优先级工作推出主线程,以便保持对 UI 的响应。
- 引入异步渲染能力,允许多个渲染并简化异步数据处理。
- 简化了 JSBridge的渲染逻辑,优化了底层渲染架构,让它更快更轻量。
二、新老架构对比
对于有过RN开发经验的都知道,原有RN架构 JS 层与 Native 的通讯过多的依赖 Bridge,而且是异步通讯,这就造成一些通讯频率较高的交互和设计就很难实现,同时也影响页面的渲染。而新架构也正是从这一点出发,对 Bridge 这层做了大量的改造,使得 UI 和 API 的调用,从原有异步方式调整到可以同步或者异步与 Native 通讯,解决了频繁通讯的瓶颈问题。
同时,新架构使用JSI(全称是 JavaScript Interface)代替原来的 Bridge, JS层直接调用 C++层进而调用 Java/OC 的方式,实现 JS 和 Java/OC 之间的相互操作,大大提高了通讯的效率。得益于JSI的全新架构,JavaScript 可以直接调用 Native 模块的方法。
三、旧架构
在介绍新架构之前,我们先看一下 RN框架目前的架构,以及它的一个缺点,以及为什么 Facebook 要重构整个框架。目前,RN使用的架构主要包含Native、JavaScript与Bridge三个部分。Native 管理 UI 更新及交互,JavaScript 调用 Native 能力实现业务功能,Bridge 在二者之间传递消息,整个架构如下图。
可以看到,React Native的架构还是很清晰的。最上层提供类 React 支持,运行在JavaScriptCore提供的 JavaScript 运行时环境中,Bridge 层将 JavaScript 与 Native 世界连接起来。然后,Bridge分为了三部分,其中Shadow Tree 用来定义 UI 效果及交互功能,Native Modules 提供 Native 功能(比如相册、蓝牙),而他们之间的相互通信 使用的是JSON 异步消息。
3.1 Bridge
在现在的架构中,Bridge 层是 React Native 技术的关键,它具有以下一些特点:
- 异步(asynchronous):不依赖于同步通信。
- 可序列化(serializable):保证一切 UI 操作都能序列化成 JSON 并转换回来。
- 批处理(batched):对 Native 调用进行排队,批量处理。
3.2 线程模型
在旧架构中,React Native一共有 3 个线程,分别是UI Thread、Shadow Thread和JS Thread。
- UI Thread:Android/iOS(或其它平台)应用中的主线程。
- Shadow Thread:进行布局计算和构造 UI 界面的线程。
- JS Thread:React 等 JavaScript 代码都在这个线程执行。
3.3 启动流程
对于RN应用来说,App 启动后首先需要初始化 React Native 运行时环境(即 Bridge),Bridge 准备好之后接着才能运行JS代码,然后执行Native 渲染。完整的启动过程如下:
其中,初始化 Bridge涉及到如下过程:
可以看到,初始化 Bridge主要分为4个步骤:
- 加载 JavaScript 代码:开发模式下从网络下载,生产环境从设备存储中读取
- 初始化 Native Modules:根据 Native Module 注册信息,加载并实例化所有 Native Module
- 注入 Native Module 信息:取 Native Module 注册信息,作为全局变量注入到 JS Context 中
- 初始化 JavaScript 引擎:即 JavaScriptCore
3.4 渲染流程
前面说过,React Native一共有 3 个线程,分别是UI Thread、Shadow Thread和JS Thread。
在渲染流程中,JS 线程将视图信息(结构、样式、属性等)传递给 Shadow 线程,创建出用于布局计算的 Shadow Tree,Shadow 线程计算好布局之后,再将完整的视图信息(包括宽高、位置等)传递给主线程,主线程据此创建视图。
对于需要响应的事件来说,则先由主线程将相关信息打包成事件消息传递到 Shadow 线程,再根据 Shadow Tree 建立的映射关系生成相应元素的指定事件,最后将事件传递到 JS 线程,执行对应的 JS 回调函数。
完整的渲染流程如下图:
通过上面的分析,不难发现现在的架构是强依赖 nativemodule,也就是大家通常说的 bridge,对于简单的 Native API 调用来说性能还能接受,而对于 UI 来说,每次的操作都是需要通过 bridge 的,包括高度计算、更新等,且 bridge 限制了调用频率、只允许异步操作,导致一些前端的更新很难及时反应到 UI 上,特别是类似于滑动、动画,更新频率较高的操作,所以经常能看到白屏或者卡顿。
四、新架构
现在的架构, JS 层与 Native 的通讯都太依赖Bridge,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能。基于上面的问题,在新的设计上,React Native 提出了几个新的概念和设计:JSI、Fabric和TuborModule。
- JSI(JavaScript interface):本次架构重构的核心重点,也正是JSI的缘故,原有重度依赖的 Native Bridge 架构得到解耦,JS 层与 Native 的通讯成本大大降低。
- Fabric:依赖 JSI 的设计,将旧架构下的 shadow tree 层移到 C++ 层,这样可以通过 JSI实现前端组件对 UI 组件的一对一控制,摆脱了旧架构下对于 UI 的异步、批量操作,降低了通讯成本。
- TuborModule:新的原生 API 架构,替换了原有的 Java module 架构,数据结构上除了支持基础类型外,开始支持 JSI 对象,让前端和客户端的 API 形成一对一的调用。
下面是React Native 的新的架构示意图:
可以看到,在新的架构方案上,Bridge层被新的JSI代替,不同于之前直接将 JavaScript 代码输入给 JSC,新的架构中引入了一层 JSI(JavaScript Interface)。作为 JSC 之上的抽象,JSI用来屏蔽 JavaScript 引擎的差异,允许换用不同的 JavaScript 引擎(比如Hermes)。
新的 JSI 层又包含了 Fabric 和 TurboModules 两部分。其中,Fabric负责管理 UI,TurboModules负责与 Native 交互。Fabric 以更现代化的方式去实现 React Native 的渲染层,简化之前渲染流程中复杂跨线程交互流程(React -> Native -> Shadow Tree -> Native UI)。而TurboModules也支持按需加载 Native 模块,从而缩短了RN初始化Native 模块带来的性能开销。
4.1 JSI
前面说过,为了升级RN的架构,RN提出了全新的JSI的概念,有了 JSI 之后,JavaScript 可以直接持有 C++对象的引用,并调用其方法。JSI 在 0.60 后的版本就已经开始支持,它是 Facebook 在 JS 引擎上设计的一个适配架构,它允许开发者向 JavaScript 运行时注册方法的 JavaScript 接口,而这些注册方法完全可以用 C++ 进行编写。除此之外,JSI还带来了如下的一些特性:
- 标准化的 JS 引擎接口,React Native 可以替换 v8、Hermes 等引擎。
- 优化升级 JS 和原生 java 或者 Objc 的通讯,但是不同于JSBridge采用的是内存共享、代理类的方式,为了实现和 Native 端直接通讯,JSI提供了一层 C++ 层实现的 JSI::HostObject,该数据结构支持 propName, 同时支持从 JS 传参。
- 原有 JS 与 Native 的数据通讯采用 JSON 和基础类型数据,但有了 JSI 后,数据类型更丰富,支持 JSI Object。
所以 ,在新架构下API 调用流程:JS->JSI->C++->JNI->JAVA,每个 API 更加独立化,不再需要全部依赖 Native module。但这也带来了另外一个问题,就是开发者在设计一个 API需要封装 JS、C++、JNI、Java 等一套接口,对开发者的要求还是比较高的。不过,好在Facebook提供了一个 codegen 模块,可以帮助开发者完成基础代码和环境的搭建。
关于封装jsi的过程,可以参考开发JSI Module
4.2 Fabric
Fabric 是RN新架构的 UI 框架,和原有的UImanager 框架的作用类似,不过UImanager的渲染性能与原生端组件和动画的渲染性能还是有很大的差距的。举个比较常见的问题,Flatlist 快速滑动的状态下,会存在很长的白屏时间,交互比较强的动画、手势很难支持,因此RN采用了全新的Fabric框架。
简单来说,JS 层新设计了 FabricUIManager,目的是支持 Fabric render 完成组件的渲染与更新。由于采用了 JSI 的设计,FabricUIManager可以和 cpp 层直接进行通讯,对应 C++ 层 UIManagerBinding,其实每个操作和 API 调用都有对应创建了不同的 JSI,从这里就彻底解除了原有的全部依赖 UIManager 单个 Native bridge 的问题,同时组件大小的 measure 也摆脱了对 Java、bridge 的依赖,直接在 C++ 层 shadow 完成,提升渲染效率。
有了 JSI 后,以前批量依赖 bridge 的 UI 操作,都可以同步的执行到 c++ 层,而在 c++ 层,新架构完成了一个 shadow 层的搭建,而旧架构是在 java 层实现,所以从这方面来说,渲染的性能也得到了大幅的提升。
4.3 TurboModule
在之前的架构中,Native Modules(无论是否需要用到)都要在应用启动时进行初始化,因为 Native 不知道 JavaScript 将会调用哪些功能模块。而新的TurboModules 允许按需加载 Native 模块,并在模块初始化之后直接持有其引用,不再依靠消息通信来调用模块功能。因此,应用的启动时间也会有所提升。并且,0.64 版本已经支持 TurboModule的使用。
总的来说,TurboModule的设计就是为了方便 JS 可以直接调用到 c++ 的方法。
4.4 CodeGen
新架构 UI 增加了 C++ 层的 shadow、component 层,而且大部分组件都是基于 JSI,因而开发 UI 组件和 API 的流程更复杂了,要求开发者具有 c++、JNI 的编程能力,为了方便开发者快速开发 Facebook 也提供了 codegen 工具,帮助生成一些自动化的代码。
CodeGen工具参考,同时,因 codegen 目前还没有正式 release,关于如何使用的文档几乎没有,还得等开源后才会知道。
另外,JSI、Turbormodule 已经在最新的版本上已经可以体验,而且开发者社区也用 JSI 开发了大量的 API 组件,例如:
- https://github.com/mrousavy/r...
- https://github.com/mrousavy/r...
- https://github.com/mrousavy/r...
- https://github.com/software-m...
- https://github.com/BabylonJS/...
- https://github.com/craftzdog/...
- https://github.com/craftzdog/...
从最新的更新情况来看,RN的新架构离发布似乎已经进入倒计时,作为RN的忠实粉丝,也一直希望RN能够尽快的发布1.0 版本。