由於 React Native 自己會含有不少原生代碼,因此對於文本的讀者,但願你:前端
首先,什麼樣的狀況下須要 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
來渲染長列表的時候,通常都會使用 UICollectionViewCell
的 dequeueReusableCellWithReuseIdentifier
來重用 cell,防止 cell 實例過多形成的內存泄漏。可是回收以後,若是要從新渲染以前的 H5 頁面,雖然沒有首次渲染的速度那麼慢,可是也仍是會存在白屏的狀況,在中、低端機器上尤爲明顯。算法
爲了解決上述的問題,考慮採用在原生的 cell 中嵌入 React Native 組件來進行活動的展現。react-native
至於爲何要看源碼,通常來講,閱讀源碼可讓咱們對於一個框架的瞭解更加深刻,這樣纔可以更優雅地使用它,可是若是要在生產環境使用 React Native,瞭解源碼能夠說是必不可少的了,至於緣由,會在文章中給你們按部就班地說明。數組
做爲一個前端開發,想到 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 加載流程:
RCTRootView
,爲 React Native 提供原生 UI 中的根視圖。RCTBridge
,提供 iOS 須要的橋接功能。RCTBatchedBridge
,其實是這個對象爲 RCTBridge
提供方法,讓其將這些方法暴露出去。[RCTCxxBridge start]
,啓動 JavaScript 解析進程。[RCTCxxBridge loadSource]
,經過 RCTJavaScriptLoader
下載 bundle,而且執行。RCTRootView
當中。下面是一個核心模塊的部分 UML 類圖,這些類會貫穿整個渲染階段:
上面的渲染流程主要是 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)
一併執行。而且觸發渲染。
整個過程的流程以下:
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 當中。
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);
}
複製代碼
說完了 native 部分的原生模塊引入,這裏能夠也看一下 JavaScript 這邊對於原生模塊的處理 。 咱們全部引入的原生模塊都來源於 NativeModules
這個 JavaScript 模塊,而這個模塊能夠經過源碼找到其路徑爲 node_modules/react-native/Libraries/BatchedBridge/NativeModules.js
,實際上導出的 NativeModules
來自於 NativeModules = global.nativeModuleProxy;
,這個 JavaScript 模塊,實際上就是來自於 native 的 nativeModuleProxy
。
nativeModuleProxy
在 native 經過
installGlobalProxy(
m_context,
"nativeModuleProxy",
exceptionWrapMethod<&JSCExecutor::getNativeModule>());
複製代碼
進行了註冊,綁定到了 global
上面,來讓 JavaScript 能夠正確引入到這個模塊。這個模塊代理了不少 native 的功能,來讓 JavaScript 進行直接調用。咱們經過 RCT_EXPORT_MODULE()
宏註冊到 JavaScript 中的方法也是經過 NativeModules
獲取而且調用的。
這裏就比較簡單了,前面小節在講到 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:若是需求不忙的話)。