前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

React Native原理浅析从JavaScriptCore到Bridge

首页2019-10-02 18:30:12Front-End
RNReact原理

第一次接手 React Native 项目的时候我有个疑问一直没想通。写的明明是 JSX,跑出来的却是货真价实的原生控件,中间那一步到底是怎么发生的?后来又碰到几次奇怪的问题,动画在 JS 线程忙的时候会卡,某个原生方法调过去半天没反应,报错栈里全是 RCT 开头的类名。这类问题不理解底层模型是修不掉的,只能靠碰运气。

这篇是我把 React Native 运行原理捋了一遍之后的笔记。从 JavaScriptCore 这个发动机讲起,到 Bridge 怎么把两个世界接上,再到初始化时那几步都发生了什么、三个线程各自在忙什么。最后会补一段现状,因为 2019 年这套基于 Bridge 的模型,现在已经被新架构替换掉了。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • JavaScriptCore 在 React Native 里扮演什么角色,为什么 iOS 上绕不开它
  • 拿浏览器的工作方式做参照,看清 RN 到底替换掉了哪一环
  • React Native 的四层架构分别是什么,哪一层是你会动的
  • React、React Native、JavaScriptCore 三者的关系,谁驱动谁
  • Bridge 的作用,以及为什么 JS 和原生之间只能传字符串
  • RCTRootView 到 MessageQueue,Bridge 各个模块分别管什么
  • RN 初始化的四个步骤,从空引擎到第一帧渲染
  • Shadow queue、主线程、JS 线程三条线各自的职责
  • Android 侧的层次架构和 Java 与 JS 的双向通信机制
  • 新架构(JSI / Fabric / TurboModules)把这套模型改成了什么样

# 一、JavaScriptCore 是这一切的发动机

讲 React Native 之前,先得知道 JavaScriptCore 是什么,这个绕不过去。

React Native 的核心驱动力就来自 JS 引擎。你写的所有 JS 和 JSX 代码都要交给它执行,没有 JS 引擎的参与,React 那套东西一行都跑不起来。在 iOS 上默认用的是系统自带的 JavaScriptCore,iOS 7 之后的设备都支持。这里有个硬约束,苹果不允许第三方应用带自己的 JIT 编译器,所以你没法在 iOS 上随意换一个自己的 JS 引擎。JavaScriptCore 出自 WebKit,Android 早期也是打包了一份 JavaScriptCore 进来用。

所以我一直觉得,深入了解 React Native 的第一站应该是 JavaScriptCore。

它在 iOS 平台上给 React Native 提供的接口其实只有那么几个,核心就是「求值一段 JS 源码」「往全局对象上注入一个原生函数」「把 JS 的返回值转成原生类型」。听着少得可怜,但这几个接口就是全部的入口。把它们弄明白了,React Native 剩下的那些看起来像魔法的东西,都可以顺藤摸瓜分析出来。

后面几节要讲的,就是 Facebook 围绕这几个接口,加上一个 React,做出了怎样一套设计。

# 二、拿浏览器做参照,看清 RN 替换掉了哪一环

理解 RN 最快的路子,是先想清楚浏览器怎么干活,然后看 RN 在哪一步分了岔。

浏览器的流程大致是这样。它读懂 HTML 和 CSS,HTML 告诉它要画什么控件,CSS 告诉它每个控件长什么样。解析 HTML 形成 DOM 树,再用 CSS 去装饰树上的每一个节点。浏览器内部有一整套按 HTML 标准实现的 UI 控件,最后由这些控件调用操作系统的绘图指令,把像素画到屏幕上。在这个流程里 JavaScript 其实可有可无,它主要负责响应用户事件、操作 DOM、发异步请求、做点简单计算。

这套设计最值钱的地方,是把「UI 的描述」和「UI 的呈现」分开了。

拆成四步看更清楚。第一步,HTML 文本描述页面有哪些元素,CSS 描述它们该长什么样。第二步,浏览器引擎解析这两者,翻译成一系列预定义的 UI 控件。第三步,UI 控件调用操作系统绘图指令绘制图像展现给用户。第四步,JavaScript 在旁边处理交互逻辑。

React Native 保留了第一步和第二步的思路。你依然是用一种声明式的语法描述页面有哪些元素,用 StyleSheet 描述它们长什么样,而且执行这套逻辑的还是同一个 JS 引擎。

分岔发生在第三步。

