天天看点

React Native架构分析

facebook 于2015年9月15日推出react native for android 版本, 加上2014年底已经开源的ios版本,至此rn (react-native)真正成为跨平台的客户端框架。本篇主要是从分析代码入手,探讨一下rn在安卓平台上是如何构建一套js的运行框架。

rn 这套框架让 js开发者可以大部分使用js代码就可以构建一个跨平台app。 facebook官方说法是learn once, run everywhere, 即在android 、 ios、 browser各个平台,程序画ui和写逻辑的方式都大致相同。因为js 可以动态加载,从而理论上可以做到write once, run everywhere, 当然要做额外的适配处理。如图:

<a href="http://img3.tbcdn.cn/l1/461/1/04602301662556708a669ff6685f8978ce668b3d?spm=5176.100239.blogcont.6.noep5t" target="_blank"></a>

React Native架构分析

rn需要一个js的运行环境, 在ios上直接使用内置的javascriptcore, 在android 则使用webkit.org官方开源的jsc.so。 此外还集成了其他开源组件,如fresco图片组件,okhttp网络组件等。

rn 会把应用的js代码(包括依赖的framework)编译成一个js文件(一般命名为index.android.bundle), , rn的整体框架目标就是为了解释运行这个js 脚本文件,如果是js 扩展的api, 则直接通过bridge调用native方法; 如果是ui界面, 则映射到virtual dom这个虚拟的js数据结构中,通过bridge 传递到native , 然后根据数据属性设置各个对应的真实native的view。 bridge是一种js 和 java代码通信的机制, 用bridge函数传入对方module 和 method即可得到异步回调的结果。

对于js开发者来说, 画ui只需要画到virtual dom 中,不需要特别关心具体的平台, 还是原来的单线程开发,还是原来html 组装ui(jsx),还是原来的样式模型(部分兼容 )。rn的界面处理除了实现view 增删改查的接口之外,还自定义一套样式表达csslayout,这套csslayout也是跨平台实现。 rn 拥有画ui的跨平台能力,主要是加入virtual dom编程模型,该方法一方面可以照顾到js开发者在html dom的部分传承, 让js 开发者可以用类似dom编程模型就可以开发原生app , 另一方面则可以让virtual dom适配实现到各个平台,实现跨平台的能力,并且为未来增加更多的想象空间, 比如react-cavas, react-opengl。而实际上react-native也是从react-js演变而来。

对于 android 开发者来说, rn是一个普通的安卓程序加上一堆事件响应, 事件来源主要是js的命令。主要有二个线程,ui main thread, js thread。 ui thread创建一个app的事件循环后,就挂在looper等待事件 , 事件驱动各自的对象执行命令。 js thread 运行的脚本相当于底层数据采集器, 不断上传数据,转化成ui 事件, 通过bridge转发到ui thread, 从而改变真实的view。 后面再深一层发现, ui main thread 跟 js thread更像是cs 模型,js thread更像服务端, ui main thread是客户端, ui main thread 不断询问js thread并且请求数据,如果数据有变,则更新ui界面。

<a href="http://img2.tbcdn.cn/l1/461/1/23bbf9d71a527b7c15fbc6f01526b0ca0ce560e9?spm=5176.100239.blogcont.7.noep5t" target="_blank"></a>

React Native架构分析

对于js开发者来说, 整个rn app就只有一个js文件, 而开发者需要编写的就只有如上部分。主要是四个部分:

require 所有依赖到的组件, 相当于java中的import 或者 c++ 中的include。

