React Native(一):iOS 源碼解讀及線上技術方案

前提

由於 React Native 自己會含有不少原生代碼,因此對於文本的讀者,但願你:前端

  • 瞭解 React Native 的基本使用方法
  • 能看懂 OC 的函數調用。🤦‍♂️

header image

背景

不要盲目

首先,什麼樣的狀況下須要 React Native,技術選型並非技術側一拍腦殼想出來的方案,而是須要根據業務場景來選擇合適的技術棧,去從技術的角度來輔助業務,加強業務的 UE、魯棒性、功能等。node

不少時候你其實並不須要 React Native,或者 React Native 會極大提升你的開發成本。這時候就須要考慮,是否能夠犧牲部分用戶體驗,使用 H5 來保證迭代速度。react

場景

在咱們 app 的首頁,會有不少動態更新的活動 cell,因爲是活動相關的 cell,固然不可能徹底用原生來實現,畢竟產品側是不會等到 app 發版以後才上線活動的。那麼根據這個場景,很容易就能夠想到使用 webview 來實現能夠動態更新的活動頁面。web

靜態化的 H5 的確是很是合適的選擇:面試

  • 開發成本低
  • 迭代速度快,基本上不收客戶端發版影響。

可是 H5 的缺點也很明顯,那就是性能。objective-c

H5 的模塊嵌入到首頁的 cell 中,若是採用客戶端渲染的 H5 頁面,會存在一個渲染時間的問題,致使用戶的體驗不是很好,並且在原生開發當中,cell 的渲染是可能會被回收的。好比,當咱們使用 UICollectionView 來渲染長列表的時候,通常都會使用 UICollectionViewCelldequeueReusableCellWithReuseIdentifier 來重用 cell,防止 cell 實例過多形成的內存泄漏。可是回收以後,若是要從新渲染以前的 H5 頁面,雖然沒有首次渲染的速度那麼慢,可是也仍是會存在白屏的狀況,在中、低端機器上尤爲明顯。算法

爲了解決上述的問題,考慮採用在原生的 cell 中嵌入 React Native 組件來進行活動的展現。react-native

至於爲何要看源碼,通常來講,閱讀源碼可讓咱們對於一個框架的瞭解更加深刻,這樣纔可以更優雅地使用它,可是若是要在生產環境使用 React Native,瞭解源碼能夠說是必不可少的了,至於緣由,會在文章中給你們按部就班地說明。數組

先看一點源碼 Orz

做爲一個前端開發,想到 React 的時候,都會想到 diff 算法,setState 流程等等 balabala 的面試問題(面試被問過 React 的人都懂)。bash

可是 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:若是需求不忙的話)。

相關文章
相關標籤/搜索