React Native(一):iOS 源码解读及线上技术方案

2,958 阅读12分钟

前提

因为 React Native 本身会含有很多原生代码,所以对于文本的读者,希望你:

  • 了解 React Native 的基本使用方法
  • 能看懂 OC 的函数调用。🤦‍♂️

header image

背景

不要盲目

首先,什么样的情况下需要 React Native,技术选型并不是技术侧一拍脑袋想出来的方案,而是需要根据业务场景来选择合适的技术栈,去从技术的角度来辅助业务,增强业务的 UE、鲁棒性、功能等。

很多时候你其实并不需要 React Native,或者 React Native 会极大提高你的开发成本。这时候就需要考虑,是否可以牺牲部分用户体验,使用 H5 来保证迭代速度。

场景

在我们 app 的首页,会有很多动态更新的活动 cell,由于是活动相关的 cell,当然不可能完全用原生来实现,毕竟产品侧是不会等到 app 发版之后才上线活动的。那么根据这个场景,很容易就可以想到使用 webview 来实现可以动态更新的活动页面。

静态化的 H5 的确是非常合适的选择:

  • 开发成本低
  • 迭代速度快,基本上不收客户端发版影响。

但是 H5 的缺点也很明显,那就是性能。

H5 的模块嵌入到首页的 cell 中,如果采用客户端渲染的 H5 页面,会存在一个渲染时间的问题,导致用户的体验不是很好,而且在原生开发当中,cell 的渲染是可能会被回收的。比如,当我们使用 UICollectionView 来渲染长列表的时候,一般都会使用 UICollectionViewCelldequeueReusableCellWithReuseIdentifier 来重用 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 加载流程:

  1. 创建 RCTRootView,为 React Native 提供原生 UI 中的根视图。
  2. 创建 RCTBridge,提供 iOS 需要的桥接功能。
  3. 创建 RCTBatchedBridge,实际上是这个对象为 RCTBridge 提供方法,让其将这些方法暴露出去。
  4. [RCTCxxBridge start],启动 JavaScript 解析进程。
  5. [RCTCxxBridge loadSource],通过 RCTJavaScriptLoader 下载 bundle,并且执行。
  6. 建立 JavaScript 和 iOS 之间的 Module 映射。
  7. 将模块映射到对应的 RCTRootView 当中。

下面是一个核心模块的部分 UML 类图,这些类会贯穿整个渲染阶段:

结合 JavaScript 后的加载流程

上面的渲染流程主要是 Native 侧的工作,而我们的代码在打包之后,本质上还是 JavaScript 代码,结合两侧的代码,可以得到一个完整的加载渲染流程。 继续上一个小节的第 5 步开始:

假设我们将 React Native 的 bundle 分成了业务 bundle 以及基础类库的 bundle。这两个 bundle 分别命名为 platform.bundle 以及 business.bundle。当然不分包的话更简单,一个 bundle 会在 bridge 初始化的时候全部执行完成。但是在实际情况下,不分包的可能性较小,因为我们不可能经常更新基础类库,这样会浪费流量,并且在基础类库下载的时候,会出现白屏的情况。而业务包却是经常更新的。

  1. 在完成了 RCTRootView 初始化之后,通过 [RCTCxxBridge loadSource] 来下载 bundle 代码。

  2. 在 bundle 下载完成之后,会触发 [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification object:self->_parentBridge userInfo:@{@“bridge”: self}]; 事件,通知 RCTRootView,对应的 JavaScript 代码已经加载完成。然后,执行 RCTRootContentView 的初始化。

  3. RCTRootContentView 在初始化的时候,会调用 bridge 的 [_bridge.uiManager registerRootView:self]; 方法,来将 RootView 注册到 RCTUIManager 的实例上。RCTUIManager,顾名思义,是 React Native 用来管理所有 UI 空间渲染的管理器。

  4. 完成 RCTRootContentView 的实例化之后,会执行 [self runApplication:bridge]; 来运行 JavaScript App。我们经常会见到 React Native 的红屏 Debug 界面,有一部分就是在这个时候,执行 JavaScript 代码报错导致的:[[RCTBridge currentBridge].redBox showErrorMessage:message withStack:stack];

  5. runApplication 方法会走到 [bridge enqueueJSCall:@“AppRegistry” method:@“runApplication” args:@[moduleName, appParameters] completion:NULL]; 中,RCTBatchedBridge 会维护一个 JavaScript 执行队列,所有 JavaScript 调用会在队列中依次执行,这个方法会传入指定的 ModuleName,来执行对应的 JavaScript 代码。

  6. 在 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 的基本数据类型。

  7. 在完成了 JavaScript 代码执行的时候,JavaScript 侧的代码会调用原生模块,这些原生模块调用,会被保存在队列中,在 void JSCExecutor::flush() 方法执行的时候,调用 void callNativeModules(JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) 一并执行。并且触发渲染。

整个过程的流程如下:

模块加载过程

UI 渲染流程

React Native 的 UI 渲染统一由 RCTUIManager 来进行管理。

上面一节有说到,在初始化 React Native 根视图 RCTRootView 的时候,会同时创建 RCTRootContentView 这个渲染视图。

  1. 而在进行 RCTBatchedBridge 初始化的时候,会初始化 RCTUIManager 对象,并且可以通过 Bridge 暴露出来的单例实例进行访问。

  2. RCTRootContentView 在进行初始化的时候,会调用 [_bridge.uiManager registerRootView:self];,来将这个 RCTRootContentView 实例注册到 Bridge 上。

  3. 在准备好了根视图之后,会调用 RCTRootViewrunApplication 方法,去执行对应的 JavaScript 代码。这里会走到上一个小节描述的流程当中,通过 callNativeModules 执行 JavaScript 调用的 Native 代码。

  4. 之后,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" 引入的原生模块,实际上是是调用 JSCExecutorgetNativeModule 方法,找到之前注册的对应的原生模块来进行引入。

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 执行完成返回的结果。

这个结果可以直接通过 JSCExecutorvoid 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:如果需求不忙的话)。