var awesomeproject = react.createclass 创建app, 并且在render函数中返回ui界面结构(采用jsx ), 实际经过编译, 都会变成js 代码, 比如 变成 react.createelement(view,{style:{flex:1}},

var styles = stylesheet.create({, 创建css 样式,实际上会直接当做参数直接反馈到上面的react.createelement

appregistry.registercomponent('awesomeproject', () =&gt; awesomeproject); 以上三个更像是参数,这个才是js 程序的入口。即把当前app的对象注册到appregistry组件中, appregistry组件是js module。

接着就等待native事件驱动渲染js端定义的app组件。

<a href="http://img1.tbcdn.cn/l1/461/1/3ab928a93376a5df39d67dec7ce07e6cbc0eabb5?spm=5176.100239.blogcont.8.noep5t" target="_blank"></a>

React Native架构分析

对于android 开发者, 普通安卓程序入口是activity.oncreate()方法 , 主要有三个对象

reactrootview, android 标准的framelayout对象,另外一个功能是提供react 世界的入口,函数startreactapplication实际调用attachmeasuredrootview触发react世界的初始化。

myreactpackage, 配置当前app 需要加载的模块,rn 的js框架会在初始化阶段就会把native的模块按照配置加载到js数据结构中(messagequeue), 从而才能在js 层即可直接判断native是否支持某个模块。支持三种类型模块配置, native module(实际就是不需要操作view结构的api), view managers(实际是映射到virtual dom中的view组件), js module 。

reactinstancemanager, 构建react世界的运行环境,发送事件到js世界, 驱动整个react世界运转。 通过builder可以创建不同的react环境, 比如内置js 路径, 开发环境dev的js名字,是否支持调试等。doinbackground会加载指定的js文件, onpostexecute会调用runapplication接口运行js app。

<a href="http://img4.tbcdn.cn/l1/461/1/c3e9e520a8d98f4b61b7a2bfe4b39dd787b7c008?spm=5176.100239.blogcont.9.noep5t" target="_blank"></a>

React Native架构分析

reactrootview第一次onmeasured计算完成, 然后会利用reactinstancemanager创建 reactcontext上下文环境。重要的是初始化bridge以及加载js文件, 利用jsbundleloader方法加载index.android.bundle. 如图

<a href="http://img4.tbcdn.cn/l1/461/1/7c594f647aa51a0080435689022b84613ae82657?spm=5176.100239.blogcont.10.noep5t" target="_blank"></a>

React Native架构分析

此刻进入js 世界, 开发者的js 语句连同react js框架层被执行。该步骤最终语句是执行appregistry.registercomponent注册一个app组件,但还没有到开始渲染。

当运行环境准备完毕, 则调用bridge方法运行上步注册的app组件,触发一连串js 和 native相互通信,配合事件驱动, 从而完成native世界的渲染。如图利用bridge方法运行上面注册的js app组件的runapplication方法:

<a href="http://img2.tbcdn.cn/l1/461/1/c136ec2fbc26629ac7bfcfa717fa830d5adec0e9?spm=5176.100239.blogcont.11.noep5t" target="_blank"></a>

React Native架构分析

所有的app在操作系统中, 最终都会使用一个事件循环来运行。

一般来说,js 开发者只需要开发各个组件对象,监听组件事件, 然后利用framework接口调用render方法渲染组件。

而实际上,js 也是单线程事件循环,不管是 api调用, virtural dom同步, 还是系统事件监听, 都是异步事件,采用observer(观察者)模式监听java层事件, java层会把js 关心的事件通过bridge直接使用javascriptcore的接口执行固定的脚本, 比如"requrire (test_module).test_methode(test_args)"。此时,ui main thread相当于work thread, 把系统事件或者用户事件往js层抛,同时,js 层也不断调用模块api或者ui组件 , 驱动java层完成实际的view渲染。js开发者只需要监听js层framework定义的事件即可。如图即js thread 的消息队列循环:

<a href="http://img4.tbcdn.cn/l1/461/1/1d667a068698fb723f7d838b20ded8d28d29fc09?spm=5176.100239.blogcont.12.noep5t" target="_blank"></a>

React Native架构分析

分析代码可知,消息线程创建于reactcontext环境初始化时, messagequeuethread.java当中, 该消息队列主要接收系统事件(如 vsync、timer、doframe、backkey)、ui事件(如键盘弹起、滚动等)以及 callback事件(js 的回调函数)。

如图即reactrootview往js 传递键盘弹出的事件:

<a href="http://img2.tbcdn.cn/l1/461/1/6aa974701ca9fa045102e708a0201ecdbeac01e2?spm=5176.100239.blogcont.13.noep5t" target="_blank"></a>

React Native架构分析

而对于android 开发者, android 已经为app创建一个默认的 main looper, 不管是android system 还是js 事件都是发送到main thread通过ui渲染出来。如图即是messagequeuethread.java直接使用主线程looper。

<a href="http://img3.tbcdn.cn/l1/461/1/38939e065f76cee9815dea24237ee4040cd4931c?spm=5176.100239.blogcont.14.noep5t" target="_blank"></a>

React Native架构分析

跟普通app不同是,此时js thread相当于work thread, js会把对应的事件或者数据通过bridge发送到ui thread。 如图即是native java层收到的js事件的处理函数:

<a href="http://img4.tbcdn.cn/l1/461/1/c7ae031da1a4ebd43225f63a59c0437345f1bb70?spm=5176.100239.blogcont.15.noep5t" target="_blank"></a>

React Native架构分析

rn框架最主要的就是实现了一套java和 js通信的方案,该方案可以做到比较简便的互调对方的接口。一般的js运行环境是直接扩展js接口,然后js通过扩展接口发送信息到主线程。但rn的通信的实现机制是单向调用,native线程定期向js线程拉取数据, 然后转成js的调用预期,最后转交给native对应的调用模块。这样最终同样也可以达到java和 js 定义的module互相调用的目的。

js调用java 使用通过扩展模块require('nativemodules')获取native模块,然后直接调用native公开的方法,比如require('nativemodules').uimanager.managechildren()。 js 调用require('nativemodules')实际上是获取messagequeue里面的一个native模块列表的属性, 如:

<a href="http://img1.tbcdn.cn/l1/461/1/088ced5ec9cdc54f869075ccb8fe67e46985c484?spm=5176.100239.blogcont.16.noep5t" target="_blank"></a>

React Native架构分析

<a href="http://img4.tbcdn.cn/l1/461/1/cd563e3f4489f8fa31a75f17f947333c17aa55c3?spm=5176.100239.blogcont.17.noep5t" target="_blank"></a>

React Native架构分析

使用_genmodules 加载所有native module到 remotemodules数组。remotemodules每项都是一个映射到native module的js对象。

<a href="http://img1.tbcdn.cn/l1/461/1/7e18b499a98e384cc9f7b24b982bdfd3436e8ad1?spm=5176.100239.blogcont.18.noep5t" target="_blank"></a>

React Native架构分析

调用remotemodules 的方法, 实际是把moduleid、methodid、args放入三个queue保存。

<a href="http://img4.tbcdn.cn/l1/461/1/cd3fd4786a0e8f6a06d154bc316108faae7340f3?spm=5176.100239.blogcont.19.noep5t" target="_blank"></a>

React Native架构分析

至此, js端调用完毕, queue中数据要等待native层通过bridge来取。

native层会在一定条件下触发事件, 通过bridge调用callfunctionreturnflushedqueue

和 invokecallbackandreturnflushedqueue ,得到的返回值就是这三个queue。

<a href="http://img4.tbcdn.cn/l1/461/1/4c233eddcad914dd3ed85e09f25be9be95c80e13?spm=5176.100239.blogcont.20.noep5t" target="_blank"></a>

React Native架构分析

bridge会把这三个queue交给parsemethodcalls解析, 然后通过jni回调函数转发到java层

<a href="http://img4.tbcdn.cn/l1/461/1/53def2b0168f1256ffa684644484eaa7b72f14b5?spm=5176.100239.blogcont.21.noep5t" target="_blank"></a>

React Native架构分析

m_callback 函数是在bridge初始化的时候设置到c++层, 如:

<a href="http://img3.tbcdn.cn/l1/461/1/cbab5b561f4da3e4efdd64f616d707bad2730f6e?spm=5176.100239.blogcont.22.noep5t" target="_blank"></a>

React Native架构分析

然后在回调函数中,陆续调用reactcallback对象的call方法,weakcallback就是java层初始化bridge时传入的nativemodulesreactcallback对象,也就是reactcallback的子类。

<a href="http://img2.tbcdn.cn/l1/461/1/ba6de61200d5114f967ebf0197af14c3e923af61?spm=5176.100239.blogcont.23.noep5t" target="_blank"></a>

React Native架构分析

到此,转入java层. 从native module配置表中,取到对应module和method,并执行。

<a href="http://img4.tbcdn.cn/l1/461/1/9fec1799cf6c6e05cb6a60c5395dea1d572012ed?spm=5176.100239.blogcont.24.noep5t" target="_blank"></a>

React Native架构分析

之前reactinstancemanager 中运行js app组件,java 是调用catalystinstance.getjsmodule 方法获取js 对象,然后直接访问对象方法runapplication。实际上getjsmodule 返回的是js对象在java层的映射对象。

java层可以调用的js模块主要在coremodulespackage.createjsmodules方法配置,有:

<a href="http://img2.tbcdn.cn/l1/461/1/18dce11fe3eb5d62475c522bc3700ec6ba1fca80?spm=5176.100239.blogcont.25.noep5t" target="_blank"></a>

React Native架构分析

如果调用jsmodules对象的方法,则会动态代理跳转到(mbridge).callfunction(moduleid, methodid, arguments);

<a href="http://img2.tbcdn.cn/l1/461/1/54bf7411607b6da8da9c5a1b0485ece5bc5f61d1?spm=5176.100239.blogcont.26.noep5t" target="_blank"></a>

React Native架构分析

接着调用reactbridge中声明的jni 函数,

public native void callfunction(int moduleid, int methodid, nativearray arguments);

<a href="http://img4.tbcdn.cn/l1/461/1/b03f9b5b5dbe64be49ebe11f65002470170bc2bd?spm=5176.100239.blogcont.27.noep5t" target="_blank"></a>

React Native架构分析

<a href="http://img3.tbcdn.cn/l1/461/1/03022fded732c7b439b0c055bbdd411d706993a8?spm=5176.100239.blogcont.28.noep5t" target="_blank"></a>

React Native架构分析

通过js 的require和 apply函数拼接一段js 代码, 然后用javascriptcore的脚本运行接口执行,并得到返回值。

<a href="http://img2.tbcdn.cn/l1/461/1/19e44379ffb7fdff0473b5fe8cbb8b4ba096d106?spm=5176.100239.blogcont.29.noep5t" target="_blank"></a>

React Native架构分析

这样就在js引擎中运行了一段js代码并得到返回值,实现了java层到js层的调用。每次有java对js的访问, 则在返回值中从js层的messagequeue.js中抓取之前累积的一堆js calls。因为java层要把时间同步、 系统帧绘制等事件传递给js, 因此queue中的js calls都会在很短的时间内被抓取。

1、 模块扩展(native module)

官方文档操作:

<a href="https://facebook.github.io/react-native/docs/native-modules-android.html?spm=5176.100239.blogcont.30.noep5t#content" target="_blank">https://facebook.github.io/react-native/docs/native-modules-android.html#content</a>

2、 组件扩展(ui component)

<a href="https://facebook.github.io/react-native/docs/native-components-android.html?spm=5176.100239.blogcont.31.noep5t#content" target="_blank">https://facebook.github.io/react-native/docs/native-components-android.html#content</a>

因为react模块加载主要在reactpackage类配置,因此扩展可以通过反射、外部依赖注入等机制,可以做到跟h5容器一样实现动态插拔的插件式扩展。比如api扩展, 通过外部传入扩展模块的类名即可反射构造函数创建新的api:

@overridepubliclist createnativemodules(reactapplicationcontext reactcontext){

list modules = new arraylist();

modules.addall(arrays.aslist(

new asyncstoragemodule(reactcontext),

new frescomodule(reactcontext),

new networkingmodule(reactcontext),

new websocketmodule(reactcontext),

new toastmodule(reactcontext)));

if (mmodulelist != null &amp;&amp; mmodulelist.size() &gt; 0) {

for (int i = 0; i &lt; mmodulelist.size(); i++) {

try {

log.i("myreactpackage", "add module:" + mmodulelist.get(i));

class c = class.forname(mmodulelist.get(i));

class[] parametertypes = {reactapplicationcontext.class};                    java.lang.reflect.constructorconstructor=c.getconstructor(parametertypes);object[] parameters ={reactcontext};                    nativemodulemodule= (nativemodule)constructor.newinstance(parameters);modules.add(module);                }catch (exception e){

log.i("myreactpackage", "add module exeception:" + e);

e.printstacktrace();

}}        }        return modules;    }

离线包支持。 目前rn官方支持内置apk打包以及dev server在线更新。而实际上,一般的容器都会实现一套离线包发布平台。大致的实现方案是自定义一个jsbundleloader,对接到应用管理发布平台。

<a href="http://img2.tbcdn.cn/l1/461/1/d3845c7b892c37b661cd29976208b74252960bfc?spm=5176.100239.blogcont.32.noep5t" target="_blank"></a>

React Native架构分析

分离react 框架代码和应用业务代码。目前官方的生产工具是把框架代码和业务代码弄成一个bundle。 但框架代码很大,需要共用, 因此要分离出框架代码单独前置加载。 应用业务代码变成很小一段js代码单独发布。如果每次都加载框架代码, 启动业务代码会比较慢,一个helloworld都需要4秒左右。初步实践方案是把reactinstancemanager设置成全局变量共享,在native app 启动初始化或者第一次进入rn app时初始化reactinstancemanager。这个可能会导致多个rn app全局变量冲突。

在线更新

离线包更新主要依赖应用管理发布平台,大致可以做到跟h5离线包一致。

一般说的是图片资源比较多, rn 使用控件显示图片,如:

<a href="http://img3.tbcdn.cn/l1/461/1/d5349c523fc4c025299f8c01c4a56bd1b9432053?spm=5176.100239.blogcont.33.noep5t" target="_blank"></a>

React Native架构分析

通过source属性设置图片资源路径, 映射到native层:

<a href="http://img2.tbcdn.cn/l1/461/1/cbabf45903b94408359cab339bb4c11fd6e54705?spm=5176.100239.blogcont.34.noep5t" target="_blank"></a>

React Native架构分析

<a href="http://img1.tbcdn.cn/l1/461/1/b0241b605f2e6bea8b14cd5125fe859d801679b8?spm=5176.100239.blogcont.35.noep5t" target="_blank"></a>

React Native架构分析

因此不管是离线包内资源还是系统资源,只要能转换成android 统一资源定位uri对象,即可获取到图片。

如果是静态资源,则直接uri统一定位。如果是动态资源, 比如要通过网关获取到base64格式的图片,则需要native扩展特别接口。

1、 可能瓶颈

*因为bridge,  js和 java是异步互通,如果实现复杂多api的逻辑,可能会导致部分效率损耗在多线程通信。js 异步的编程方式多多少少带来一些不便。*因为bridge,  可能某些场景做不到及时响应。比如帧动画的实时控制。*android版本刚推出不完善,并且目前rn版本还在不停的更新中, 可能存在暗坑。*加入js引擎, 内存的控制比较麻烦,会比普通native增加不少。

2、 待研究

动态注入的api插件实现方案,能跟h5容器共用实现。

因为rn已经具备很多的灵活, js也可以做到很多大型控件,所以native ui扩展需要定义js 和 native边界, 哪些是js 实现, 哪些是native实现。

动画的实现方式。

h5容器和rn容器融合方案

write once, 完全跨平台。

js 层支持 fragment manager