本文为原创内容,谢绝转载
引入
React Native作为2015年诞生的跨平台解决方案,旨在降低移动端的开发门槛的同时依旧能保有移动端高效的渲染能力,React Native产出的并不是“web应用”,“H5”,又或者是“混合应用”。他的卖点是一个实实在在由原生组件去渲染的真正移动化应用,从使用感受上和用Objective-C或Java编写的应用相比几乎是无法区分的。 React Native所使用的基础UI组件和原生应用完全一致。 开发者只是把这些基础组件使用JavaScript和React的方式组合起来,那么这种由非原生语言编写的视图组件在借由原生能力绘制渲染时都经历了哪些操作和转换?
正文
首先这里我们将本节要讲的整个过程分为两个阶段,来拆解描述。
准备阶段由原生来承担大部分工作,包括启动JS运行环境,创建空白画布,实例化自定义module及导出各种可供JS使用的API等。
而渲染阶段的主要职责包括根据注册信息创建实际视图、维护虚拟dom树、维护待渲染队列及对布局样式进行转换等。
接下来我会围绕这两个阶段做一个详细的描述。
准备阶段
在准备阶段,首先你需要指定并创建RN的入口视图并将其添加到原生的视图容器中,这个视图可以和任意纯原生视图一样去控制其入口视图大小和位置, 也就是说支持对一个页面只做局部RN化渲染;
然后,你需要创建和JS端的桥接对象并且设置加载路径,在设置完成后交由我们刚才创建的根视图去使用这个配置好的桥接对象。
而其中React Native的启动又会包含以下几个步骤:
对照图片,你可以看到在启动环节,iOS和Android两端存在一定差异, 但主流程基本是一致的。首先通过入口视图调用runApplation函数启动JS环境, JSC启动后由桥接对象负责在子线程实例化所有的module,所谓的Module,主要是指RN中可被JS端直接使用的自定义视图或代码块,这里我们主要说视图Module,也就是ViewModules, module实例化完成后由JS端的render函数主动发起渲染并且加载到原生视图上。
对于iOS来讲在JavaScript Core启动完成后会创建用于真实渲染的父视图画布,所有由JS端绘制的视图都会被添加在这个画布上而不是直接绘制在用户手动创建的入口视图上,但是对于Android而言,用户手动创建的入口视图则直接承担绘制RN视图的角色,iOS和Android两端在画布创建的同时都会生成一个RootTag用来做RootView的标识,JS端在发起渲染请求时也会携带这个RootTag,用来标识想要被添加到的RootView,
而在子线程,当runApplication执行完成后, 桥接对象会遍历所有标记导出的module模块并且进行实例化创建,完成后会把完整的注册列表,分配到到关键类RCTUIManager中。需要注意的是,对于iOS而言这些module在运行初期就会被全量被加载到环境中,当同时存在多个桥接对象时并不能做到按需加载。
在导出module的同时,桥接对象也会导出所有支持的function和style列表, 比如createView, setChildren或者borderColor等CSS样式。
那么到此阶段为止我们有了空白的画布, 所有支持调用的视图列表、方法列表和样式列表, 准备阶段就已经完成了,接下来就是渲染阶段了。
渲染阶段
在开始正式进入渲染阶段之前,我们先了解一下与RN渲染相关的概念
首先来看图片左侧的实际视图相关类,所有的对象都继承于原生的View,这些RN视图经过桥接对象的实例化和UIManager的分发创建的,最终都将以原生View的方式显示在视图上。
而对于图片左侧虚拟dom来说,它的基类就是我们的抽象类NSObject,只负责逻辑处理。
实际视图和虚拟dom是一一对应关系,我们在准备阶段创建的父视图RCTRootContentView会对应RCTRootShadowView节点, 其他viewModules则对应RCTShadowView节点,各类shadowView的职责都是通过 facebook的Yoga跨平台布局框架在子线程进行布局相关的计算,并且更新实际视图。
这些实际视图和虚拟dom最终都由我们的核心类RCTUIManager负责维护。
好了, 在了解到这些之后我们再来看一下RN的整体渲染流程
渲染流程大致流程分为这几个核心节点, 首先原生触发runApplication后会直接对初始化时设置的入口文件进行render, render经过diff比对后调用createInstance, 随后进入原生的createView方法去创建虚拟dom树和实体视图并且将其放入待渲染列表进行维护,同时shadowView也会根据JS端设置的样式做具体的样式转换,转换完成后会主动发起请求去更新实际视图。
最后js端的createInstance函数会在合适的时机调用setChilder通知原生将实体视图添加到界面上进行渲染。
在此过程中,iOS和Android两端的核心流程基本一致,这里我们就先拿iOS举例,一起来看看createView方法的具体实现。
可以看到这个方法接收四个参数,
ReactTag 表示想要创建的view的tag标识,
viewName对应启动时导出的ViewModules列表中的class名称
rootTag则是这个子节点对应的根视图tag
props则包含视图相关的布局信息。
除了方法的入参之外, 我们来讲解一下方法的具体实现:
首先在方法中会根据viewName去viewModules列表中获取相关信息和对应的实体类,并且创建对应的shadowView,再根据rootTag来将子dom节点和root节点做关联,生成视图树。
然后会去创建实体视图类,但要注意的是这里并没有把视图进行实际渲染,只是放入渲染队列进行维护等待JS端调用渲染。
最后会把视图相关的props传给虚拟dom去做转换处理,转换完成后由虚拟dom通知视图进行样式更新。
在这个过程中需要进行的样式转换也分为两部分:布局转换和CSS样式转换, 布局转换交由yoga框架完成, CSS样式转化由RN自己完成,在实际转换时会交由子类dom去做具体的个性化处理,也就是说如果我们有自定义布局或拓展布局语法的需求可以通过继承shadowView这种方式去完成。
简言之在调用createView后原生端会去创建真实渲染的view和其对应的虚拟dom,并且维护dom树,完成view样式的转换,最后维护渲染队列等待渲染。
同样的, 实际渲染请求也一样是由JS发起的,在上一步render调用栈的completeWork函数中会调用setChildren方法,去通知原生批量渲染。setChildren同样有两个参数containerTag和reactTags,表示根视图和子视图。原生会根据之前维护的虚拟dom和待渲染队列去对这些视图进行批量添加渲染。到此,一个JavaScript XML描述的组件得以用原生视图的方式展现在界面上。
总结
好了,今天的内容就分享到这,最后我们再用一个时序图来总结一下整个渲染流程
首先原生端启动APP,激活JS环境。
然后,主线程创建RN根视图,桥接对象在子线程实例化并导出所有支持JS使用的viewModuels以及props、method等。
环境启动完成后,JS端由入口文件发起render触发渲染,经由一系列diff判断及处理,最终在ReactNativeRenderer中调用原生UIManager的createView函数创建实际视图和虚拟dom,并且维护渲染队列以及dom树。
最后,Renderer内部触发去主动调用setChildren,通知原生将待渲染队列中的对象在主线程批量添加到视图层进行渲染。
好了,现在我们知道在创建根视图后会进入JS的代码渲染逻辑,并且两端通过JSC在JS线程中频繁通信用来更新dom树,最后切换到主线程进行批量渲染。那么对于页面视图数量多、渲染层级深、渲染次数多、线程切换频繁的重量级页面,可能带来的掉帧卡顿风险我们应该如何进行规避呢?与原生类似,我们在RN中也可以采用常见的优化手段——预加载来规避这些问题。具体怎么做呢?近期会对React Native预加载方案做具体分享。