使用 JS 構建跨平臺的原生應用:React Native iOS 通信機制初探
在初識 React Native 時,非常令人困惑的一個地方就是 JS 和 Native 兩個端之間是如何相互通信的。本篇文章對 iOS 端 React Native 啟動時的調用流程做下簡要總結,以此窺探其背后的通信機制。
JS 啟動過程
React Native 的 iOS 端代碼是直接從 Xcode IDE 里啟動的。在啟動時,首先要對代碼進行編譯,不出意外,在編譯后會彈出一個命令行窗口,這個窗口就是通過 Node.js 啟動的 development server 。
問題是這個命令行是怎么啟動起來的呢?實際上,Xcode 在 Build Phase 的最后一個階段對此做了配置:

因此,代碼編譯后,就會執行 packager/react-native-xcode.sh 這個腳本。
查看這個腳本中的內容,發現它主要是讀取 XCode 帶過來的環境變量,同時加載 nvm 包使得 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 命令是全局安裝的,在我本機上它的地址是 /usr/local/bin/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 時用戶及時得到響應。
Tips: 通過訪問 http://localhost:8081/debug/bundles 可以看到內存中緩存的所有編譯后的文件名及文件內容,如:

Native 啟動過程
Native 端就是一個 iOS 程序,程序入口是 main 函數,像通常一樣,它負責對應用程序做初始化。
除了 main 函數之外, AppDelegate 也是一個比較重要的類,它主要用于做一些全局的控制。在應用程序啟動之后,其中的 didFinishLaunchingWithOptions 方法會被調用,在這個方法中,主要做了幾件事:
- 定義了 JS 代碼所在的位置,它在 dev 環境下是一個 URL,通過 development server 訪問;在生產環境下則從磁盤讀取,當然前提是已經手動生成過了 bundle 文件;
- 創建了一個 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 模塊信息表作為通信的基礎。由于對 iOS 端開發并不熟悉,文中如有錯誤的地方還請指出。
參考資料:
來自: http://taobaofed.org/blog/2015/12/30/the-communication-scheme-of-react-native-in-ios/