讲React Native之前,了解JavaScriptCore会有帮助,也是必要的。React Native的核心驱动力就来自于JS Engine. 你写的所有JS和JSX代码都会被JS Engine来执行, 没有JS Engine的参与,你是无法享受ReactJS给原生应用开发带来的便利的。在iOS上,默认的就是JavaScriptCore, iOS 7之后的设备都支持. iOS 不允许用自己的JS Engine. JavaScriptCore来自于WebKit, 所以,安卓上默认也是用JavaScriptCore
你深入了解
React Native
的第一站应该是JavaScriptCore
JavaScriptCore
在iOS
平台上给React Native
提供的接口也仅限于那几个接口,你弄明白了JavaScriptCore
那几个接口, React Native 剩下的魔法秘密都可以顺藤摸瓜来分析了。Dom Render
来渲染所有的元素.UI的描述和呈现分离开了
在react native 里面,1和2是不变的,也是用html语言描述页面有哪些功能,然后stylesheet告诉浏览器引擎每个控件应该长什么样。并且和浏览器用的是同一个引擎
在步骤3里面UI控件不再是浏览器内置的控件,而是
react native
自己实现的一套UI控件(两套,android一套,ios一套),这个切换是在MessageQueque
中进行的,并且还可以发现,他们tag也是不一样的
Javascript在react native里面非常重要
React Native最重要的三个概念应该就是
React Native
、React
和JavascriptCore
React Native
呢? 它比较复杂。复杂在哪里?前面我们说了React 是纯JS库,意味着React只能运行JS代码,通过JS Engine提供的接口(Html Tag)绘制html支持的那些元素,驱动有限的声卡显卡。简单点说, React只能做浏览器允许它做的事情, 不能调用原生接口, 很多的事情也只能干瞪眼React Native它可不一样
React Native组件结构
驱动硬件的能力决定能一个软件能做多大的事情,有多大的主控性。研究过操作系统底层东西或者汇编的同学明白,我们大部分时候写的代码是受限的代码,很多特权指令我们是没法使用的,很多设备我们是不允许直接驱动的。我们现在的编程里面几乎已经没有人提中断了,没有中断,硬件的操作几乎会成为一场灾难.
在一定程度上,React Native和NodeJS有异曲同工之妙。它们都是通过扩展JavaScript Engine, 使它具备强大的本地资源和原生接口调用能力,然后结合JavaScript丰富的库和社区和及其稳定的跨平台能力,把javascript的魔力在浏览器之外的地方充分发挥出来
JavaScriptCore + ReactJS + Bridges 就成了React Native
JavaScriptCore
负责JS代码解释执行ReactJS
负责描述和管理VirtualDom
,指挥原生组件进行绘制和更新,同时很多计算逻辑也在js里面进行。ReactJS自身是不直接绘制UI的,UI绘制是非常耗时的操作,原生组件最擅长这事情。Bridges
用来翻译ReactJS的绘制指令给原生组件进行绘制,同时把原生组件接收到的用户事件反馈给ReactJS
。Bridges
来实现深入
Bridge
前面有提到, RN厉害在于它能打通JS和Native Code, 让JS能够调用丰富的原生接口,充分发挥硬件的能力, 实现非常复杂的效果,同时能保证效率和跨平台性。
打通RN任督二脉的关键组件就是
Bridge
. 在RN中如果没有Bridge, JS还是那个JS,只能调用JS Engine提供的有限接口,绘制标准html提供的那些效果,那些摄像头,指纹,3D加速,声卡, 视频播放定制等等,JS都只能流流口水,原生的、平台相关的、设备相关的效果做不了, 除非对浏览器进行定制
Bridge
原生代码负责管理原生模块并生成对应的JS模块信息供JS代码调用。每个功能JS层的封装主要是针对ReactJS做适配,让原生模块的功能能够更加容易被用ReactJS调用。MessageQueue.js
是Bridge
在JS层的代理,所有JS2N和N2JS的调用都会经过MessageQueue.js
来转发。JS和Native之间不存在任何指针传递,所有参数都是字符串传递。所有的instance都会被在JS和Native两边分别编号,然后做一个映射,然后那个数字/字符串编号会做为一个查找依据来定位跨界对象。RCTRootView
是React Native
加载的地方,是万物之源。从这里开始,我们有了JS Engine, JS代码被加载进来,对应的原生模块也被加载进来,然后js loop开始运行。 js loop的驱动来源是Timer和Event Loop(用户事件). js loop跑起来以后应用就可以持续不停地跑下去了。AppDelegate.m
的- (BOOL)application:didFinishLaunchingWithOptions:里面都可以看到RCTRootView的初始化代码,RCTRootView初始化完成以后,整个React Native运行环境就已经初始化好了,JS代码也加载完毕,所有React的绘制都会有这个RCTRootView来管理。RCTRootView做的事情如下
RCTBridge
JS Bundle
并且初始化JS运行环境.loadingView
, 注意不是屏幕顶部的那个下拉悬浮进度提示条. RN第一次加载之后每次启动非常快,很少能意识到这个加载过程了。loadingView默认情况下为空, 也就是默认是没有效果的。loadingView可以被自定义,直接覆盖RCTRootView.loadingView就可以了.开发模式下RN app第一次启动因为需要完整打包整个js所以可以很明显看到加载的过程,加载第一次以后就看不到很明显的加载过程了,可以执行下面的命令来触发重新打包整个js来观察loadingView
的效果 watchman watch-del-all && rm -rf node_modules/ && yarn install && yarn start – –reset-cache
, 然后杀掉app
重启你就会看到一个很明显的进度提示.JS
运行环境准备好以后把加载视图用RCTRootContentView
替换加载视图AppRegistry.runApplication
正式启动RN JS代码,从Root Component()
开始UI绘制一个App可以有多个
RCTRootView
, 初始化的时候需要手动传输Bridge
做为参数,全局可以有多个RCTRootView
, 但是只能有一个Bridge
如果你做过
React Native
和原生代码混编,你会发现混编就是把AppDelegate
里面那段初始化RCTRootView
的代码移动到需要混编的地方,然后把RCTRootView
做为一个普通的subview
来加载到原生的view
里面去,非常简单。不过这地方也要注意处理好单Bridge实例的问题,同时,混编里面要注意RCTRootView
如果销毁过早可能会引发JS回调奔溃的问题
RCTRootContentView reactTag
在默认情况下为1. 在Xcode view Hierarchy debugger
下可以看到,最顶层为RCTRootView
, 里面嵌套的是RCTRootContentView
, 从RCTRootContentView
开始,每个View都有一个reactTag
RCTRootView
继承自UIView, RCTRootView主要负责初始化JS Environment
和React代码,然后管理整个运行环境的生命周期。 RCTRootContentView
继承自RCTView
, RCTView
继承自UIView, RCTView封装了React Component Node更新和渲染的逻辑, RCTRootContentView
会管理所有react ui components. RCTRootContentView
同时负责处理所有touch事件这是一个加载和初始化专用类,用于前期JS的初始化和原生代码的加载
RCTBridgeModule protocol
的类, 供JS后期使用.RCTBatchedBridge
如果RCTBridge是总裁, 那么RCTBatchedBridge就是副总裁。前者负责发号施令,后者负责实施落地
JSExecutor
native node_modules
native hooks
和modules
, 执行 JS bundle scriptnative invocations
JS executor
这是实现远程代码加载的核心。热更新,开发环境代码加载,静态
jsbundle
加载都离不开这个工具。
bundle
, http server
)加载 script bundle
string
的形式返回记录所有原生代码的导出函数地址(JS里面是不能直接持有原生对象的),同时生成对应的字符串映射到该函数地址。JS调用原生函数的时候会通过message的形式调用过来
J2N call
,然后执行对应的native方法
- 如果是原生方法的调用则直接通过方法名调用,MessageQueue会帮忙把Method翻译成MethodID, 然后转发消息给原生代码,传递函数签名和参数给原生MessageQueue, 最终给RCTModuleMethod解析调用最终的方法
- 如果JS调用的是一个回调block,MessageQueue会把回调对象转化成一个一次性的block id, 然后传递给RCTModuleMethod, 最终由RCTModuleMethod解析调用。基本上和方法调用一样,只不过生命周期会不一样,block是动态生成的,要及时销毁,要不然会导致内存泄漏
实际上是不存在原生MessageQueue对象模块的,JS的MessageQueue对应到原生层就是RCTModuleData & RCTModuleMethod的组合, MessageQueue的到原生层的调用先经过RCTModuleData和RCTModuleMethod翻译成原生代码调用,然后执行
React Native
的初始化从RootView
开始,默认在AppDelegate.m:- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
里面会有RootViewd
的初始化逻辑,调试的时候可以从这里入手
React Native的初始化分为几个步骤
JS Engine
初始化(生成一个空的JS引擎)require
, Warning window
, Alert window
, fetch
等都是在这里进行的。基础设施初始化好以后就可以开始加载js代码了这里需要提一下的是
这里的导出是没有对象的,只有方法和模块。JS不是一个标准的面向对象语言,刚从Java转JavaScript的同学都会在面向对象这个概念上栽跟头,这里特别提醒一下
这里讨论的主要是RN相关的原生代码和用户自定义的RN模块的原生代码的加载和初始化。原生代码初始化主要分两步
JS Engine不直接管理UI的绘制
Bridge
派发给MessageQueue,然后在JS层进行业务逻辑的计算,再由React
来进行Virtual Dom的管理和更新。Virtual Dom
再通过MessageQueue发送重绘指令给对应的原生组件进行UI更新
后只是告诉Native Modules这有一个原生模块,是一个空的模块。要导出任何方法给JS使用都必须手动用宏RCT_EXPORT_METHOD来导出方法给JS用.NativeModules
这一个JS模块下面去,你如果想要让自己的模块成为一个顶级模块就必须再写一个JS文件封装一遍NativeModules里面的方法。constantsToExport
方法, 接受一个常量词典 |
Native Modules
导出宏具体使用方法见官方文档Native Modules
React Native有三个重要的线程:
Javascript thread. javascript
线程。 大家都知道javascript是单线程模型,event驱动的异步模型。React Native用了JS引擎,所以也必需有一个独立的js 线程. 所有JS和原生代码的交互都发生在这个线程里。死锁,异常也最容易发生在这个线程可以看到Shadow queue是queue而不是thread, 在iOS里面queue是thread之上的一层抽象,GCD里面的一个概念,创建queue的时候可以指定是并行的还是串行的。也就是说,一个queue可能对应多个thread
内部机制
JS用时序
UIManager
(将JavaScript映射成Android Widget
)以及一些其他的功能组件(例如:Fresco、Okhttp)等,在java层均封装为Module,java层核心jar包是react-native.jar,封装了众多上层的interface,如Module,Registry,bridge等JavaScriptCore
,Web
开发者可以尽情使用ES6的新特性,如class、箭头操作符等,而且 React Native运行在JavaScriptCore
中的,完全不存在浏览器兼容的情况。Bridge桥接了java , js 通信的核心接口。JSLoader主要是将来自assets目录的或本地file加载javascriptCore,再通过JSCExectutor
解析js文件Component
:Js层通js/jsx编写的Virtual Dom
来构建Component
或Module,Virtual DOM是DOM在内存中的一种轻量级表达方式,可以通过不同的渲染引擎生成不同平台下的UI。component的使用在 React 里极为重要, 因为component的存在让计算 DOM diff 更高效。注:JSCore,即JavaScriptCore,JS解析的核心部分,IOS使用的是内置的
JavaScriptCore
,Androis上使用的是 https://webkit.org 家的jsc.so。
Java层核心类及原理,如下所示
ReactContext
ReactInstanceManager
ReactInstanceManager
是ReactNative应用总的管理类,创建ReactContext
、CatalystInstance
等类,解析ReactPackage
生成映射表,并且配合ReactRootView
管理View的创建与生命周期等功能。ReactRootView
root view
。CatalystInstance
CatalystInstance
是ReactNative
应用Java层、C++层、JS层通信总管理类,总管Java层、JS层核心Module
映射表与回调,三端通信的入口与桥梁。JavaScriptModule
JavaScriptModule
是JS Module
,负责JS到Java的映射调用格式声明,由CatalystInstance
统一管理。NativeModule
NativeModule
是java Module
,负责Java到Js的映射调用格式声明,由CatalystInstance
统一管理。JavascriptModuleRegistry
NativeModuleRegistry
CoreModulePackage
NativeModules&JsModules
Java与Js之间的调用,是以两边存在两边存在同一份模块配置表,最终均是将调用转化为{moduleID,methodID,callbackID,args},处理端在模块配置表里查找注册的模块与方法并调用。
Java 调用Js
Java通过注册表调用到CatalystInstance实例,透过ReactBridge的jni,调用到Onload.cpp中的callFunction,最后通过javascriptCore,调用BatchedBridge.js,根据参数{moduleID,methodID}require相应Js模块执行。流程如下图:
Js 调用Java
如果消息队列中有等待Java 处理的逻辑,而且 Java 超过 5ms 都没有来取走,那么 JavaScript 就会主动调用 Java 的方法,在需要调用调Java模块方法时,会把参数{moduleID,methodID}等数据存在MessageQueue中,等待Java的事件触发,把MessageQueue中的{moduleID,methodID}返回给Java,再根据模块注册表找到相应模块处理。流程如下图: