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, 当然要做额外的适配处理。如图:


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界面。

二、 代码流程

1、JS入口

对于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', () => AwesomeProject); 以上三个更像是参数,这个才是JS 程序的入口。即把当前APP的对象注册到AppRegistry组件中, AppRegistry组件是js module。

接着就等待Native事件驱动渲染JS端定义的APP组件。

2、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。

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

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

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

3、事件循环

所有的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 的消息队列循环:

分析代码可知,消息线程创建于ReactContext环境初始化时, MessageQueueThread.java当中, 该消息队列主要接收系统事件(如 Vsync、timer、doFrame、backkey)、UI事件(如键盘弹起、滚动等)以及 callback事件(JS 的回调函数)。
如图即ReactRootView往JS 传递键盘弹出的事件:

而对于Android 开发者, Android 已经为APP创建一个默认的 Main Looper, 不管是Android System 还是JS 事件都是发送到Main thread通过UI渲染出来。如图即是MessageQueueThread.java直接使用主线程Looper。

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

三、 通信机制

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

1、JS调用java

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

使用_genModules 加载所有native module到 RemoteModules数组。RemoteModules每项都是一个映射到native module的JS对象。

调用RemoteModules 的方法, 实际是把moduleID、methodId、args放入三个queue保存。

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

native层会在一定条件下触发事件, 通过bridge调用callFunctionReturnFlushedQueue
和 invokeCallbackAndReturnFlushedQueue ,得到的返回值就是这三个queue。

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

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

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

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

2、java调用JS

之前ReactInstanceManager 中运行JS APP组件,JAVA 是调用catalystInstance.getJSModule 方法获取JS 对象,然后直接访问对象方法runApplication。实际上getJSModule 返回的是js对象在java层的映射对象。

java层可以调用的JS模块主要在CoreModulesPackage.createJSModules方法配置,有:

如果调用JSModules对象的方法,则会动态代理跳转到(mBridge).callFunction(moduleId, methodId, arguments);

接着调用ReactBridge中声明的JNI 函数,
public native void callFunction(int moduleId, int methodId, NativeArray arguments);

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

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

四、 扩展机制

1、 模块扩展(native module)
官方文档操作:
https://facebook.github.io/react-native/docs/native-modules-android.html#content

