展开目录
一、大前端发展历史
【秦国覆灭】上世纪90年代,3Com公司的Palm OS成为移动领域(掌上电脑)的霸主,市场占有率达90%
【汉朝建立】20世纪末,微软推出windows CE和Windows Mobile,取代了Palm OS
【东汉末年,黄巾起义】Symbian和Blackberry昙花一现,一度市场占有率达到40%
【三分天下】
- 魏 :2007年,W3C(万维网联盟)立项HTML5,并于2008年1月发布HTML5的第一份正式草案
- 蜀 :2007年1月,苹果推出第一部iphone
- 吴: 2007年11月,google宣布推出Android系统,并于2008年10月发布第一部Android智能手机
【赤壁之战】
- 2010-2012年: FaceBook牵头成立MobileWeb工作组,大举进攻移动领域,Web App的呼声越来越高,一度认为Native App在3年内会消亡
- 2012年: Facebook宣布放弃使用HTML5构造自己的主体应用,WebApp被打入冷宫,HTML5进入最惨淡的一年
- 2014年10月底:HTML5定稿,迎来了 原生+HTML5 的混合模式
【第一代跨平台方案】
- 2015年:FaceBook 推出 ReactNative
- 2016年4月:阿里推出Weex
【第二代跨平台方案】2018年2月:Google发布Flutter第一版
【第三代跨平台方案】2018年6月:Facebook 宣布对 ReactNative 进行大规模重构,预计2021年底重构完成;
二、跨平台框架对比
为什么要进行跨平台的研究?
「微观上」:企业要提高开发效率,降低开发成本,实现一次编码,到处运行;「宏观上」:现在处于多终端发展时代,多种终端生态互不兼容,造成生产力的浪费,势必会有一种新技术代表新进生产力打破目前的枷锁,进一步解放和提高生产力;
新一代的跨平台方案一定比老一代的好吗?
答案是否定的;此处「代」的划分仅仅是从技术角度上,不代表先进与否
2.1、Native App
第三方应用程序会与平台进行交互,以创建 widgets 或访问相机等服务,其中,widgets 呈现给屏幕画布,并且事件被传回给 widgets
2.2、基于WebView的第一代跨平台框架
第一代跨平台框架基于「 JavaScript 和 WebViews」,代表者为:PhoneGap、微信小程序。第三方应用程序创建 HTML 并将其显示在平台上的 WebView 中,对于平台提供的一些系统服务,通过JS Bridge来调用,由于这些调用不是很频繁,JS Bridge并不会成为性能瓶颈。然而,一个完整HTML5页面的展示要经历浏览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加N个数量级,所以这种方案的瓶颈在于WebView对H5页面的渲染。这种开发模式开发的App既有原生应用代码又有Web应用代码,因此又被称为Hybrid App(混合应用程序)
2.3、以React Native为代表的第二代跨平台框架
这种方案也称为「泛Web容器方案」,这种方案放弃了WebView渲染,采用原生自带的UI组件代替了核心的渲染引擎,所以这种方案的性能比第一代方案要好很多,代表者就是RN、Weex。同时这种方案保持了JavaScript作为开发语言,支持前端丰富的生态(比如RN使用React.js极大地方便了UI的创建)。由于前端和Native的交互都要通过中间的Bridge,很自然的Bridge就成了这种方案的性能瓶颈
2.4、以Flutter为代表的第三代跨平台框架
为什么说Flutter是一种新的方案呢?因为他采用了一种「自绘引擎」的方式,和以往的方案都不一样;Flutter既不用WebView进行组件渲染,也不使用原生组件进行渲染,他完全自己搞了一套跨平台UI渲染框架,渲染引擎依靠跨平台的Skia图形库来实现,手机平台只需要提供一块画布即可。同时开发语言使用既支持JIT又支持AOT的Dart语言,既提升了执行效率,也为支持动态化提供可能
三、架构一览
本文的分析基于ReactNative 0.54.3版本!!!!
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 Thread」:根据 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渲染机制的重点就是这三棵树的创建和同步,关键步骤如下:
- 第一步:通过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控件
六、通信机制
在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,实现了两者的相互调用
6.1 、桥的初始化
在RN的启动流程中,会对通信桥进行初始化,通信桥的初始化最关键的就是创建了两张表和建立了两个桥;两张表中,一张是JavaScriptModuleRegistry,供Java调用JS使用, 一张是NativeModuleRegistry,供JS调用Java使用;两个桥,一个是NativeToJSBridge,是Java调用JS的桥梁、一个是JsToNativeBridge,JS 调用Java的桥梁
6.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及方法并执行
6.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
七、性能瓶颈
基于此架构,中间层Bridge必然会成为RN的性能瓶颈,RN中存在如下问题:
1、通信效率低下,容易出现堵塞
JS层和Native层只能通过桥来进行通信,多次线程切换、串行消息处理、参数通过JSON序列化和反序列化传递,导致效率低下,容易出现堵塞
2、异步调用导致不能同步响应,用户体验不佳
受限于通信机制,RN里JS和Native侧相互之间只能异步调用,用户的操作和App的响应是异步的,且之间可能会有不小的延迟,用户的交互体验不佳
八、新架构未来
也许是感受到了Flutter的压力和挑战,FaceBook在Flutter发布的几个月后就宣布了重构计划,如果RN能解决性能瓶颈,凭借完备的生态或许可以打败Flutter。
Facebook 在 2018 年 6 月 14 日的 BLOG 中透露,Facebook 正在对 React Native 进行大规模重构,以提升性能,改善开发体验,并在之后的开发者大会上,宣布了大规模重构 React Native 的计划及重构路线图,RN 新老架构对比如下:
8.1、旧框架
- 业务启动一次性初始化全部 NativeModule
- 所有的调用为异步操作(同步桥除外)
- JS 和 原生端通过 JSON 进行通信
- 桥通讯由于排队、线程切换易引起阻
8.2、旧框架
- JSI:增加引擎抽象层,实现引擎解耦便于切换引擎,同时支持 JS 持有 C++ HostObject 类型对象引用,实现 JS 和 Native 相互感知。
- TurboModule:重构后的 NativeModules,用于向前端暴露 Native 能力,实现 NativeModule 按需加载 和 JS与 Native 同步调用
- Fabric:新 UI 架构,替换原有 UIManager
- Code Gen:工具,用于 Fabric 和 TurboModule 的 JSI 框架代码,同时加入 JS 静态代码检查功能