移动端的发展

上世纪 90 年代,3Com 公司的 Palm OS 成为移动领域(掌上电脑)的霸主,市场占有率达 90%。直到 20 世纪末,微软推出了 Windows CE 和 Windows Mobile,取代了 Palm OS。随后,Symbian 和 Blackberry 昙花一现,也曾一度占有 40%的市场。再后来,W3C 立项 HTML5,并于 2008 年 1 月发布 HTML5 第一份正式草案。2007 年 1 月,苹果推出了第一部 iPhone,iOS 系统诞生。同年 11 月,google 宣布推出 Android 系统,并于次年 10 月发布了第一部 Android 智能手机。至此,移动端的基本格局已经奠定。

2010-2012 年:FaceBook 牵头成立 Moblie Web 工作组,大举进攻移动领域,Web App 的呼声越来越高,一度认为 Native App 在 3 年内会消亡

2012 年:FaceBook 宣布放弃使用 HTML 5 构造自己的主题应用,HTML 5 进入最惨淡的一年

2014 年 10 底:HTML5 定稿,迎来了原生+HTML5 的混合开发模式

2015 年:FackBook 推出 React Native

2018 年 2 月:Google 发布 Flutter 第一版本

2018 年 6 月:FaceBook 宣布对 React Native 进行大规模重构,并在 2023 年基本完成重构工作

跨平台方法的对比

[!question] 为什么要进行跨平台的研究
【微观上】:企业要提高开发效率,降低开发成本,实现一次编码,到处运行;【宏观上】:现在处于多端发展时代,多种终端生态互不兼容,造成生产力浪费,势必会有一种新技术代表先进生产力

Native App

第三方应用程序会与平台进行交互,以创建 widgets(组件) 或访问相机等服务。其中 widgets 呈现给屏幕画布,并将事件传回给 widgets

基于 WebView 的第一代跨平台框架

第一代跨平台框架基于 JavaScript 和 WebViews,代表者有:PhoneGap,微信小程序。

第三方应用程序创建 HTML 并将其显示在平台的 WebView 上,对于平台提供的一些系统服务,通过 JS Bridge 来调用。由于这些调用不是很频繁,JS Bridge 并不会成为性能瓶颈。然而,一个完整 HTML5 页面的展示要经历浏览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加 N 个数量级,所以这种方案的瓶颈在于 WebView 对于 H5 页面的渲染。这种开发模式开发的 App 既有原生应用代码又有 Web 应用代码,因此又被称为 Hybrid App(混合应用程序)

以 React Native 为代表的第二代跨平台框架

这种方案也称为泛 Web 容器方案 ,这种方案放弃了 WebView 渲染,采用原生自带的 UI 组件代替了核心的渲染引擎,所以这种方案的性能要比第一代方案好很多。代表者就是 RN 、Weex。同时这种方案保持了 JavaScript 作为开发语言,支持前端丰富的生态(比如 RN 使用 React.js 极大地方便了 UI 的创建)。由于前端和 Native 的交互都要通过中间的 Bridge,很自然的 Bridge 就成了这种方案的性能瓶颈

以 Flutter 为代表的第三代跨平台框架

为什么说 Flutter 是一种新的方案呢?因为他采用了一种自绘引擎的方式,和以往的方案都不一样;Flutter 既不用 WebView 进行组件渲染,也不适用原生组件进行渲染,他完全自己搞了一套跨平台 UI 渲染框架,渲染引擎依靠跨平台的 Skia 图形库来实现,手机平台只需要提供一块画布即可。同时开发语言使用即支持 JIT 又支持 AOT 的 Dart 语言,即提升了执行效率,也为支持动态化提供可能

ReactNative 架构一览

本文基于 React Native 0.54.3 版本 Android 的架构分析