2、 组件扩展(UI component)
官方文档操作:
https://facebook.github.io/react-native/docs/native-components-android.html#content

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

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList();
        modules.addAll(Arrays.<NativeModule>asList(
                new AsyncStorageModule(reactContext),
                new FrescoModule(reactContext),
                new NetworkingModule(reactContext),
                new WebSocketModule(reactContext),
                new ToastModule(reactContext)));
        if (mModuleList != null && mModuleList.size() > 0) {
            for (int i = 0; i < 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.Constructor constructor = c.getConstructor(parameterTypes);
                    Object[] parameters = {reactContext};
                    NativeModule module = (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,对接到应用管理发布平台。

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

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

资源离线

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

通过source属性设置图片资源路径, 映射到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

性能比较数据

  • Demo还在实现当中,等抓完再补充。

环境搭建

React Native运行原理解析的更多相关文章

  1. 《React Native 精解与实战》书籍连载「React Native 底层原理」

    此文是我的出版书籍<React Native 精解与实战>连载分享,此书由机械工业出版社出版,书中详解了 React Native 框架底层原理.React Native 组件布局.组件与 ...

  2. (转)Apache和Nginx运行原理解析

    Apache和Nginx运行原理解析 原文:https://www.server110.com/nginx/201402/6543.html Web服务器 Web服务器也称为WWW(WORLD WID ...

  3. View Animation 运行原理解析

    Android 平台目前提供了两大类动画,在 Android 3.0 之前,一大类是 View Animation,包括 Tween animation(补间动画),Frame animation(帧 ...

  4. JavaScript运行原理解析

    原文:1.http://blog.csdn.net/liaodehong/article/details/50488098 2.Stack的三种含义 (阮一峰) 3. http://lib.csdn. ...

  5. React Native Debug原理浅析

    第一次在segmentfault写博客,很紧张~~~公司项目上ReactNative,之前也是没有接触过,所以也是一边学习一边做项目了,最近腾出手来更新总结了一下RN的Debug的一个小知识点,不是说 ...

  6. .NET CORE学习笔记系列(5)——ASP.NET CORE的运行原理解析

    一.概述 在ASP.NET Core之前,ASP.NET Framework应用程序由IIS加载.Web应用程序的入口点由InetMgr.exe创建并调用托管,初始化过程中触发HttpApplicat ...

  7. 转:Apache和Nginx运行原理解析

    Web服务器 Web服务器也称为WWW(WORLD WIDE WEB)服务器,主要功能是提供网上信息浏览服务. 应用层使用HTTP协议. HTML文档格式. 浏览器统一资源定位器(URL). Web服 ...

  8. View 动画 Animation 运行原理解析

    这次想来梳理一下 View 动画也就是补间动画(ScaleAnimation, AlphaAnimation, TranslationAnimation...)这些动画运行的流程解析.内容并不会去分析 ...

  9. Apache和Nginx运行原理解析

    Web服务器 Web服务器也称为WWW(WORLD WIDE WEB)服务器,主要功能是提供网上信息浏览服务. 应用层使用HTTP协议. HTML文档格式. 浏览器统一资源定位器(URL). Web服 ...

随机推荐

  1. linux 删除命令

    rm *    文件名rm -r */ 文件夹rm -rf * 文件夹或文件名 -r 代表文件夹之下的都删除掉 -f 代表暴力删除,无需确认直接删完

  2. 读书笔记-《Maven实战》-2018/4/17

    第五章 坐标和依赖 1.如同笛卡尔坐标系一样,Maven也通过坐标三元素定位一个资源. <groupId>com.dengchengchao.test</groupId> &l ...

  3. logback学习二

    转载:https://www.cnblogs.com/DeepLearing/p/5663178.html 属性 : debug : 默认为false ,设置为true时,将打印出logback内部日 ...

  4. Future学习

    接着上一篇继续并发包的学习,本篇说明的是Callable和Future,它俩很有意思的,一个产生结果,一个拿到结果.        Callable接口类似于Runnable,从名字就可以看出来了,但 ...

  5. 在java中如何使用etcd的v2 和v3 api获取配置,并且对配置的变化进行监控

    etcd 和zookeeper 很像,都可以用来做配置管理.并且etcd可以在目前流行的Kubernetes中使用. 但是etcd 提供了v2版本合v3的版本的两种api.我们现在分别来介绍一下这两个 ...

  6. 服务器&阵列卡&组raid 5

    清除raid信息后,机器将会读不到系统, 后面若进一步操作处理, raid信息有可能会被初始化掉,那么硬盘数据就有可能会被清空, 导致数据丢失, 否则如果只是清除raid信息,重做raid是可以还原系 ...

  7. Unity CommandBuffer的一些学习整理

    1.前言 近期在整理CommandBuffer这块资料,之前的了解一直较为混乱. 算不上新东西了,但个人觉得有些时候要比加一个摄像机再转RT廉价一些,至少省了深度排序这些操作. 本文使用两个例子讲解C ...

  8. 剑指架构师系列-MySQL调优

    介绍MySQL的调优手段,主要包括慢日志查询分析与Explain查询分析SQL执行计划 1.MySQL优化 1.慢日志查询分析 首先需要对慢日志进行一些设置,如下: SHOW VARIABLES LI ...

  9. 转:rabbitMQ 安装与管理

    安装环境 虚拟机:VMware® Workstation 10.0.1 build Linux系统:CentOS6.5 官方安装:http://www.rabbitmq.com/install-rpm ...

  10. 【C#复习总结】匿名类型由来

    1 属性 这得先从属性开始说,为什么外部代码访问对象内部的数据用属性而不是直接访问呢,这样岂不是更方便一些,但是事实证明直接访问是不安全的.那么,Anders Hejlsberg(安德斯·海尔斯伯格) ...