本文基于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,你可以:
- 一套代码完成iOS和Android的开发;
- 开发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
。但是Hermes
在React Native v0.68.1
中还是可选状态。
React Native的工作原理
有了JavaScript和Objective-C能够互相调用的基础后,React Native的工作原理很简单:
JavaScript调用Objective-C构建用户界面。iOS并不支持使用JavaScript来构建用户界面。而Objective-C有构建用户界面的能力(通过UIKit)。所以JavaScript是通过调用Objective-C间接完成用户界面的构建;
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.json
和package.json
这三个文件是通过npm导入JavaScript库需要的文件,至少需要导入react
和react-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.js
和node_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;
其中最重要的参数是bridge
和moduleName
,moduleName
前面提到过,它是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
的源码比较简单,它主要做的事情是:
- 创建并持有
bridge
; - 监听JavaScript的执行情况;
- 当监听到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
方法需要传入module
、method
和args
三个参数,通过这三个参数就可以确定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
方法需要传入moduleID
、methodID
和params
等参数,代表着要调用的Objective-C哪个模块(类),哪个方法,和传入参数。
所以JavaScript和Objective-C的相互调用,是通过MessageQueue的callFunctionReturnFlushedQueue
和enqueueNativeCall
两个方法来实现的。
知道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
模块的createView
和setChildren
方法来创建用户界面。
如果你用手指在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模块都是RCTBridgeModule
,RCTBridgeModule
会指定自己的methodQueue
,所以JavaScript调用Objective-C时也是跨线程调用的。
例如,“点击一个按钮,按钮产生一个被点击的动画效果”,这个动作需要跨越的线程有:
主线程---->JavaScript线程---->动画模块线程---->主线程
(传递触摸事件) (调用OC动画模块) (启动动画)
性能损耗
React Native的性能是接近原生的。到底有多接近原生,要看连接JavaScript和Objective-C的bridge的性能。
目前的bridge存在如下几个地方的性能损耗:
- JavaScript代码的加载和执行。目前400kb左右的JavaScript代码加载和执行大概需要100ms左右。总之JavaScript代码越多,执行的越久。100ms是用户可感知的等待了;
- 批处理机制。JavaScript和Objective-C之间的相互调用都是批量处理的。例如在MessageQueue的
enqueueNativeCall
方法的实现中,一个调用最多有5ms的等待; - 跨线程调用。上面一节提到,点击一个按钮这样简单的动作,都需要跨越三个线程的调用。
除了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岂不是美滋滋?