内容简介:因为 React Native 本身会含有很多原生代码,所以对于文本的读者,希望你:首先,很多时候你其实并不需要 React Native,或者 React Native 会极大提高你的开发成本。这时候就需要考虑,是否可以牺牲部分用户体验,使用 H5 来保证迭代速度。
因为 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:如果需求不忙的话)。
以上所述就是小编给大家介绍的《React Native(一):iOS 源码解读及线上技术方案》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Phoenix解读 | Phoenix源码解读之索引
- Phoenix解读 | Phoenix源码解读之SQL
- Redux 源码解读 —— 从源码开始学 Redux
- AQS源码详细解读
- SDWebImage源码解读《一》
- MJExtension源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。