- 2015年:FaceBook 推出 ReactNative
- 2016年4月:阿里推出Weex
- 2018年2月:Google发布Flutter第一版
一、介绍 和 原理
转载原文: 「ReactNative」原理剖析
架构
- JS代码层:提供了React.js支持,React.js的JSX代码转化为JS代码运行在JavaScriptCore提供的 JavaScript 运行时环境中
- JS引擎层。
- 通信层:将 JavaScript 与 Native 层连接起来;通信层又可以分为三部分,其中Shadow Tree 用来定义 UI 效果及交互功能、Native Modules 提供 Native 功能(比如相册、蓝牙等)、而他们之间的相互通信 使用的是JSON 异步消息
- 原生层。
三个线程
- 「JS Thread」: 主要负责 React、JS的执行,输出 App 的视图信息(结构、样式、属性等)
- 「Shadow Thread」: 根据 JS 线程的视图信息,创建出用于布局计算的 ShadowTree;(主要用到UIManagerModule,是RN中非常重要的Native Module,故也叫Native Module Thread)
- 「Main Thread」: 根据 ShadowTree 提供的完整视图信息,负责真实 Native View 的创建
1、启动流程
总结起来,启动流程主要做了两件事情:一件是准备环境,一件是调用JS侧的入口函数。
准备环境:主要做了这些事,在后台创建上下文、初始化通信桥、加载JSBundle、初始化JS执行环境等。
调用JS侧的入口函数:即调用AppRegistry.js的runApplication方法,为一次Native到JS的调用。
2、渲染原理
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渲染机制的重点就是这三棵树的创建和同步,关键步骤如下:
- 第一步: 通过React.js的JSX定义UI结构
- 第二步: 编译阶段,通过 Babel 将 JSX 转化为 React.createElement的形态
- 第三步: 在JS侧,通过深度优先遍历将JSX编写的UI组件转化为Fiber Tree结构,每个组件节点都包含子组件、父组件和兄弟组件的引用
- 第四步: JS侧在创建Fiber Tree各个节点的时候会通过Bridge桥向Native侧发送对应的指令,Native侧收到这些指令之后会创建对应的Shadow Tree节点,同时会生成对应的UIViewOperation,加入到UIViewOperationQueue中,以供在UI线程进行真正的UI操作。JS侧发送完一批UI指令之后会触发Native侧的onBatchComplete回调,进而后序遍历ShadowTree,分别计算每个节点的宽度和高度,然后前序遍历ShadowTree,确定每个节点的最终位置,生成相应的UpdateLayoutOperation,加入到UIViewOperationQueue中
- 第五步: 触发FrameCallback,从UIViewOperationQueue中依次取出UIViewOperation,生成对应的View Tree,挂载到RootView,进行原生UI渲染逻辑
虚节点和LayoutOnly节点区别?
- 虚节点在计算布局时会被忽略,也不会生成相应的Native控件
- LayoutOnly节点指一个节点只会影响到它的子节点的位置,而本身不需要绘制任何内容,那么这个节点就是LayoutOnly节点, 不会生成相应的Native控件
3、通信机制
在RN中有三个线程:JS线程、UI线程、Shadow线程(Native Modules线程),而在Native Modules线程中,主要用来进行Yoga布局计算,同时也负责C++层和原生通信。我们知道Java可以通过JNI的方式和C++代码实现相互调用,JS 可以通过 JavaScriptCore 实现和C++的相互调用,而JavaScriptCore是由C++实现的JS引擎,所以很自然的,C++成为了连接Java和JS的桥梁
所以RN的通信机制总结起来就是一句话:一个C++实现的桥打通了Java和JS,实现了两者的相互调用
3.1 、桥的初始化
在RN的启动流程中,会对通信桥进行初始化,通信桥的初始化最关键的就是创建了两张表和建立了两个桥;两张表中,一张是JavaScriptModuleRegistry,供Java调用JS使用, 一张是NativeModuleRegistry,供JS调用Java使用;两个桥,一个是NativeToJSBridge,是Java调用JS的桥梁、一个是JsToNativeBridge,JS 调用Java的桥梁
3.2 、Native调用JS
Native调用JS的流程相对简单,如下:
- 在Java层把要实现的功能编写成接口并继承JavaScriptModule,并交由ReactPackage管理,最终会在RN初始化的时候添加到JavaScriptModuleRegistry注册表中
- JavaScriptModuleRegistry通过动态代理生成对应的JavaScriptModule,然后通过invoke()调用相应的JS方法,该方法会进一步去调用CatalystInstanceImpl.callJSFunction() ,该方法会通过JNI将相关参数传递到C++层
- C++层通过NativeToJsBridge将callFunction的消息放入消息队列等待执行;C++层中保有MessageQueue.js中的一些属性对象,通过这些属性对象进入JS层
- 在JS层里,找到对应的JavaScriptModule及方法并执行
3.3、 JS调用Native
在JSToNative的通信方式中,又分为两种调用方式:异步调用和同步调用
「异步调用」: 指的是在JSToNative的通信方式中,调用的发起在JS线程,逻辑处理和计算在Native Module线程和UI线程,异步的方式不会阻塞JS线程
「同步调用」: 指的是调用和处理过程都发生在JS线程中;如果逻辑计算简单,这没什么影响。如果逻辑计算复杂,那肯定得卡死JS线程。所以在RN中,它的应用较少,且官方在注释中也标明要慎用
所以我们选择带Callback的异步调用来做具体的分析,调用流程如下:
整个流程可以分为两个部分,第一个部分是JS调用Native、第二个部分是Native将执行结果回调至JS侧(和Native调用JS的流程很相似)
JS调用Native流程如下:
- 从JS侧进入C++层,通过JSC桥接获取Java Module的注册表,然后回到JS侧,把它转换为对应的JS Native Module(属性、方法转换),并根据不同的调用类型,将xxMethod()的调用封装成消息,放入了MessageQueue的队列中
- xxMethod()消息处理的时候,会进入C++层,拿到对应的module信息,通过JSToNativeBridge,将该函数调用消息放入到线程的消息队列中等到执行。此时C++层的函数调用被映射为同名的Java层JavaModuleWrapper对象,并调用其中的invoke方法,传入的参数是methodId和对应的参数信息
- Java层的JavaModuleWrapper对象,根据参数信息,找到对应的JavaMethodWrapper对象。在执行其invoke方法中,通过反射调用对应的NativeModule,从而完成JS到Native的调用
- JS到Native的调用的时候,会将callback和参数放在一起传入到Java层,在Java层执行Native Module的时候会解析出callback参数。等执行完成,将结果通过callback返回给JS 侧,这一过程就是一个Native调用JS的过程,只不过调用的方法变成了invokeCallback
4、性能瓶颈 和 新架构未来
基于此架构,中间层Bridge必然会成为RN的性能瓶颈,RN中存在如下问题:
1、通信效率低下,容易出现堵塞
JS层和Native层只能通过桥来进行通信,多次线程切换、串行消息处理、参数通过JSON序列化和反序列化传递,导致效率低下,容易出现堵塞
2、异步调用导致不能同步响应,用户体验不佳
受限于通信机制,RN里JS和Native侧相互之间只能异步调用,用户的操作和App的响应是异步的,且之间可能会有不小的延迟,用户的交互体验不佳
4.1 新架构未来
也许是感受到了Flutter的压力和挑战,FaceBook在Flutter发布的几个月后就宣布了重构计划,如果RN能解决性能瓶颈,凭借完备的生态或许可以打败Flutter。
Facebook 在 2018 年 6 月 14 日的 BLOG 中透露,Facebook 正在对 React Native 进行大规模重构,以提升性能,改善开发体验,并在之后的开发者大会上,宣布了大规模重构 React Native 的计划及重构路线图,RN 新老架构对比如下:
旧框架
- 业务启动一次性初始化全部 NativeModule
- 所有的调用为异步操作(同步桥除外)
- JS 和 原生端通过 JSON 进行通信
- 桥通讯由于排队、线程切换易引起阻
新框架
- JSI:增加引擎抽象层,实现引擎解耦便于切换引擎,同时支持 JS 持有 C++ HostObject 类型对象引用,实现 JS 和 Native 相互感知。
- TurboModule:重构后的 NativeModules,用于向前端暴露 Native 能力,实现 NativeModule 按需加载 和 JS与 Native 同步调用
- Fabric:新 UI 架构,替换原有 UIManager
- Code Gen:工具,用于 Fabric 和 TurboModule 的 JSI 框架代码,同时加入 JS 静态代码检查功能
二、rn相关
1、RN reload原理解析
handleReloadJs
native逻辑:reload -> http请求 -> bundle cache -> 创建JSBundleloader -> 触发RN重新加载。
js端: npm start -> node_modules/react-native/local-cli/cli.js start -> @react-native-community/cli -> @react-native-community/cli-plugin-metro -> runServer -> metro.runServer
- 启动metro服务,这时server就通过node把http服务搭建好了;
- APP通过reload发起重新打包请求;
- metro服务接收请求,重新计算依赖并打包返回;
- APP接收到新的bundle包并保存到本地,RN加载bundle并重新构建。
client为app,server是metro运行的http服务,双方通信使用http协议。
2、热更新
RN会将一系列资源打包成js bundle文件,系统加载js bundle文件,解析并渲染。所以,RN热更新的根本原理就是更换js bundle文件,并重新计算加载ReactRootView/RCTRootView
- android activity动态 LinearLayout.addView(ReactRootView)
- ios viewControl动态 view.addSubview(RCTRootView)
方案
- 使用微软官方开源的 react-native-code-push
拆包 metro
- react-native-multibundler github地址
推荐其他:
- 再谈移动端跨平台框架 Flutter 与 React Native
- 2022 年 React Native 的全新架构更新
- RN reload原理解析
- react-native-multibundler
欢迎关注我的前端自检清单,我和你一起成长