React Native原理

本文基于iOS平台来分析React Native v0.68.1的原理。阅读本文章需要一定的React Native开发基础。

React Native的作用

React Native来源于React。React是Facebook开源的用来构建用户界面的一个JavaScript库,在web页面开发领域取得了巨大的成功。在拥有了大量优秀的React开发者后,Facebook想,能不能使用React来开发手机app?于是就有了React Native。

所以,如果说React是用来在web浏览器上做用户界面的JavaScript库,那么React Native就是用来在iOS和Android上做用户界面的JavaScript库

当然,通过JavaScript语言自身的能力,手机app的业务逻辑也完全可以通过React Native完成。

所以通过React Native,你可以:

  1. 一套代码完成iOS和Android的开发;
  2. 开发app中的某些页面(Hybrid App)。

React Native的基础

现在我们知道,React Native来源于React,是一个JavaScript库。所以React Native的基础,就是iOS和Android需要有执行JavaScript代码的能力

iOS从iOS7开始开放JavaScriptCore框架。通过JavaScriptCore,Objective-C和JavaScript之间就能相互调用了:

#import 
...
// 初始化JavaScriptCore上下文
JSContext *ctx = [[JSContext alloc] init];

// 执行JavaScript代码:
[ctx evaluateScript:@"var sum = 1 + 1;"];
// 获取上面的执行结果sum:
JSValue *sum = ctx[@"sum"];
NSLog(@"sum = %d", [sum toInt32]); // sum = 2

// 将Objective-C的block传入JavaScript
ctx[@"add"] = ^(int a, int b) {
    return a + b;
};
// 在JavaScript中调用Objective-C中的block:
[ctx evaluateScript:@"sum = add(3, 4);"];
// 获取上面的执行结果sum:
sum = ctx[@"sum"];
NSLog(@"sum = %d", [sum toInt32]); // sum = 7

目前Android也是使用JavaScriptCore来实现Java与JavaScript的相互调用。但是Android使用JavaScriptCore存在性能问题。为了解决这个问题,Facebook引入了新的JavaScript引擎Hermes。但是HermesReact Native v0.68.1中还是可选状态。

React Native的工作原理

有了JavaScript和Objective-C能够互相调用的基础后,React Native的工作原理很简单:

  1. JavaScript调用Objective-C构建用户界面。iOS并不支持使用JavaScript来构建用户界面。而Objective-C有构建用户界面的能力(通过UIKit)。所以JavaScript是通过调用Objective-C间接完成用户界面的构建;

  2. Objective-C把系统事件传递给JavaScript进行处理。系统事件包括触摸屏幕事件,定时器事件等。例如用户点击了一个按钮,那么Objective-C会把这个触摸事件传递给JavaScript进行处理。

这就是React Native的工作原理。当然JavaScript和Objective-C相互调用的内容不止上面这些,除此之外,JavaScript还可以通过调用Objective-C获取iOS的硬件、数据库、网络等能力;Objective-C还会通过调用JavaScript进行初始化等。

所以React Native最终是通过Objective-C(UIKit)来构建用户界面的,因此我们说React Native的性能接近于原生。那为什么是“接近”而不是“等于”呢?是因为在目前的架构中,JavaScript与Objective-C的相互调用是异步的,可能存在几毫秒的延迟。所以React Native的性能只能说是接近原生。

React Native v0.68.1中,Facebook发布了新的架构,该架构目前还是可选阶段。等到该架构正式使用后,相信React Native的性能会进一步提升。

React Native的开发流程

捋一捋React Native的开发流程,可以帮助我们更好地了解React Native的原理。

创建React Native工程

首先需要创建React Native工程,你可以在已有的原生项目中引入React Native(Hybrid App),也可以从零开始创建一个React Native工程。

一个最基本的React Native的工程目录是这样的:

工程目录

其中,android目录是Android Studio的工程目录,存放的是Android的原生代码;ios目录是Xcode的工程目录,存放的是iOS的原生代码;node_modules目录、package-lock.jsonpackage.json这三个文件是通过npm导入JavaScript库需要的文件,至少需要导入reactreact-native两个JavaScript库;index.js是开始编写我们自己的JavaScript代码的文件。