RN 里的 UI 控件不再是浏览器内置的那套,而是自己实现的一套原生控件,Android 一套、iOS 一套。这个切换动作是在 MessageQueue 里完成的,顺带你会发现两端控件的 tag 命名也不一样。所以最终画到屏幕上的是货真价实的原生控件,不是模仿原生外观的网页元素,这也是 RN 相比 WebView 方案在体验上的根本优势。

那 JavaScript 的地位就完全不同了。在浏览器里它是配角,在 RN 里它是绝对的主角。它负责管理 UI 组件的生命周期,管理 Virtual DOM,所有业务逻辑都靠它实现或者衔接,还要负责调用原生代码去操纵原生组件。有一点要记住,JavaScript 自己是没有任何绘图能力的,它做的全部事情就是算出「该画成什么样」,然后把指令发给原生组件去画。

记住这句话,后面讲 Bridge 的时候会反复回到它。

# 三、React Native 的四层架构

下面这张图把 RN 的分层画得很清楚,用颜色区分了每一层由谁负责。

React Native 架构分层图,绿色为业务代码,蓝色为跨平台引擎,黄色为平台相关的 bridge,红色为系统平台

绿色是应用开发的部分,也就是你写的业务代码,日常 90% 的时间都花在这一层。

蓝色是公用的跨平台代码和工具引擎,React 本身、Virtual DOM 的调和逻辑、Yoga 布局引擎都在这里。这层一般不会动,除非你要给上游提 PR。

黄色是平台相关的代码,做定制化的时候会在这里增删。它不跨平台,得针对每个平台单独写,iOS 写 Objective-C,Android 写 Java,Web 写 JS。不过每个 bridge 都有一个对应的 JS 文件,这部分是可以共享的,写一份就够了。要做三端融合,你必须理解这一层;要自己封装一个原生控件给 JS 用,你写的就是这一层。

红色是系统平台本身,UIKit、Android View 体系、各种系统服务。注意红色上面那条虚线,它表示所有平台相关的东西都被 bridge 隔在了外面,上层看不到具体平台。

分层的价值就在那条虚线上。它意味着你可以在完全不碰红色和黄色的前提下,写出一个能同时跑在两端的应用。

# 四、React、React Native 和 JavaScriptCore 的关系

这三个名字经常被混着说,理清它们的关系是理解 RN 的关键。

React 是一个纯 JS 库。所有 React 代码和其它 JS 代码一样,都需要 JS 引擎来解释执行。因为安全模型的限制,浏览器里的 JS 代码不允许调用自定义的原生代码,而 React 最初就是为浏览器 JS 开发的一套库。它封装了一套 Virtual DOM 的概念,实现了数据驱动的编程模式,为复杂的 Web UI 提供了一种状态管理机制。标准 HTML 和 CSS 之外的事情,它无能为力。调用原生控件、驱动摄像头、读写磁盘文件、自定义网络库,这些 React 都做不到。

可以简单把 React 理解成一个纯函数,接受特定格式的数据,输出计算好的描述数据。JS 引擎负责调用并运行这个函数。

那 React Native 呢?它复杂得多,复杂在两个地方。

第一点是驱动关系反过来了。在浏览器里,是 JS 引擎驱动 React 脚本执行,React 最终由浏览器驱动。到了 React Native 这里,是原生代码(Timer 和用户事件)在驱动 JS 引擎,JS 引擎解析执行 React 和相关的 JS 代码,把计算好的结果返回给原生代码,然后原生代码根据这个结果去驱动设备上所有能驱动的硬件。注意是所有硬件。在 RN 这里,JS 代码已经摆脱了浏览器沙箱的限制,能调用任何原生接口。

第二点是绘制方式不一样。它利用 React 的 Virtual DOM 和数据驱动模式简化了原生应用的开发,但它自己不负责绘制,只算出绘制指令,最终的绘制交给原生控件,用户体验和纯原生是一致的。

顺着上面聊,驱动硬件的能力决定了一个软件能做多大的事。写过底层或者汇编的同学都清楚,我们平时写的大多是受限代码,很多特权指令用不了,很多设备不允许直接驱动。RN 干的就是把这道限制在 JS 侧打开。

从这个角度看,React Native 和 Node.js 有异曲同工之妙。两者都是通过扩展 JavaScript 引擎,让它具备调用本地资源和原生接口的能力,然后借助 JavaScript 丰富的生态和稳定的跨平台特性,把 JS 的能力发挥到浏览器之外的地方去。

