ReactNative设计与实现之三:整体架构

ReactNative设计与实现之三:整体架构

在介绍完RN的背景实践之后,接下来我们将介绍RN的整体架构。

一、RN的整体架构

一个RN工程可以分为三大部分,JS域、native域以及负责两个域之间通信的C++ Bridge。如下图所示:

其中,JS域为单线程,使用的编程语言是JavaScript,JS代码运行在JavaScriptCore上。JS域主要负责实现APP的业务逻辑、并指定需要渲染的组件以及组件的布局。

Native域是一个多线程的环境,它有负责UI渲染的主UI线程,以及其他后台任务线程。值得注意的是,负责运行JS代码的线程是native域中多条后台线程中的一条。熟悉JS事件循环的同学应该都很清楚,JS本身没有线程,线程由宿主环境提供。native域的主要作用是提供宿主环境,并负责UI渲染与交互。native域的编程语言因平台而异,Android主要是Java和Kotlin,iOS主要是OC和Swift。

C++ Bridge主要负责JS域与Native域的通信,而通信则是指JS与Java、OC等语言之间的相互调用。在背景实践两篇文章中提到的NativeModule,就是通过C++ Bridge来实现的。

二、RN性能

性能是我们考量一个框架或一套技术方案的重要指标之一,下面我们将分别讨论RN三大部分的性能问题。

相对于Java、OC等编成语言,一直以来JS都留给我们执行慢的印象,其实随着JS语言的发展以及JS引擎的不断优化,现代JS代码的执行速度已经非常快了,性能问题一般不会出在JS域。Android与iOS对其各自的性能都有保证,性能问题一般也不会出现在native域。RN的性能瓶颈往往会出现在C++ Bridge上。

我们知道,在JS域和Native域中运行的是不同的语言,因此在不同域中定义的变量是不能相互访问的,所有跨语言的通信都需要通过C++ Bridge来完成。事实上,这与客户端与服务器间通过web通信的方式类似 -- 数据必须序列化后才能通过。而数据的序列化与反序列化是非常耗时的。同时,这里有一件非常酷的事 -- 当你在Chrome中调试RN的JS代码时,JS域和native域实际是运行在不同的计算机设备中的(你的PC和你的移动设备),它们之间通过WebSocket来桥接。

因此,为了构建一个高性能的RN APP,我们必须将桥上传递的数据量保持在最低限度

三、UI的异步更新

有一个很明显的性能陷阱:跨JS与native域进行UI的同步更新。如下图所示:

我们在JS域通过Button.setColor来改变一个button的颜色,然后由native侧来完成实际的改变button颜色的操作。当native在执行时,JS线程会被阻塞,直到native侧更新button颜色的操作执行完成后,JS线程才会继续执行。这是一个很明显的性能陷阱,问题在于,我们真的需要这份同步吗?

让我们来看看RN是怎么解决这个问题的,事实上,RN本身并没去解决这个问题。React.js在Web上解决了一个类似的问题,RN直接复用了它的解决方案。Web与RN有着类似的结构,我们有JS脚本,它运行在JS线程上,同时我们有DOM,DOM是浏览器native结构的一部分,每次通过DOM API来更新DOM时,都是一次跨JS与native域的UI的同步更新操作。React为了解决这个问题,提出了虚拟DOM的概念,结合一个智能的diff算法,将我们在JS侧对组件的更改批量、异步地发送到native侧 -- 同时页最小化了需要通过Bridge传递的数据量(diff算法)。

因此,RN中的UI更新是异步完成的。如下图所示:

JS侧的更新指令被批量、异步地发送到native侧,在native执行实际的更新操作时,JS线程不会被阻塞。这里需要注意一点,当我们在JS侧调用Button.setColor来更改button的颜色,紧接着调用Button.getColor来获取button的最新颜色,我们可能获取不到button的最新的颜色。因为,此时native侧的更新操作可能还没有完成,对于这种场景,我们需要特别注意。

四、RN中的线程(Android)

RN的Android端主要有三个线程,负责UI渲染的main_ui线程,负责执行JS代码的mqt_js线程,以及与NativeModule相关的mqt_native_modules线程,每个线程都有与其绑定的消息队列。

RN Android端在ReactQueueConfigurationImpl.java中定义并创建了mqt_js和mqt_native_modules线程,main_ui线程由Android本身创建。我们可以通过ReactContext来取得对这些线程的引用,代码如下:

CatalystInstance catalystInstance = reactContext.getCatalystInstance();
if (catalystInstance == null) {
  return;
}
ReactQueueConfiguration queueCfg =
        catalystInstance.getReactQueueConfiguration();
if (queueCfg == null) {
  return;
}
MessageQueueThreadImpl jsQueueThread = 
        (MessageQueueThreadImpl) queueCfg.getJSQueueThread();
MessageQueueThreadImpl nativeModulesQueueThread = 
        (MessageQueueThreadImpl) queueCfg.getNativeModulesQueueThread();

这三个线程之间的交互主要借助Android的Handler来完成。如下图所示:

mqt_js线程中运行的是JS代码,它位于JS域;而main_ui与mqt_native_modules线程中运行的是native代码,它们位于native域。native侧(main_ui和mqt_native_modules线程)通过调用jniCallJSFunctionjniCallJSCallback方法来执行JS侧的代码,从而实现与mqt_js线程交互;mqt_js线程通过调用NativeModule暴露给JS侧的方法与mqt_native_modules线程交互;而main_ui与mqt_native_modules可通过Handler直接交互。

关于JS域与native域代码互调的细节,我们将在下一篇文章《ReactNative设计与实现之四:Android端源码分析》中详细介绍。

五、Android APK的结构

下图是基于React-Native 0.53.3打出的Android APK结构:

上图中,所有与RN相关的动态链接文件,都给出了其在源码中的位置,一共有八个;同时,有三个文件名被加粗。第一个是assets下面的index.android.bundle,我们编写的JS代码以及这些代码所需要的依赖都会被打包到这个文件中。在APP的启动过程中,首先会加载这个bundle文件,并交由JavaScriptCore来执行。与业务相关的逻辑都在这个bundle中,如果我们能通过某种机制将这个bundle文件替换为新的bundle文件,在下次启动APP时,就会加载这个新的bundle文件,这样就达到更新业务逻辑的目的。这就是RN的热更新,CodePush等热更新方案就是通过这一机制来实现的。注意:热更新只能更新JS部分的改动,而不能更新native部分的改动,所有native部分的改动都需要通过正常的发版机制来完成。

第二个文件是libjsc.so,上文中我们有提到,RN中编译并继承了一个全功能的JS引擎:JavaScriptCore。libjsc.so就是JavaScriptCore的动态链接文件,它比较大,有4M左右,且每一个由RN工程打包出来的apk都包含一个独立的libjsc.so。

第三个文件是libreactnativejni.so,它是所有与RN相关的so文件的入口文件。

六、小结

本文首先介绍了RN的整体架构,其分为JS域、native域与C++ Bridge三大部分;随后介绍的RN的性能以及其UI异步更新的特性;接着又介绍了RN Android端的线程模型以及它们之间的交互,并给出了获取这些线程的引用的方法;最后简单分析了RN Android APK的结构。接下来,我们将开始分析Android端的源码

编辑于 2018-10-09

文章被以下专栏收录