RN 的老架构主要包含 React、JavaScript、Bridge 和 Native 四个部分。从上到下可以分成四层,分别是 JS 代码层、JS 引擎层、通信层、原生层。最上面的 JS 代码层提供 React.js 支持,React.js 的 JSX 代码转换为 JS 代码运行在 [[JavaScriptCore]] 提供的 JavaScript 运行时环境中,通信层将 JavaScript 与 Native 层连接起来;通信层又分为三部分,其中 Shadow Tree 用来定义 UI 效果及交互功能、Native Modules 提供 Native 功能(比如相册、蓝牙等)、而他们之间的相互通信使用的是 JSON 异步消息

基于上述架构,RN 运行时创建三个线程:

  • JS Thread」: 主要负责 React,JS 的执行,输出 App 的视图信息(结构、样式、属性等)
  • Shadow Thread」:根据 JS 线程的视图信息,创建出用于布局计算的 ShadowTree;(主要用到 UIManagerModule,是 RN 中非常重要的 Native Module,故也叫做 Native Module Thread)
  • Main Thead」:根据 ShadowTree 提供的完整试图信息,负责真实 Native View 的创建
    下面,将分为启动流程、渲染原理、通信机制三个部分详细剖析一下 RN 的实现原理

启动流程

总结起来,启动流程主要做了两件事:一件是准备环境,一件事调用 JS 侧的入口函数
准备环境:在后台创建上下文、初始化通信桥、加载 JSBundle、初始化 JS 执行环境。
调用 JS 侧的入口函数:即调用 Appregistry.js 的 runApplication 方法,为一次 Native 到 JS 的调用

渲染原理

RN 运行时会创建三个线程:JS Thread、Shadow Thread、Main Thread,在这三个线程中分别会创建三棵树,JS 线程中会创建一棵叫做Fiber Tree,在 Shadow 线程中会创建一棵树叫做Shadow Tree,在 UI 线程中则是 View Tree 。其中,Fiber Tree 在 JS 侧创建,Shadow Tree 和 View Tree 在 Native 测创建,RN 渲染机制的重点就是这三棵树的创建和同步,关键步骤如下:

  1. 第一步: 通过 React.js 的 JSX 定义 UI 结构
  2. 第二步: 编译阶段,通过 Babel 将 JSX 转化为 React.createElement 的形态
  3. 第三步: 在 JS 侧,通过深度优先遍历将 JSX 编写的 UI 组件转换为 Fiber Tree 结构,每个组件节点都包含子组件、父组件和兄弟组件的引用
  4. 第四步: JS 侧在创建 Fiber Tree 各个节点的时候会通过 Bridge 桥向 Native 侧发送对应的指令。Native 侧收到这些指令之后会创建对应的 Shadow Tree 节点,同时会生成对应的 UIViewOperation,加入到 UIViewOperationQueue 中,以供 UI 线程进行真正的 UI 操作。JS 侧发送完一批 UI 指令之后会触发 Native 侧的 onBatchComplete 回调,进而后续遍历 ShadowTree,分别计算每个节点的宽度和高度,然后前序遍历 ShadowTree,确定每个节点的最终位置,生成相应的 UpdateLayoutOperation,加入到 UIViewOperationQueue 中
  5. 第五步: 出发 FrameCallback,从 UIViewOperationQueue 中依次取出 UIViewOperation,生成对应的 View Tree,挂载到 RootView,进行原生 UI 渲染逻辑

[!question]- 虚节点和 Layout Only 节点区别?

  1. 虚节点在计算布局时会被忽略,也不会生成相应的 Native 节点
  2. LayoutOnly 节点指一个节点只会影响到它的子节点的位置,而本身不需要绘制任何内容,那么这个节点就是 LayoutOnly 节点,也不会生成相应的 Native 控件

通信机制


在 RN 中有三个线程:JS 线程、UI 线程、Shadow 线程(即 Native Modules 线程),而在 Native Modules 线程中,主要用于进行 Yoga 布局计算,同时也负责 C++层和原生通信。我们知道 Java 可以通过 JNI 的方式和 C++代码实现相互调用,而 Objective-C 可以直接调用 C++代码。JS 可以通过 [[JavaScriptCore]] 实现和 C++的相互调用,而 JavaScriptCore 是由 C++实现的 JS 引擎,所以很自然的,C++就成为了连接原生和 JS 的桥梁。

