隨着後起之秀 Flutter 的崛起,RN 漸漸失去光環。雖然有一天 RN 可能會退出歷史的舞臺,但它帶來 JavaScript 與 Native 交互的思想依然會流傳下去。小程序就借鑑了這種思想。javascript
網上關於 RN 通訊原理的文章,幾乎都是站在客戶端的角度來說解,這篇文章想站在前端的角度聊一聊,JS 與 Native 是如何交互的。前端
若是您想閱讀 RN 的源碼,建議不要選擇最新的版本,新版本對部分底層代碼採用 C++ 進行了重寫,閱讀和調試體驗都不是很好。而在通訊方式上其實沒有多大變化。java
我閱讀的 RN 版本是 0.43.4,基於該版本寫了一個可運行的 Demo,僅包含 JavaScript 和 Objective-C 通訊的核心部分,只有幾百行代碼。ios
JS 端通訊部分的核心代碼不到兩百行,主要由 NativeModules.js
、MessageQueue.js
、BatchedBridge.js
三個文件構成。其中 BatchedBridge 是 MessageQueue 實例化的對象。git
我把它們都放在 Demo 工程的Arch.js
文件中。BatchedBridge 對象提供了 JS 觸發 Native 調用的方法:github
var enqueueNativeCall = function(moduleID, methodID, params, onFail, onSuccess) {
if (onFail || onSuccess) {
// 若是存在 callback 回調,添加到 callbacks 字典中
// OC 根據 callbackID 來執行回調
if (onFail) {
params.push(this.callbackID);
this.callbacks[this.callbackID++] = onFail;
}
if (onSuccess) {
params.push(this.callbackID);
this.callbacks[this.callbackID++] = onSuccess;
}
}
// 將 Native 調用存入消息隊列中
this.queue[MODULE_INDEX].push(moduleID);
this.queue[METHOD_INDEX].push(methodID);
this.queue[PARAMS].push(params);
// 每次都有 ID
this.callID++;
const now = new Date().getTime();
// 檢測原生端是否爲 global 添加過 nativeFlushQueueImmediate 方法
// 若是有這個方法,而且 5ms 內隊列還有未處理的調用,就主動調用 nativeFlushQueueImmediate 觸發 Native 調用
if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
global.nativeFlushQueueImmediate(this.queue);
// 調用後清空隊列
this.queue = [[], [], [], this.callID];
}
}
複製代碼
該函數將調用 Native 實例模塊 ID,方法 ID,以及回調 ID 分別存入三個隊列中,this.queue 持有這三個隊列。小程序
if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
global.nativeFlushQueueImmediate(this.queue);
// 調用後清空隊列
this.queue = [[], [], [], this.callID];
}
複製代碼
這段代碼是 JS 主動觸發 Native 調用的關鍵所在,它有一個條件當 now - this.lasFlush > 5ms
時才執行。也就是隊列上一次被清空的時間若是已經超過 5ms 就執行nativeFlushQueueImmediate
函數。api
在下一節 Native call JS 咱們將會講到每次 Native 調用 JS 時,會將 queue 做爲返回值傳給 Native 執行,假設沒有在 5ms 內沒有 Native call JS,那麼 JS call Native 都得不到執行。數組
因此這裏設定了一個 5ms 的門限,若是在這段時間內,沒有 Native 調用 JS,JS 就會主動觸發 Native 調用。多線程
在 global 的 nativeFlushQueueImmediate
函數體,是在原生端實現的。執行時會觸發原生端 block 的調用,並傳入參數 queue,觸發 native 調用。
self->_context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls) {
AHJSExecutor *strongSelf = weakSelf;
[strongSelf->_bridge handleBuffer:calls];
};
複製代碼
如今的問題是,JS 如何知道 Native 有哪些方法能夠調用的呢?是 Native 在開始執行 JS 代碼前,提早注入到 JS 環境的,保存在 global 的__batchedBridgeConfig
屬性中。
它包含全部支持 JS 調用的模塊和方法,同時會區分每一個方法是同步、異步、仍是 Promise,這些信息在 Native 初始化時會提供。
每一個模塊和方法,都會關聯一個 ID,這個 ID 實際上是模塊和方法在各自列表中所處的下標。發起調用時,JS 端將 ID 存入消息隊列,Native 拿到 ID,在 Native 端的配置表中,找到對應的原生類(實例)和方法,並進行調用。
對象和方法約定好了,還須要約定參數,將參數值按順序放入一個數組中。使用者須要注意參數的個數和順序,保持與 Native 端的方法匹配,否者會報錯。
最後是 JS callback 的處理,JS 和 Native 通訊是沒法傳遞事件的,因此選擇將事件序列化,給每一個 callback 一個 ID,本身存一份,再將 ID 傳給 Native,當 Native 要執行這個回調時,經過 invokeJSCallback
函數把這個 ID 回傳給 JS,JS 再根據 ID 查找對應的 callback 並執行。
Native call JS 依賴於 JavaScriptCore,該框架提供建立 JS 上下文環境,以及執行 JS 代碼的接口,相對來講直接不少,不過由於 Native 端是多線程的環境,因此須要分狀況來討論,主要能夠分爲三種:
同步調用的場景很是少,由於它僅限於在 JS 線程調用,而實際狀況是,Native 和 JS 的通訊幾乎都是跨線程的。由於頁面刷新和事件回調都發生在主線程。
對於 Native 端來講,JS 線程是普通的一個線程,跟其餘線程沒有區別,只不過是用這個線程來初始化 JS 的上下文環境,以及執行 JS 代碼。
同步調用支持有返回值,而異步調用的 api 是沒有返回值的,只能使用 callback。
Native 異步調用 JS 主要由callFunctionReturnFlushedQueue
函數分發:
var callFunctionReturnFlushedQueue = function(module, method, args) {
this.__callFunction(module, method, args);
return this.flushedQueue();
}
複製代碼
該函數定義在BatchedBridge
對象,並由 global 的__batchedBridge
屬性所持有。
Native 在調用時把 moduleName 和各參數值都放在一個數組中,使用 JSValue 包裝,再經過 JavaScriptCore 提供的JSObjectCallAsFunction
函數觸發 JS 調用。
這裏跟 JS call Native 不同的是,不須要使用 ID,而是直接執行的。
在上述方法中 return 了this.flushedQueue()
,它是前面提到的清理 JS call Native 消息隊列,將隊列中的信息返回給 Native 執行。
Native 調用 JS 若是有返回值,會有兩種形式,一種是等待 JS 方法執行完成,拿到 return 的返回值;另外一種是不等待 JS 方法執行完成,JS 執行完後經過 callback 回調給調用方。
其實不論是異步仍是同步 Native 都是經過下面的方法執行 JS 調用的:
- (void)_executeJSCall:(NSString *)method
arguments:(NSArray *)args
unwrapResult:(BOOL)unwrapResult
callback:(AHJSCallbackBlock)onComplete
複製代碼
若是該方法是在 JS 線程調用的,那麼會同步返回返回值;若是是其餘線程調用該方法,返回值是經過異步 callback 返回的。
最後是異步調用 JS callback 的狀況,其實跟異步調用相似,只是在 JS 端定了一個新的函數:
var invokeCallbackAndReturnFlushedQueue = function(callbackId, args) {
this.__invokeCallback(callbackId, args);
return this.flushedQueue();
}
複製代碼
接受一個 callbackID,找到對應的 callback 並執行。
那麼 Native 是怎麼知道 JS 有哪些模塊能夠調用的呢?其實這隻需 RN 框架內部定義就好,對於使用 RN 開發,主要是在 JS 端,知道 Native 有哪些功能提供給 JS 調用就好。
本質緣由是 JS 是單線程執行的。而 Native 端負責 UI 展現的又只能是主線程,誇線程通訊若是使用同步可能會阻塞 UI。
爲了提升性能,批量處理 JS 的 Native 調用,能夠減小 JS 與 RN 通訊的頻率,是一種函數節流的方案。
感謝閱讀。若是你對實現細節感興趣,能夠看一看我寫的 Demo。