前提
因为 React Native 本身会含有很多原生代码,所以对于文本的读者,希望你:
- 了解 React Native 的基本使用方法
- 能看懂 OC 的函数调用。🤦♂️

背景
不要盲目
首先,什么样的情况下需要 React Native,技术选型并不是技术侧一拍脑袋想出来的方案,而是需要根据业务场景来选择合适的技术栈,去从技术的角度来辅助业务,增强业务的 UE、鲁棒性、功能等。
很多时候你其实并不需要 React Native,或者 React Native 会极大提高你的开发成本。这时候就需要考虑,是否可以牺牲部分用户体验,使用 H5 来保证迭代速度。
场景
在我们 app 的首页,会有很多动态更新的活动 cell,由于是活动相关的 cell,当然不可能完全用原生来实现,毕竟产品侧是不会等到 app 发版之后才上线活动的。那么根据这个场景,很容易就可以想到使用 webview
来实现可以动态更新的活动页面。
静态化的 H5 的确是非常合适的选择:
- 开发成本低
- 迭代速度快,基本上不收客户端发版影响。
但是 H5 的缺点也很明显,那就是性能。
H5 的模块嵌入到首页的 cell 中,如果采用客户端渲染的 H5 页面,会存在一个渲染时间的问题,导致用户的体验不是很好,而且在原生开发当中,cell 的渲染是可能会被回收的。比如,当我们使用 UICollectionView
来渲染长列表的时候,一般都会使用 UICollectionViewCell
的 dequeueReusableCellWithReuseIdentifier
来重用 cell,防止 cell 实例过多造成的内存泄漏。但是回收之后,如果要重新渲染之前的 H5 页面,虽然没有首次渲染的速度那么慢,但是也还是会存在白屏的情况,在中、低端机器上尤其明显。
为了解决上述的问题,考虑采用在原生的 cell 中嵌入 React Native 组件来进行活动的展示。
至于为什么要看源码,一般来说,阅读源码可以让我们对于一个框架的了解更加深入,这样才能够更优雅地使用它,但是如果要在生产环境使用 React Native,了解源码可以说是必不可少的了,至于原因,会在文章中给大家循序渐进地说明。
先看一点源码 Orz
作为一个前端开发,想到 React 的时候,都会想到 diff 算法,setState 流程等等 balabala 的面试问题(面试被问过 React 的人都懂)。
但是 React Native 源码的核心部分并不在于此。
概述
React Native 整体的结构如下图:

C++ 作为胶水层,抹平了 Android 和 iOS 平台的大部分差异,提供了给了 JavaScript 层基本一致的服务,从而让一套代码可以运行在两个平台之上。
简单来说,React Native 在执行的时候,还是会以 JavaScript 代码的方式进行执行,通过 Bridge 将 UI 变化映射到 Native,Native 采用其所在平台的方式,渲染成为 Native 的实际展示组件。当 Native 事件触发的时候,又通过 Bridge 映射这个事件到 JavaScript 中,JavaScript 进行计算之后,重新将需要渲染的内容还给 Native,实现一次用户交互过程。
从加载流程开始
React Native 有茫茫多的代码,这里找到一个切入点,开始整体代码流程的分析。
先介绍几个贯穿始终的类。
RCTRootView
所有 React Native 的 UI 映射到 Native 中的时候,都会通过 RCTRootView
这个根视图进行进行挂载和渲染。
// 这两个方法都是 RCTRootView 的初始化方法
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions;
- (instancetype)initWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;
bundleURL
:从本地或者远程异步拉取 React Native bundle,并且执行。moduleName
: 每一个 React Native 模块在其入口,都会通过AppRegistry.registerComponent
方法,为每一个模块都注册一个唯一的模块名,让 Native 来进行调用。这里就是注册的那个模块名。
上面的代码是 RCTRootView
的两个核心初始化方法,后者需要自己初始化 RCTBridge
,如果你的项目中有多个需要嵌入 React Native 的地方,那么尽量使用后者,然后手动实例化一个 RCTBridge
的单例。所有和 JavaScript 进行交互的操作都会通过这个 RCTBridge
实例进行。
RCTBridge
这个对象实例肩负着非常重要的职责。如果采用上一小节说到的 initWithBridge
来初始化 React Native 视图的话,那么就需要手动初始化 Bridge 对象了。
/**
* Creates a new bridge with a custom RCTBridgeDelegate.
*
* All the interaction with the JavaScript context should be done using the bridge
* instance of the RCTBridgeModules. Modules will be automatically instantiated
* using the default contructor, but you can optionally pass in an array of
* pre-initialized module instances if they require additional init parameters
* or configuration.
*/
- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)delegate
launchOptions:(NSDictionary *)launchOptions;
手动初始化 Bridge,可以通过继承 Bridge 提供的 Delegate 来进行。
RCTBridge
对象会持有一个 RCTBatchedBridge
实例,这个实例会处理所有的核心逻辑。
RCTCxxBridge
/RCTBatchedBridge
RCTCxxBridge
这个对象就已经下沉到了 C++ 中,RCTBatchBridge
的方法都来源于这个对象。这个对象在加载的时候,有三个比较核心的方法:
// 用于 JavaScript 源代码的加载,会启动一个 RCTJavaScriptLoader
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress
// 创建一个 JavaScript Thread 执行 JavaScript 代码,会实例化一个 JSCExecutor
- (void)start
// 执行 JavaScript 源代码,具有同步和异步两种方式
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync
上面的三个方法都是直接和 React Native 进程启动相关的。囊括了代码的加载与执行过程。
具体的代码可以在 React/CxxBridge/RCTCxxBridge.mm
中找到。
简单的加载流程
整理一下上面的两个对象提供的方法,可以得到一个完整的 React Native 加载流程:
- 创建
RCTRootView
,为 React Native 提供原生 UI 中的根视图。 - 创建
RCTBridge
,提供 iOS 需要的桥接功能。 - 创建
RCTBatchedBridge
,实际上是这个对象为RCTBridge
提供方法,让其将这些方法暴露出去。 [RCTCxxBridge start]
,启动 JavaScript 解析进程。[RCTCxxBridge loadSource]
,通过RCTJavaScriptLoader
下载 bundle,并且执行。- 建立 JavaScript 和 iOS 之间的 Module 映射。
- 将模块映射到对应的
RCTRootView
当中。
下面是一个核心模块的部分 UML 类图,这些类会贯穿整个渲染阶段:

结合 JavaScript 后的加载流程
上面的渲染流程主要是 Native 侧的工作,而我们的代码在打包之后,本质上还是 JavaScript 代码,结合两侧的代码,可以得到一个完整的加载渲染流程。 继续上一个小节的第 5 步开始:
假设我们将 React Native 的 bundle 分成了业务 bundle 以及基础类库的 bundle。这两个 bundle 分别命名为 platform.bundle
以及 business.bundle
。当然不分包的话更简单,一个 bundle 会在 bridge 初始化的时候全部执行完成。但是在实际情况下,不分包的可能性较小,因为我们不可能经常更新基础类库,这样会浪费流量,并且在基础类库下载的时候,会出现白屏的情况。而业务包却是经常更新的。
-
在完成了
RCTRootView
初始化之后,通过[RCTCxxBridge loadSource]
来下载 bundle 代码。 -
在 bundle 下载完成之后,会触发
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification object:self->_parentBridge userInfo:@{@“bridge”: self}];
事件,通知RCTRootView
,对应的 JavaScript 代码已经加载完成。然后,执行RCTRootContentView
的初始化。 -
RCTRootContentView
在初始化的时候,会调用 bridge 的[_bridge.uiManager registerRootView:self];
方法,来将 RootView 注册到RCTUIManager
的实例上。RCTUIManager
,顾名思义,是 React Native 用来管理所有 UI 空间渲染的管理器。 -
完成
RCTRootContentView
的实例化之后,会执行[self runApplication:bridge];
来运行 JavaScript App。我们经常会见到 React Native 的红屏 Debug 界面,有一部分就是在这个时候,执行 JavaScript 代码报错导致的:[[RCTBridge currentBridge].redBox showErrorMessage:message withStack:stack];
。 -
runApplication
方法会走到[bridge enqueueJSCall:@“AppRegistry” method:@“runApplication” args:@[moduleName, appParameters] completion:NULL];
中,RCTBatchedBridge
会维护一个 JavaScript 执行队列,所有 JavaScript 调用会在队列中依次执行,这个方法会传入指定的 ModuleName,来执行对应的 JavaScript 代码。 -
在 OC 层面,实际执行 JavaScript 代码的逻辑在
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async
中,这个方法有同步和异步两个版本,根据不同的场景可以选择不同的调用方式。实际上的执行逻辑会落在 C++ 层中的void JSCExecutor::loadApplicationScript( std::unique_ptr<const JSBigString> script, std::string sourceURL)
方法中。最终,通过JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef sourceURL)
方法来执行 JavaScript 代码,然后获取 JavaScript 执行结果,这个执行结果在 iOS 中是一个JSValueRef
类型的对象,这个对象可以转换到 OC 的基本数据类型。 -
在完成了 JavaScript 代码执行的时候,JavaScript 侧的代码会调用原生模块,这些原生模块调用,会被保存在队列中,在
void JSCExecutor::flush()
方法执行的时候,调用void callNativeModules(JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch)
一并执行。并且触发渲染。
整个过程的流程如下:

UI 渲染流程
React Native 的 UI 渲染统一由 RCTUIManager
来进行管理。
上面一节有说到,在初始化 React Native 根视图 RCTRootView
的时候,会同时创建 RCTRootContentView
这个渲染视图。
-
而在进行
RCTBatchedBridge
初始化的时候,会初始化RCTUIManager
对象,并且可以通过 Bridge 暴露出来的单例实例进行访问。 -
RCTRootContentView
在进行初始化的时候,会调用[_bridge.uiManager registerRootView:self];
,来将这个RCTRootContentView
实例注册到 Bridge 上。 -
在准备好了根视图之后,会调用
RCTRootView
的runApplication
方法,去执行对应的 JavaScript 代码。这里会走到上一个小节描述的流程当中,通过callNativeModules
执行 JavaScript 调用的 Native 代码。 -
之后,
RCTUIManager
会接手所有和 UI 相关的渲染工作。执行batchComplete
回调,进行- (void)_layoutAndMount
操作。完成视图的布局以及挂载工作。
至此,React Native 就完成了加载工作,并且将对应的原生视图渲染到了 UI 当中。
JS 调用 Native 方法
注册
Native 方法想要被 JavaScript 调用,首先需要将这个方法暴露出去。
为此,React Native 提供了 RCT_EXPORT_MODULE()
这个宏。
/**
* Place this macro in your class implementation to automatically register
* your module with the bridge when it loads. The optional js_name argument
* will be used as the JS module name. If omitted, the JS module name will
* match the Objective-C class name.
*/
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
从代码中可以看出,这个宏会通过 RCTRegisterModule(self)
方法,将对应的 js_name
注册到 RCTModuleClasses
中。这个 NSMutableArray
数组会存储所有暴露给 JavaScript 的模块,类似于模块的注册表。
之后,调用 RCTCxxBridge
的 _buildModuleRegistry
方法,来注册对应的 Native Modules。通过这个方法注册的原生模块,就可以通过 JavaScript 来引入了。
引入
在引入的时候,通过
import { NativeModules } from "react-native"
引入的原生模块,实际上是是调用 JSCExecutor
的 getNativeModule
方法,找到之前注册的对应的原生模块来进行引入。
JSValueRef JSCNativeModules::getModule(JSContextRef context, JSStringRef jsName) {
if (!m_moduleRegistry) {
return nullptr;
}
std::string moduleName = String::ref(context, jsName).str();
const auto it = m_objects.find(moduleName);
if (it != m_objects.end()) {
return static_cast<JSObjectRef>(it->second);
}
auto module = createModule(moduleName, context);
if (!module.hasValue()) {
// Allow lookup to continue in the objects own properties, which allows for overrides of NativeModules
return nullptr;
}
// Protect since we'll be holding on to this value, even though JS may not
module->makeProtected();
auto result = m_objects.emplace(std::move(moduleName), std::move(*module)).first;
return static_cast<JSObjectRef>(result->second);
}
JavaScript 侧
说完了 native 部分的原生模块引入,这里可以也看一下 JavaScript 这边对于原生模块的处理
。
我们所有引入的原生模块都来源于 NativeModules
这个 JavaScript 模块,而这个模块可以通过源码找到其路径为 node_modules/react-native/Libraries/BatchedBridge/NativeModules.js
,实际上导出的 NativeModules
来自于 NativeModules = global.nativeModuleProxy;
,这个 JavaScript 模块,实际上就是来自于 native 的 nativeModuleProxy
。
再次回到 native
nativeModuleProxy
在 native 通过
installGlobalProxy(
m_context,
"nativeModuleProxy",
exceptionWrapMethod<&JSCExecutor::getNativeModule>());
进行了注册,绑定到了 global
上面,来让 JavaScript 可以正确引入到这个模块。这个模块代理了很多 native 的功能,来让 JavaScript 进行直接调用。我们通过 RCT_EXPORT_MODULE()
宏注册到 JavaScript 中的方法也是通过 NativeModules
获取并且调用的。
native 调用 JavaScript 方法
这里就比较简单了,前面小节在讲到 React Native 启动的时候,说到过 native 可以进行 JavaScript 代码的执行,在执行完成之后,可以拿到 JavaScript 执行完成返回的结果。
这个结果可以直接通过 JSCExecutor
的 void JSCExecutor::callFunction( const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments)
方法执行。
至此
到这里,就基本上讲完了 React Native 如何和 JavaScript 进行交互,以及 React Native 如何渲染成为原生视图的整个过程。
其中涉及到的代码还是会比较多,本文只能对于其中比较重要的部分的功能进行简单说明,将整个渲染过程串起来,有兴趣的小伙伴还是最好自己去打一下断点,看看每个函数执行时候的参数。
由于我的 OC 功底确实不是很好,所以文中难免会有所疏漏,如果有大佬能够提供修改建议那是再好不过了。
至于为什么要读 React Native 的源码呢? 在进行跨平台开发的时候,React Native 本身提供的功能只是最基础的,在需要将 React Native 和原生混合使用的时候(当然这是大多数场景),是需要 native 来为 React Native 提供很多必要的功能的,这时就难免需要修改原生代码。
在接触了几个线上产品之后,React Native 混入到原生开发当中,来提供热更新功能,基本上已经是比较普及的方案了。下一篇文章应该会基于当前的解决方案,写一个原生 APP 混入 React Native 作为部分模块的 demo (小声BB:如果需求不忙的话)。