所以 RN 的通信机制总结起来就是一句话:一个 C++实现的 bridge (桥)打通了原生和 JS,实现了两者的相互调用

桥的初始化

在 RN 的启动流程中,会对通信桥进行初始化。通信桥的初始化最关键的就是创建两张表和建立两个桥。两张表中,一张是 JavaScriptModuleRegistry,供原生调用 JS 使用,一张是 NativeModuleRegistry,供 JS 调用原生使用;两个桥中,一个是 NativeToJSBridge,是原生调用 JS 的桥梁,一个是 JSToNativeBridge,JS 调用原生的桥梁。

Native 调用 JS

Native 调用 JS 的流程相对简单:

  1. 在 Java 层把要实现的功能编写成接口并继承 JavaScriptModule,并交由 ReactPackage 管理,最终会在RN初始化的时候添加到JavaScriptModuleRegistry注册表中
  2. JavaScriptModuleRegistry通过动态代理生成对应的JavaScriptModule,然后通过invoke()调用相应的JS方法,该方法会进一步去调用CatalystInstanceImpl.callJSFunction,该方法会通过JNI将相关参数传递到C++层
  3. C++层通过NativeToJsBridge将callFunction的消息放入消息队列等待执行;C++层中保有MessageQueue中的一些属性对象,通过这些属性对象进入JS层
  4. 在JS层里,找到对应的JavaScriptModule及方法执行

JS调用Native

在JSToNative的通信方式中,又分为两种调用方式:

  • 「异步调用」:指的是在JSToNative的通信方式中,调用的发起在JS线程,逻辑处理和计算在Native Module线程和UI线程,异步的方式不会阻塞JS线程
  • 「同步调用」:指的是调用和处理过程都发生在JS线程中;如果逻辑计算简单,这没什么影响,但是如果逻辑计算复杂,那肯定得卡死JS线程。


整个流程可以分为两个部分,第一个部分是JS调用Native,第二个部分是Native将执行结果回调至JS侧(和Native调用JS的流程很相似)
JS调用Native流程如下:

  1. 从JS侧进入C++层,通过JSC桥接获取Java Module的注册表,然后回到JS侧,将它转换为对应的JS Native Module,并根据不同的调用类型,将xxMethod的调用封装成消息,放入MessageQueue的队列里
  2. xxMethod消息处理的时候,会进入C++层,拿到对应的module信息,通过JSToNativeBridge,将该函数调用消息放到线程的消息队列中等待执行。此时C++层的函数调用会映射为同名的Java层JavaModuleWrapper对象,并调用其中的invoke方法,传入的参数是methodId和对应的参数信息
  3. Java层的JavaModuleWrapper对象,根据参数信息,找到对应的JavaMethodWrapper对象,再执行其invoke方法,通过反射调用对应的NativeModule,从而完成JS到Native的调用

性能瓶颈与新架构

基于此架构,中间层Bridge必然成为RN的性能瓶颈,其存在以下问题:

  1. 通信效率低下,容易出现堵塞

    JS层和Native层只能通过桥来通信,多次线程切换、串行消息处理、参数通过JSON序列化和反序列化传递,导致效率低下,容易出现堵塞

  2. 异步调用导致不能同步响应,用户体验差

    受限制于通信机制,RN里JS和Native侧相互之间只能异步调用,用户的操作和APP的响应是异步的,且之间可能会有不小的延迟,用户体验不佳

而新架构将解决这些问题,新架构的主要内容有:

  • JSI: 增加引擎抽象层,实现引擎解耦,同时支持JS持有C++ HostObject类型对象引用,实现JS和Native的相会感知
  • TurboModule:重构后的NativeModules,用于向前端暴露Native能力,实现NativeModule的按需加载和JS与Native的同步调用