所以可以把等式写成这样,JavaScriptCore 加 React 加 Bridge,就是 React Native。

  • JavaScriptCore 负责 JS 代码的解释执行
  • React 负责描述和管理 Virtual DOM,指挥原生组件绘制和更新,大量计算逻辑也在 JS 里进行。它自身不直接绘制 UI,因为绘制是非常耗时的操作,原生组件最擅长这事
  • Bridge 负责把 React 的绘制指令翻译给原生组件,同时把原生组件收到的用户事件反馈回 React。要在不同平台实现不同效果,定制 Bridge 就行

# 4.1 深入 Bridge

前面反复提到,RN 厉害的地方在于它打通了 JS 和原生代码,让 JS 能调用丰富的原生接口,充分发挥硬件能力,同时保证效率和跨平台性。

打通这条路的关键组件就是 Bridge。

如果没有 Bridge,JS 还是那个 JS,只能调用引擎提供的有限接口。摄像头、指纹、3D 加速、声卡、视频播放定制这些东西,JS 一个都摸不着,原生的、平台相关的、设备相关的效果统统做不了。

Bridge 的作用就是给 RN 内嵌的 JS 引擎扩展出一批原生接口供 JS 调用。本地存储、图片资源访问、图形绘制、网络访问、震动、NFC、原生控件绘制、地图、定位、通知,全都是通过 Bridge 封装成 JS 接口之后注入引擎的。理论上任何原生代码能实现的效果,都可以通过 Bridge 封装成 JS 能调用的组件和方法。

具体的组织方式是这样的。每一个支持 RN 的原生功能,必须同时有一个原生模块和一个 JS 模块,JS 模块是原生模块的封装,方便 JavaScript 调用。Bridge 负责管理这两者之间的沟通。

有一个设计细节很关键,RN 里 JS 和原生的分隔非常干净。JS 不会直接持有原生层的对象实例,原生也不会直接持有 JS 层的对象实例,所有互调都要经过 Bridge 层那几个最基础的方法转接。

那两边的对象是怎么对上号的呢?

答案是编号加映射。JS 和原生两边分别给所有实例编号,维护一张映射表,跨界调用时传的是那个数字或字符串编号,接收方拿编号去表里查找对应的真实对象。JS 和原生之间不存在任何指针传递,所有参数都是序列化成字符串传过去的。

MessageQueue.js 是 Bridge 在 JS 层的代理,所有 JS 调原生和原生调 JS 的请求都要经过它转发。这个「所有跨界调用都必须序列化成字符串再走一遍队列」的设计,是老架构性能瓶颈的根源,也是后来新架构要解决的核心问题,第十二节会展开。

# 五、Bridge 各模块分别在管什么

上一节讲的是 Bridge 的设计思路,这一节拆开看具体由哪些类承担。下面这些是 iOS 侧老架构的类名,Android 侧概念对应但命名不同,第十一节会讲。

# 5.1 RCTRootView

RCTRootView 是 React Native 加载的地方,可以说是万物之源。从这里开始才有了 JS 引擎,JS 代码被加载进来,对应的原生模块也被加载进来,然后 JS 的事件循环开始运行。这个循环的驱动来源是 Timer 和用户事件,一旦跑起来,应用就可以持续不停地运转下去。

fe
  • 一、JavaScriptCore 是这一切的发动机
  • 二、拿浏览器做参照,看清 RN 替换掉了哪一环
  • 三、React Native 的四层架构
  • 四、React、React Native 和 JavaScriptCore 的关系
    • 4.1 深入 Bridge
  • 五、Bridge 各模块分别在管什么
    • 5.1 RCTRootView
    • 5.2 RCTRootContentView
    • 5.3 RCTBridge
    • 5.4 RCTBatchedBridge
    • 5.5 RCTJavaScriptLoader
    • 5.6 RCTContextExecutor
    • 5.7 RCTModuleData
    • 5.8 RCTModuleMethod
    • 5.9 MessageQueue
  • 六、React Native 的初始化过程
    • 6.1 原生代码初始化
    • 6.2 JavaScript 环境初始化
    • 6.3 NativeModules 是怎么加载的
    • 6.4 三个线程各管什么
  • 七、内部机制与调用时序
  • 八、Android 侧的框架结构
    • 8.1 Java 层的核心类
    • 8.2 启动过程速览
  • 九、Java 与 JS 的双向通信
    • 9.1 Java 调用 JS
    • 9.2 JS 调用 Java
  • 十、这套模型现在变成了什么样
  • 总结
  • 参考

← React Native 打包前奏之 iOS 证书与描述文件配置详解React Native启动流程源码拆解从MainActivity到第一帧 →