react native js 與 native 的通信與交互方式


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 。它的調用過程為: 

  1. ReactPackager.createClientFor
  2. client.buildBundle
  3. processBundle
  4. saveBundleAndMap

上面四步完成的是 buildBundle 的功能,細節很多很復雜。總體來說,buildBundle 的功能類似於 browerify 或 webpack : 

  1. 從入口文件開始分析模塊之間的依賴關系;
  2. 對 JS 文件轉化,比如 JSX 語法的轉化等;
  3. 把轉化后的各個模塊一起合並為一個 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 模塊信息表作為通信的基礎。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM