JS 的啟動過程
React Native 的 iOS 端代碼是直接從 Xcode IDE 里啟動的。在啟動時,首先要對代碼進行編譯,不出意外,在編譯后會彈出一個命令行窗口,這個窗口就是通過 Node.js 啟動的 development server 。
問題是這個命令行是怎么啟動起來的呢?實際上,Xcode 在 Build Phase 的最后一個階段對此做了配置,其實就是增加了一個 sh 腳本,讓小蔥的在編譯會自動去執行這個腳本,打開 npm,相當於你直接手動命令行切到 react-native 目錄下,執行 npm start 命令的效果是一樣的:

因此,代碼編譯后,就會執行 packager/react-native-xcode.sh
這個腳本。
查看這個腳本中的內容,發現它主要是讀取 XCode 帶過來的環境變量,同時加載 npm 包使得 Node.js 環境可用,最后執行 react-native-cli 的命令:
react-native bundle \
--entry-file index.ios.js \
--platform ios \
--dev $DEV \
--bundle-output "$DEST/main.jsbundle" \
--assets-dest "$DEST"
react-native
命令是全局安裝的,查看該文件,它調用了 react-native 包里的 local-cli/cli.js
中的 run 方法,最終進入了 private-cli/src/bundle/buildBundle.js
。它的調用過程為:
- ReactPackager.createClientFor
- client.buildBundle
- processBundle
- saveBundleAndMap
上面四步完成的是 buildBundle 的功能,細節很多很復雜。總體來說,buildBundle 的功能類似於 browerify 或 webpack :
- 從入口文件開始分析模塊之間的依賴關系;
- 對 JS 文件轉化,比如 JSX 語法的轉化等;
- 把轉化后的各個模塊一起合並為一個
bundle.js
。
之所以 React Native 單獨去實現這個打包的過程,而不是直接使用 webpack ,是因為它對模塊的分析和編譯做了不少優化,大大提升了打包的速度,這樣能夠保證在 liveReload 時用戶及時得到響應,由於我們公司的項目都比較大,一般都有十幾個 RN 的模塊,打開 RN 調試模式, commond + R 就像刷新瀏覽器頁面一樣,手機頁面就會刷新,這是你會在手機頂部看到 localhost:8081/xxx/xxx/xxx ,因為這是調試模式,是打開 chrome 去運行 js 環境的,所以有些很多測試環境的關於 websocket 的 bug 你一般都可以不用解,在生產環境下是不會遇到這種 bug 的.
Tips: 通過訪問 http://localhost:8081/debug/bundles 可以看到內存中緩存的所有編譯后的文件名及文件內容,如:

Native 啟動過程
Native 端就是一個 iOS 程序,程序入口是 main 函數,像通常一樣,它負責對應用程序做初始化,記住一定要仔細看appdelegate,任何應用他都是相當於橋梁一樣的作用.
除了 main 函數之外, AppDelegate
也是一個比較重要的類,它主要用於做一些全局的控制。在應用程序啟動之后,其中的 didFinishLaunchingWithOptions
方法會被調用,在這個方法中,主要做了幾件事:
- 定義了 JS 代碼所在的位置,它在 dev 環境下是一個 URL,通過 development server 訪問;在生產環境下則從磁盤讀取,當然前提是已經手動生成過了 bundle 文件,這個 bundle 文件在你下載軟件的時候已經下載下來了,他每次都會檢查是否有新的 bundle 包更新,如果有就會從服務器下載最新的 bundle 包,這個不需要重新上傳 appstore,所以可以做熱更新之類的功能.
- 創建了一個
RCTRootView
對象,該類繼承於UIView
,處於程序所有 View 的最外層; - 調用 RCTRootView 的
initWithBundleURL
方法。在該方法中,創建了bridge
對象。顧名思義,bridge 起着兩個端之間的橋接作用,其中真正工作的是類就是大名鼎鼎的 RCTBatchedBridge 。
RCTBatchedBridge 是初始化時通信的核心,我們重點關注的是 start 方法。在 start 方法中,會創建一個 GCD 線程,該線程通過串行隊列調度了以下幾個關鍵的任務。
loadSource
該任務負責加載 JS 代碼到內存中。和前面一致,如果 JS 地址是 URL 的形式,就通過網絡去讀取,如果是文件的形式,則通過讀本地磁盤文件的方式讀取。
initModules
該任務會掃描所有的 Native 模塊,提取出要暴露給 JS 的那些模塊,然后保存到一個字典對象中。
一個 Native 模塊如果想要暴露給 JS,需要在聲明時顯示地調用 RCT_EXPORT_MODULE
。它的定義如下:
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
可以看到,這就是一個宏,定義了 load 方法,該方法會自動被調用,在方法中對當前類進行注冊。模塊如果要暴露出指定的方法,需要通過 RCT_EXPORT_METHOD 宏進行聲明,原理類似。
setupExecutor
這里設置的是 JS 引擎,同樣分為調試環境和生產環境:
在調試環境下,對應的 Executor 為 RCTWebSocketExecutor,它通過 WebSocket 連接到 Chrome 中,在 Chrome 里運行 JS;
在生產環境下,對應的 Executor 為 RCTContextExecutor,這應該就是傳說中的 javascriptcore
。
moduleConfig
根據保存的模塊信息,組裝成一個 JSON ,對應的字段為 remoteModuleConfig。
injectJSONConfiguration
該任務將上一個任務組裝的 JSON 注入到 Executor 中。
下面是一個 JSON 示例,由於實際的對象太大,這里只截取了前面的部分:

JSON 里面就是所有暴露出來的模塊信息。
executeSourceCode
該任務中會執行加載過來的 JS 代碼,執行時傳入之前注入的 JSON。在調試模式下,會通過 WebSocket 給 Chrome 發送一條 message,內容大致為:
{
id = 10305;
inject = {remoteJSONConfig...};
method = executeApplicationScript;
url = "http://localhost:8081/index.ios.bundle?platform=ios&dev=true";
}
JS 接收消息后,執行打包后的代碼。如果是非調試模式,則直接通過 javascriptcore 的虛擬環境去執行相關代碼,效果類似。
JS 調用 Native
前面我們看到, Native 調用 JS 是通過發送消息到 Chrome 觸發執行、或者直接通過 javascriptcore 執行 JS 代碼的。而對於 JS 調用 Native 的情況,又是什么樣的呢?
在 JS 端調用 Native 一般都是直接通過引用模塊名,然后就使用了,比如:
var RCTAlertManager = require('NativeModules').AlertManager
可見,NativeModules 是所有本地模塊的操作接口,找到它的定義為:
var NativeModules = require('BatchedBridge').RemoteModules;
而BatchedBridge中是一個MessageQueue的對象:
let BatchedBridge = new MessageQueue(
__fbBatchedBridgeConfig.remoteModuleConfig,
__fbBatchedBridgeConfig.localModulesConfig,
);
在 MessageQueue 實例中,都有一個 RemoteModules 字段。在 MessageQueue 的構造函數中可以看出,RemoteModules 就是 __fbBatchedBridgeConfig.remoteModuleConfig 稍微加工后的結果。
class MessageQueue {
constructor(remoteModules, localModules, customRequire) {
this.RemoteModules = {};
this._genModules(remoteModules);
...
}
}
所以問題就變為: __fbBatchedBridgeConfig.remoteModuleConfig
是在哪里賦值的?
實際上,這個值就是 從 Native 端傳過來的JSON 。如前所述,Executor 會把模塊配置組裝的 JSON 保存到內部:
[_javaScriptExecutor injectJSONText:configJSON
asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
callback:onComplete];
configJSON 實際保存的字段為: _injectedObjects['__fbBatchedBridgeConfig']
。
在 Native 第一次調用 JS 時,_injectedObjects 會作為傳遞消息的 inject 字段。
JS 端收到這個消息,經過下面這個重要的處理過程:
'executeApplicationScript': function(message, sendReply) {
for (var key in message.inject) {
self[key] = JSON.parse(message.inject[key]);
}
importScripts(message.url);
sendReply();
},
看到沒,這里讀取了 inject 字段並進行了賦值。self 是一個全局的命名空間,在瀏覽器里 self===window
。
因此,上面代碼執行過后,window.__fbBatchedBridgeConfig 就被賦值為了傳過來的 JSON 反序列化后的值。
總之:
NativeModules = __fbBatchedBridgeConfig.remoteModuleConfig = JSON.parse(message.inject[‘__fbBatchedBridgeConfig’]) = 模塊暴露出的所有信息
好,有了上述的前提之后,接下來以一個實際調用例子說明下 JS 調用 Native 的過程。
首先我們通過 JS 調用一個 Native 的方法:
RCTUIManager.measureLayoutRelativeToParent(
React.findNodeHandle(scrollComponent),
logError,
this._setScrollVisibleLength
);
所有 Native 方法調用時都會先進入到下面的方法中:
fn = function(...args) {
let lastArg = args.length > 0 ? args[args.length - 1] : null;
let secondLastArg = args.length > 1 ? args[args.length - 2] : null;
let hasSuccCB = typeof lastArg === 'function';
let hasErrorCB = typeof secondLastArg === 'function';
let numCBs = hasSuccCB + hasErrorCB;
let onSucc = hasSuccCB ? lastArg : null;
let onFail = hasErrorCB ? secondLastArg : null;
args = args.slice(0, args.length - numCBs);
return self.__nativeCall(module, method, args, onFail, onSucc);
};
也就是倒數后兩個參數是錯誤和正確的回調,剩下的是方法調用本身的參數。
在 __nativeCall 方法中,會將兩個回調壓到 callback 數組中,同時把 (模塊、方法、參數) 也單獨保存到內部的隊列數組中:
onFail && params.push(this._callbackID);
this._callbacks[this._callbackID++] = onFail;
onSucc && params.push(this._callbackID);
this._callbacks[this._callbackID++] = onSucc;
this._queue[0].push(module);
this._queue[1].push(method);
this._queue[2].push(params);
到這一步,JS 端告一段落。接下來是 Native 端,在調用 JS 時,經過如下的流程:

總之,就是在調用 JS 時,順便把之前保存的 queue 作為返回值 一並返回,然后會對該返回值進行解析。
在 _handleRequestNumber 方法中,終於完成了 Native 方法的調用:
- (BOOL)_handleRequestNumber:(NSUInteger)i
moduleID:(NSUInteger)moduleID
methodID:(NSUInteger)methodID
params:(NSArray *)params
{
// 解析模塊和方法
RCTModuleData *moduleData = _moduleDataByID[moduleID];
id<RCTBridgeMethod> method = moduleData.methods[methodID];
@try {
// 完成調用
[method invokeWithBridge:self module:moduleData.instance arguments:params];
}
@catch (NSException *exception) {
}
NSMutableDictionary *args = [method.profileArgs mutableCopy];
[args setValue:method.JSMethodName forKey:@"method"];
[args setValue:RCTJSONStringify(RCTNullIfNil(params), NULL) forKey:@"args"];
}
與此同時,執行后還會通過 invokeCallbackAndReturnFlushedQueue
觸發 JS 端的回調。具體細節在 RCTModuleMethod 的 processMethodSignature 方法中。
再小結一下,JS 調用 Native 的過程為 :
- JS 把(調用模塊、調用方法、調用參數) 保存到隊列中;
- Native 調用 JS 時,順便把隊列返回過來;
- Native 處理隊列中的參數,同樣解析出(模塊、方法、參數),並通過 NSInvocation 動態調用;
- Native方法調用完畢后,再次主動調用 JS。JS 端通過 callbackID,找到對應JS端的 callback,進行一次調用
整個過程大概就是這樣,剩下的一個問題就是,為什么要等待 Native 調用 JS 時才會觸發,中間會不會有很長延時?
事實上,只要有事件觸發,Native 就會調用 JS。比如,用戶只要對屏幕進行觸摸,就會觸發在 RCTRootView 中注冊的 Handler,並發送給JS:
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
args:@[eventName, reactTouches, changedIndexes]];
除了觸摸事件,還有 Timer 事件,系統事件等,只要事件觸發了,JS 調用時就會把隊列返回。這塊理解可以參看 React Native通信機制詳解 一文中的“事件響應”一節。
總結
俗話說一圖勝千言,整個啟動過程用一張圖概括起來就是:

本文簡要介紹了 iOS 端啟動時 JS 和 Native 的交互過程,可以看出 BatchedBridge 在兩端通信過程中扮演了重要的角色。Native 調用 JS 是通過 WebSocket 或直接在 javascriptcore 引擎上執行;JS 調用 Native 則只把調用的模塊、方法和參數先緩存起來,等到事件觸發后通過返回值傳到 Native 端,另外兩端都保存了所有暴露的 Native 模塊信息表作為通信的基礎。