编写JavaScript代码

可以做一个Hybrid App作为例子。我们使用React Native来做一个最简单的用户界面:一个红色的方块。那么我可以在index.js文件中编写如下代码:

// 从react和react-native两个JavaScript库中导入所需要的对象
import React from 'react'
import { AppRegistry, View } from 'react-native'

// 实现红色方块
const RedBlock = props => {
    return ;
};

// 注册红色方块
AppRegistry.registerComponent('RedBlock', () => RedBlock);

这样JavaScript部分的代码就完成了。

打包JavaScript代码

我们的JavaScript代码分散在index.jsnode_modules文件夹内的各个文件内,React Native需要把它们打包成一个文件来执行。

在React Native工程根目录下,可以通过终端命令npx react-native bundle进行打包(可以通过npx react-native bundle -h命令查看其用法):

npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ReactNative.jsbundle --assets-dest ./ 

就可以把所有的JavaScript代码打包成一个文件了。在当前的项目中,全部的JavaScript代码有400多kb大小,400多行代码。

提示:在实际开发时,不需要这样打包。开发的时候会通过node启动一个http服务器,JavaScript代码是从这个http服务器拉取的。每当你修改了JavaScript代码后,会重新从http服务器拉取最新的JavaScript代码,从而实现热调试功能。

JavaScript代码打包好后,把它拖进Xcode工程内,准备让Objective-C调用。

编写原生代码

一般情况下我们并不需要编写原生代码,因为这意味着我们需要懂得原生开发,这不是React Native愿意看到的。React Native希望我们可以使用JavaScript完成整个app的开发。

但是我们这个项目是一个Hybrid App,所以还是需要写一点原生代码的。其实很简单,所有的魔法都是从一个RCTRootView开始的:

#import "ViewController.h"
#import 

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 我们打包的JavaScript代码的URL
    NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"ReactNative" withExtension:@"jsbundle"];
    
    // 创建RCTRootView
    RCTRootView *redblock = [[RCTRootView alloc] initWithBundleURL:bundleURL moduleName:@"RedBlock" initialProperties:nil launchOptions:nil];
    redblock.frame = CGRectMake(100,100,100,100);
    [self.view addSubview:redblock];
}

@end

创建RCTRootView需要bundleURL和moduleName两个参数。bundleURL就是我们上一步打包的JavaScript代码的URL,用来取得我们的JavaScript代码。moduleName就是我们上面在index.js中使用AppRegistry.registerComponent注册的红色方块组件名字。

运行Xcode项目,就能等到我们的成果:

红色方块

这就是一个React Native项目开发的流程。

RCTRootView揭秘

