Facebook在2018年6月官方宣布了大规模重构React Native的计划及重构路线图。目的是为了让React Native更加轻量化、更适应混合开发,接近甚至达到原生的体验。(也有可能是React Native团队感受到了Google Flutter的追赶压力,必须从架构上做出重大革新,未来才有可能和Flutter进行全面的竞争)。
从Facebook公布的官方信息来看,这是一次革命性的架构重构,主要的重构内容如下:
1.改变线程模式。UI 更新不再同时需要在三个不同的线程上触发执行,而是可以在任意线程上同步调用 JavaScript 进行优先更新,同时将低优先级工作推出主线程,以便保持对 UI 的响应。
2.引入异步渲染能力。允许多个渲染并简化异步数据处理。
3.简化JSBridge,让它更快、更轻量。
目前React Native有哪些问题?
目前业内React Native框架已经有了广泛的应用。京东在这个方面起步比较早,相对来说整体解决方案也比较成熟。目前京东深度定制和扩展的JDReact解决方案已经累计接入了200+个RN业务和20+的独立APP,并且承担了千万级的DAU。从业务实际开发中还是遇到了不少坑,其中性能问题比较明显,具体有以下几类问题:
-
加载性能偏慢,因为系统或者自定义的原生UI组件和API的注册加载过程中需要验证所有属性和JS API的一致性,影响加载性能,甚至直接导致主UI线程很容易阻塞。
-
JSBridge,React Native整体生命周期和JSBridge绑定太紧,所有的原生和JS之间操作全部是通过这个Bridge,而且每次的事件通讯是有时间间隔的,导致整体渲染过程是异步的。
-
手势问题,React Native目前的架构,从JS侧很难解决很多复杂的手势问题,需要重新定制SDK来解决问题。
-
返回事件的处理,目前的返回事件不能像原生一样,在组件中监听。
-
Layout的计算,整体的UI计算必须要在shadow layout中完成,没有办法在整体的平台框架中计算。
现有的Native \u0026amp; JS Component组件如下,通过这些组件可以完成原生UI渲染和API调用。这些组件都是通过packageManger注册到系统的,当RN业务启动后,需要对整体的属性和方法做一些校验,存在性能损失;另外RN是允许多个packagemanger同时注册的,当API数量偏大时,导致的问题需要循环遍历,调用过程也存在性能损耗。
1.Native Modules ,原生端API接口。
2.ViewManager,原生UI组件。
3.Native Navigation,原生导航组件。
4.ComponentKit \u0026amp; Litho,原生端基于yoga UI组件。
5.RCTSurface,原生端Surface实现。
加载过程中首先需要加载初始化React Native Core Bridge,包含以上的一些组件功能,然后才能运行业务的JS Code,只有这步完成后,React Native才能将JS的组件渲染成原生的组件:
所以目前的架构下这些组件和API太过依赖JSBridge的初始化,而且通讯能力也局限于这一条通道。从渲染的层次来看,React Native是多线程运行的,最常见的是JS线程和原生端的线程,一旦线程间异常,JSBridge整体将会阻塞,我们经常也能看到JS运行异常了,实际JS线程已经无响应了,但原生端还能响应滚动事件。
如何彻底解决这些问题?
针对先有框架的一些问题Facebook在最近的版本中尝试过很多优化工作,从2013年发布到目前已经更新到了V0.58,去年一年发布了10多个版本。从版本更新可以看出,除了一些组件的更新和BUG修复外,Facebook做了性能优化方面的尝试,让其在加载和渲染性能上尽可能的达到原生。
重大性能优化的版本:
-
0.33 Lazy module
-
0.40 RAM bundle/unbundle
-
0.43 FlatList/SectionList/VirtualizedList
-
0.50 SwipeableFlatList/Fiber
以下是目前官方建议的一些优化性能的方案:
1.组件的懒加载注册,原生端可以采用懒注册,在业务使用到该组件时注册。
2.按需打包,直接减少业务包大小,去掉一些不需要的module,提高渲染速度。
3.业务的懒加载,直接减少业务渲染过程中require各个组件的时间。
4.UNBundle,将业务分解成小的模块,提供性能。
5.移走初始化过程中不必要的JS module模块。
6.提供prepack工具优化JS代码。
最新的架构又提出了Fibe/Relay Modern架构,整体渲染性能相比以前有了很大的提高,最新的JDReact SDK已经升级到这个架构,目标是将加载JSBridge的开销降到最低,但是文章前面提到的瓶颈问题还是没有突破。
我们和跨端平台框架Flutter启动和渲染做了对比,在启动性能上React Native稍微优于Flutter,但渲染方面明显不如Flutter,也就是我们说的瓶颈问题,对比如下图:
所以,我们的结论是,在现有架构下的各种优化都很难彻底解决性能问题。
唯有架构重构才是王道
在最近的开发者大会中,Facebook对下一代架构重构的进展进行了介绍,我们也对master分支上提交的部分源码进行了分析,可以了解新架构的一些雏型设计,整体架构还在不断优化中,相信还会有更多惊喜。从现有的信息和代码来看,JS层业务的影响较小,不会因此次大规模架构重构后需要大量适配业务代码。这次的重构主要是JSBridge及原生架构的重构,下面我们从几个层面对比介绍整体框架:
现有架构渲染原理
UI的渲染过程分为三层:JS业务层、shadow tree、原生UI层。其中JS和shadow tree是通过JSBridge来同步数据的,JS层会将所有UI node生成一串JSON数据,传递到原生shadow层,原生shadow层通过传入node数据,新增新UI或者删除一些不需要的UI组件,这就完成了下图这三个层次之间的驱动关系:
带来的问题是整体UI渲染异步且太过于依赖JSBrige,很容易出现阻塞而影响整体UI体验,从JDReact的业务开发经验来看,比如初始化过程中UI复杂度过高,点击UI时响应时间会很长,就是因为UI被阻塞了很难响应touch事件,另外UI大小计算JS framework没有办法直接计算,需要依赖原生计算完成后的回调。
再看看SrollView的例子,这是业务或者社区反馈性能和体验问题最大的组件。最初版本的ScollView是一次渲染的不作任何回收,所以启动性能慢且内存占用较大。后续版本Flatlist作了组件的回收,内存基本稳定了,但是快速滑动过程中出现了体验问题,容易白屏且容易卡顿。大家看下面的流程图就能明白为什么Flatlist(基于ScollView实现)/ScrollView 快速滑动下会有长时间的白屏或者卡顿。
在Flatlist快速滑动过程中JS层会根据滑动的事件,触发Flatlist item的render渲染每一条数据,但是因为JSBridge的异步关系导致了shadow层最终呈现到原生的UI是异步的,而且滑动太快后会有大量的UI事件会阻塞在JSBridge,也导致了长时间的白屏出现,同时当部分item滑出可视区域一定的范围后UI内容会被回收等待下次滑到该区域后重新渲染。
新架构Fabric渲染原理
回到之前ScrollView的例子,看看Fabric是怎么解决快速滑动过程中的性能问题的。
1.初始化:JS到Shadow层已经是同步操作了,而shadow层到原生UI变成了可以异步也可以同步操作了,组件可以根据自己的业务场景来适配不同的操作。
2.滑动过程:原生端直接控制JS层渲染,同时创建shadow层的node节点,node节点会采用同步的方式渲染原生UI,整个过程中滑动的渲染是采用的同步操作,所以不会出现旧架构下白屏的问题。
Fabric –新的UI架构
1.React Fabric Renderer (JS) ,JS端的Render架构。
2.FabricUIManager (JS, C++) ,JS端和原生端UI管理模块。
3.ComponentDescriptor (C++) ,原生端组件的唯一描述及组件属性定义。
4.Platform-specific Component Impl (ObjC++, Java) ,原生端组件。
RCTSurface (ObjC++, Java),Surface组件。
从这些组件的结构描述来看,新的Fabric架构大致如下:
1.shadow层从原有的Java层,挪到了C++层。
2.由C++层来管理整体的UI组件,原有的Java层UIManager换到C++层,管理这些C++层到虚拟组件。
3.而原生的组件透过JNI层会在C++层生成对应的实例,绑定一些属性和方法。
4.JS层FabricUIManager透过JSI,唤起C++层去生产node节点,并最终对应到我们的ComponentDescriptor。
从整体来看JS端的node节点可以完整的和C++端的node节点一一对应,透过JSI可以完成同步的调用和属性同步,同样C++到原生java层到组件是通过JNI来完成的,而且也是同步操作。
下面我们参考下目前Facebook开放出来的部分代码:
1.ComponentDescriptor,原生和原生UI对应的一层抽象层,这边实现了原生端组件的属性和事件,并通过唯一标示注册到comonentRegister中,以下是已经开放出来的switch组件的代码架构。
ComponentDescriptor组件
整体的Fabric的UIManger 组件和消息通道是怎么建立的呢?大家可以参考文件Scheduler.cpp,JS会通过JSI调用该接口来初始化。
1.Fabric component 注册。
2.消息通道注册。
3.初始化UIManager和UIManagerbinding,其中UIManager提供了创建node、clone node,添加shadow node、关闭surface等功能,而UIManagerbinding是基于JSI接口直接实现了和JS端UIManger的直接调用,大家可以参考源码JS端是通过JSI的get方法,通过属性的方式通知UIManagerbinding执行C++层的UIManger,而UImanger最终会根据生成的shadow node生成对应的UI。
Fabric UI架构初始化
下面我们看看JS端是如何生成原生组件的,大家可以对照源码,在JS端我们有FabricUIManager,在初始化UIManagerBinding过程中,注册到运行的JS环境,因为UIManagerBinding是JSI实现的,所以可以理解为我们创建了一个Host代理对象,注册到了JS,而JS侧也对应同样的数据结构来一一对应。
下面是创建一个node的列子:
从目前的结构来看,后续Fabric UI开发,需要从C++ component层、shadow层、原生Java层,三个层次开发,而且创建的shadow层也是通过JSI的方式和JS层的node节点一一对应的。
JS端测量大小
JSI介绍
上面介绍Fabric架构时提到了JSI,那到底什么是JSI呢?如何能做到更原子级的控制每个模块和API呢?他是架起JS和原生java或者Objc的桥梁,类似于现有架构的JSBridge,但是采用的是内存共享、代理类的方式,JSI所有的运行环境都是在JSCRuntime环境下的,为了实现和JS端直接通讯,我们需要在C++层实现一个JSI::HostObject,该数据结构只有get、set两个接口,通过prop来区分不同接口的调用。
然后通过JSI接口生成一个JSObject,可以看到生成的代理对象和我们的HostObjectProxy是共享内存的,并且proxy中也实现了set和get方法。以下是具体的流程:
在JS端对应的是LazyObject,通过对这些Object的set、get,来完成对应C++实现的hostobject方法的调用:
TurboModule架构
这是基于新的JSI架构实现的Native module架构。JS层通过JSI获取到对应的C++层的module对象的代理,最终通过JNI回调Java层module。
C++层NativeMoudleProxy是通过JSI实现的对象,可以通过它传入module的名字获取C++层注册的module,已经这个module封装的所有的API method name。所以在JS业务加载的时候,会将这个proxy生成JS object代理对象到JS层。
JS层通过getNativeModule API并传递prop到JSI,最终会通过JSI接口找到Host object NativeModuleProxy,因NativeModuleProxy主要作用是将注册在C++层的JSI module通过名字生成JS Object传递的JS层调用, 所以其get方法中只有一个属性,就是通过JSINativeModules获取对应的module,而JSINativeModules是有缓存机制的,如果没有缓存的就直接解析该module中所有的API,如果有直接读取缓存的module信息。
大家可以看到在解析过程中,新版本增加了同步和异步的方法,也就是promise和sync。所以JSI module实际是可以同步操作API的,不像之前JSBridge的API都是异步操作的,同步操作的好处就是能做到线程间的同步。
所有的JSI module都是通过JSIMoudleregistry来注册的,当然这里注册的都是C++层的moulde,而所有的C++module最终是通过descriptor绑定到java层的turbomoudle中注册的Java层module,也就是我们最终在原生端实现的API,所以C++层module会通过对应的method prop来触发Java层的方法调用。
而C++层Native module是在java层instance manger初始化过程中注册的,遍历并注册java层和C++module。
所有理解Native Module的调用实际就是JSI的调用,而运行返回结果是基础的数据类型或者JSI object的,所以一个turbo module的method调用,返回值是可以是JSI object。开发者可以根据自己业务需要,将一些完整的数据结构封装成JSI host object,这样就可以做到,JS端同样可以获取到该对象,并同原生端对象形成了代理关系,可以同步完成一系列该Object porp功能操作,举个例子:
以前的调用方式通过JSBridge获取到一个picture,这个数据类型对应到JS端只能是一些基础的数据类型,比如我们参见的图片地址String类型,所以如果现在要上传这个图片的话,我们将JS端的数据再回传到原生端,如下图:
但有了JSI后就不一样了,我们在JS端透过JSI获取到的是JS Object,也就是picture,但是这个picture不再是简单的数据类型,而是和原生端形成绑定关系的结构,能够支持很多属性的同步设置,如改变alpha值等等,会直接触发host object的属性和函数调用,所以我们不再需要像之前一样改变alpha需要很多的JSBridge的调用,同样上传过程可以直接操作c++层的Object执行上传操作。
下面简单列举一下相关层次的调用关系:
社区化
React Native 目前有52个直接依赖包,然后这些包间接递归依赖了589个包,大家可以在http://npm.anvaka.com/#/view/2d/react-native网站看看整体的依赖关系,是一个非常复杂的关系图。
从目前React Native开源的代码来看,整体是个很大的repo工程,包含了各种各样的Native组件、API、JS module,如何让这些组件和API维护更简单,让接入React Native架构的APP能将API或者组件快速裁剪变小也成了这次架构重构的目标。
-
删除掉一些不需要的modules,将所有的module分离成小的repo,类似于组件化模块。
-
减小业务开发的bundle代码的大小(按需引入和编译需要的component),进而提升业务的渲染和启动速度。
-
分离后将这些组件或API开放给社区,社区可以贡献自己的资源,这样可以让组件的维护和迭代更快,帮助减少组件维护成本。
-
减少目前React Native SDK开发的依赖,简化代码结构,让SDK升级起来更轻量、更快速。
-
增加了社区的贡献让PR的修复加快,让更多的开发者集中在更合理的位置,减少重复开发,降低开放的复杂度。
未来facebook计划:
1.删除所有觉得使用率低或者无价值的组件或者API。
2.将现有维护的模块移到外部的repo,单独维护。
具体组件的规划和归类,大家可以同步参考Facebook提供的列表,有很多组件已经明确要删掉或者移到开源社区来,比如WebView。
以下是React Native组件社区的规划图:
以后原生端Native组件主要分三部分,一部份会放到系统的SDK,一部分移到开源社区维护,开发者再贡献一部分组件。对开发者而言带来的影响:
1.所有组件不能像之前一样一次就同步下来了,需要根据拆分的repo由开发者自己按需安装repo,碎片化比较严重。
2.因为社区运营后开发者贡献会越来越快,版本迭代也会加速,所以版本的控制和代码的安全性也是一个重要的问题。
3.很多社区化的组件是对原生Native module是有依赖的,所以增加了前端开发人员的集成开发难度。
总结
我们通过源码分析给大家简单介绍了Facebook的React Native下一代框架的设计,相信不管从性能体验和功能上都会有很大的变化。虽然整体变化很大但对于前端开发者而言JS的变化微乎其微,而重点改造在原生端组件和API架构,封装起来变得更加复杂,需要封装C++ shadow层,所以从以前的JAVA开发扩展到了C++和JAVA开发,对于开发者知识结构和储备要求更高,但对于提升性能而言,这些都是值得的。
社区化运营后Facebook官方可以从以前的组件和框架一起开发,简化到只需关注整体框架能力和性能了,让开发者贡献维护现在组件,大大提高了框架的迭代周期。
京东多端融合技术团队也会持续保持关注,待React Native新架构稳定后也会对渲染引擎进行升级并引入新的架构和特性。未来也会打通JDReact和JDFlutter双引擎的底层和组件,提供更为全面的跨端解决方案。
更多内容,请关注前端之巅。