RCTRootView是React Native的魔法发生的地方。所以,要想了解React Native,就要从RCTRootView入手。可以先看看RCTRootView`指定的构造方法:

- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initialProperties:(nullable NSDictionary *)initialProperties;

其中最重要的参数是bridgemoduleNamemoduleName前面提到过,它是JavaScript代码中使用AppRegistry.registerComponent注册的组件名。我们的重点是bridge,它是React Native中最核心的东西bridge顾名思义就是“桥”的意思,它发挥的作用也恰恰和桥一样:它是JavaScript和Objective-C之间的一座桥,用来实现JavaScript和Objective-C之间的相互调用。

这里不深究bridge。但是我们需要知道,bridge创建时,需要去加载和执行JavaScript代码。没错,就是上面打包的400多kb,400多行的JavaScript代码。bridge会创建一个JavaScriptCore上下文,然后从头到尾执行这份JavaScript代码,执行完毕后,React Native中的各种变量就存在这个上下文中了(例如AppRegistry对象),就准备好随时被Objective-C调用。

RCTRootView的源码比较简单,它主要做的事情是:

  1. 创建并持有bridge;
  2. 监听JavaScript的执行情况;
  3. 当监听到JavaScript代码执行完毕后,通过bridge调用JavaScript中的AppRegistry模块的runApplication方法,随后就开始进行用户界面构建了。

bridge浅析

React Native目前是通过bridge来实现JavaScript和Objective-C之间相互调用的,所以bridge是React Native的核心。但是bridge比较复杂,而且新的架构已经发布了,目前的bridge(React Naitve 0.68.1)将会被淘汰,所以这里只会简单分析一下当前bridge的特点。

批量处理的bridge

RCTRootView的bridge是一个RCTBridge对象。RCTBridge的实际功能是由其内部变量batchedBridge实现的(目前batchedBridge是一个RCTCxxBridge对象)。batchedBridge,可以翻译为“批量处理的桥”,通过阅读源码可以知道,在当前的bridge设计中,JavaScript和Objective-C的相互调用是批量处理的。

批量处理,就是说JavaScript和Objective-C的相互调用不会马上执行,而是攒够一批,然后再一起处理。例如当用户的手指在屏幕上滑动时,会在短时间内产生大量的触摸事件,根据当前的bridge的设计,这些触摸事件是成批传递给JavaScript的,而不是每一个触摸事件单独实时进行传递。

加载并执行JavaScript代码

bridge在创建时,最重要的任务是加载并执行JavaScript代码。JavaScript代码就是前面我们说的通过npx react-native bundle命令打包的JavaScript代码,在前面我们的RedBlock项目中,打包后的代码大概是400多kb,共400多行代码。这400多行代码执行完毕后,React Native的环境就准备就绪了(React Native需要的各种变量已创建完毕),就可以准备好被Objective-C调用。

JavaScript是一种没有多线程能力的语言。在当前bridge的设计中,会创建一个名为"com.facebook.react.JavaScript"的线程,然后让所有JavaScript都在这个线程中执行。

Objective-C和JavaScript相互调用的方式

JavaScript代码被执行后,会创建一个MessageQueue变量。MessageQueue是bridge的重要组成部分,因为JavaScript和Objective-C相互调用的两个重要方法都在MessageQueue里。

首先,Objective-C对JavaScript的调用,都是通过调用MessageQueue的callFunctionReturnFlushedQueue方法来实现的:

  callFunctionReturnFlushedQueue(
    module: string,
    method: string,
    args: mixed[],
  ): null | [Array, Array, Array, number] {
    this.__guard(() => {
      this.__callFunction(module, method, args);
    });

    return this.flushedQueue();
  }

callFunctionReturnFlushedQueue方法需要传入modulemethodargs三个参数,通过这三个参数就可以确定Objective-C需要调用JavaScript的哪个模块(类),哪个方法和传入的参数。

而JavaScript对Objetive-C的调用也和callFunctionReturnFlushedQueue方法有关,因为该方法执行后会返回一个flushedQueue,这个flushedQueue存放的是JavaScript需要调用的Objective-C的内容。Objective-C拿到这个flushedQueue后,就会执行JavaScript需要调用的内容。

而这个flushedQueue的内容,是通过MessageQueue的enqueueNativeCall方法来添加的:

// 为了方便理解,已剔除调试和打印代码
enqueueNativeCall(
    moduleID: number,
    methodID: number,
    params: mixed[],
    onFail: ?(...mixed[]) => void,
    onSucc: ?(...mixed[]) => void,
  ) {
  // 处理回调方法
    this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
    // 将要调用的Objective-C内容保存到队列,体现了批处理的特点
    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);
    this._queue[PARAMS].push(params);

    // 如果距离上一次调用超过MIN_TIME_BETWEEN_FLUSHES_MS(5ms)
    // 立即让Objective-C执行队列内容
    // 所以JavaScript对Objective-C的调用最多是5ms一批
    const now = Date.now();
    if (
      global.nativeFlushQueueImmediate &&
      now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
    ) {
      const queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      global.nativeFlushQueueImmediate(queue);
    }
  }

enqueueNativeCall方法需要传入moduleIDmethodIDparams等参数,代表着要调用的Objective-C哪个模块(类),哪个方法,和传入参数。

所以JavaScript和Objective-C的相互调用,是通过MessageQueue的callFunctionReturnFlushedQueueenqueueNativeCall两个方法来实现的。

知道Objetive-C和JavaScript相互调用的渠道后,很容易将相互调用的内容打印出来。在上面的项目中,为了构建红色方块用户界面,JavaScript和Objetive-C相互调用的情况如下(已省略部分无关调用):

OC->JS AppRegistry runApplication 
JS->OC UIManager createView
JS->OC UIManager createView
JS->OC UIManager setChildren
JS->OC UIManager createView
JS->OC UIManager setChildren
JS->OC UIManager setChildren

可以看到,Objective-C首先调用JavaScript的AppRegistry模块的runApplication方法。然后,JavaScript通过调用Objective-C的UIManager模块的createViewsetChildren方法来创建用户界面。

如果你用手指在React Native构建的用户界面上滑动,你会发现:

OC->JS RCTEventEmitter receiveTouches
OC->JS RCTEventEmitter receiveTouches
OC->JS RCTEventEmitter receiveTouches
...

Objective-C通过调用JavaScript的RCTEventEmitter模块的receiveTouches方法,将大量的触摸事件传递给JavaScript进行处理。

跨线程调用

上面提到,JavaScript是在自己专有的线程中运行的。所以Objective-C和JavaScript的相互调用,是要跨线程的。

每个可以被调用的Objective-C模块都是RCTBridgeModuleRCTBridgeModule会指定自己的methodQueue,所以JavaScript调用Objective-C时也是跨线程调用的。

例如,“点击一个按钮,按钮产生一个被点击的动画效果”,这个动作需要跨越的线程有:

主线程---->JavaScript线程---->动画模块线程---->主线程
 (传递触摸事件)    (调用OC动画模块)  (启动动画) 

性能损耗

React Native的性能是接近原生的。到底有多接近原生,要看连接JavaScript和Objective-C的bridge的性能。

目前的bridge存在如下几个地方的性能损耗:

  1. JavaScript代码的加载和执行。目前400kb左右的JavaScript代码加载和执行大概需要100ms左右。总之JavaScript代码越多,执行的越久。100ms是用户可感知的等待了;
  2. 批处理机制。JavaScript和Objective-C之间的相互调用都是批量处理的。例如在MessageQueue的enqueueNativeCall方法的实现中,一个调用最多有5ms的等待;
  3. 跨线程调用。上面一节提到,点击一个按钮这样简单的动作,都需要跨越三个线程的调用。

除了bridge的性能损耗外,React Native还有一个问题:所有的用户界面都是在一个RCTRootView上面构建的,这意味着这一个RCTRootView上面渲染的视图可能会非常多,这也是会影响性能的因素。我们知道,在原生开发的时候,一个app会有很多个UIViewController,每个UIViewController承载不同的用户界面。但是在React Native中,RCTRootView承载着所有的用户界面(就算使用React Navigation进行所谓的页面跳转,实际还是在一个RCTRootView上面渲染内容)。

React Native的前景

至此,React Native的原理分析完毕。React Native的原理就是JavaScript和Objetive-C的相互调用,相互调用的效率决定其性能。

React Native是Facebook开源的一个项目,Facebook自家的产品,包括Facebook、Instagram和Oculus都在使用。另外的一些大厂,例如腾讯QQ、京东和特斯拉也在使用。可以说只要Facebook没有倒闭,React Native就会一直维护和更新,会越来越好。

React Native最大的优势当然是跨平台,一套代码解决Android和iOS两个平台app的开发,这简直太棒了!另外,React Native使用Flexbox布局,并支持热调试,开发用户界面的效率非常高。最后,React Native支持热更新,可以绕过上架审核的过程,快速部署新内容。

但是目前在国内,Google开源的flutter其实比React Native更受欢迎,因为据说flutter的性能更好?但是我认为它们的差距其实可以忽略不计,它们都只是一个工具,真正重要的是使用工具的人:你能把这个工具用的多好?

最后,如果你是一个JavaScript全栈工程师,恰好你又懂React,那么React Native岂不是美滋滋?

你可能感兴趣的:(React Native原理)