React Native原理之跨端通訊機制

雲音樂技術團隊發表於2022-02-28

圖片來源:https://unsplash.com/photos/g...

作者:五廿

跨端通訊

在移動端開發場景中,能使用一份程式碼就能同時在安卓和 iOS 系統上執行 APP 的方案,熟稱為跨端方案。而 Webview ,React Native 都是雲音樂大前端團隊用的比較多的跨端方案,這些方案雖然能提高開發效率,但它們不能像原生語言一樣直接呼叫系統的能力,於是在做 HTML5(以下簡稱 H5) 或者 React Native(以下簡稱 RN) 需求的時候,開發者們經常碰到要呼叫 Native 能力的情況。Native 能力用原生語言編寫,有自己的執行環境,RN 頁面使用 JS 編寫,也有獨立的執行環境,這種跨越執行環境的呼叫被稱為跨端通訊

webView jsb

H5 中的跨端通訊稱為 JSBridge,在進行一次 JSBridge 呼叫的時候會攜帶呼叫引數,預設有 4 個引數:

ModuleId: 模組 ID
MethodId: 方法 ID
params: 引數
CallbackId: JS 回撥名

其中 ModuleIdMethodId 能定位到具體呼叫的原生方法,params 引數作為原生方法呼叫的引數,最後通過 CallbackId 回撥 JS 的回撥函式,H5 就能從回撥函式中拿到呼叫結果。該流程中主要使用了 Webview 容器中攔截請求客戶端呼叫 JS 函式的能力,比如安卓中通常使用的是 WebChromeClient.onJsPrompt 方法來攔截 H5 的請求,evaluateJavascript 方法用來執行回撥。但是 React Native 中沒有引入 Webview 來實現這些呼叫的能力,它採用了完全不同的方式來處理。另外,在雲音樂團隊的 APP 中, 會同時存在 H5 和 RN 頁面,也就是同一個 APP 中兩種跨端通訊方式並存,但它們最後呼叫的原生方法卻是來自同一個原生模組。本文主要從 Android 系統的 RN 實現來介紹 RN 的通訊機制和橋接能力(以下簡稱 Bridge),並結合以上通訊場景中會碰到的問題來講解如何實現一個業務中可用的 Bridge。大體由三部分組成,首先介紹 RN 中不同的組成模組和它們各自的角色;第二部分是各個模組之間的呼叫方式和具體的示例;最後一部分探討業務中的 Bridge 的實現。

RN 組成

在 RN 中,主要有三個重要的組成模組:平臺層( Android 或者 OC 環境),橋接層( C++ )和JS 層

  • 平臺層負責原生元件的渲染和提供各式各樣的原生能力,由原生語言實現;
  • 橋接模組負責解析 JS 程式碼,JS 和 Java/OC 程式碼互調,由 C++ 語言實現;
  • JS 層負責跨端頁面具體的業務邏輯。

rn 通訊模組

相比起 Webview 的結構來說,RN 的結構多了一層橋接層,也就是 C++ 層。文章先來介紹一下這個模組的作用,以及為什麼會多出這麼一個模組。

橋接層(C++ 層)

React Native 和 H5 一樣,使用了 JS 作為跨端頁面的開發語言,因此它必須要有一個 JS 執行引擎,而在使用 H5 的情況下,Webview 是 JS 的執行引擎,同時 Webview 還是頁面的渲染引擎。RN 不一樣的地方在於,已經有了自己的渲染層,這個功能交給了 Java 層,因為 RN 的 JS 元件程式碼最後都會渲染成原生元件。因此 RN 只需要一個 JS 執行引擎來跑 React 程式碼。 RN 團隊選擇了 JSCore 作為 JS 的執行引擎,而 JSCore 的對外介面是用 C 和 C++ 編寫的。因此平臺層的 Java 程式碼 / OC 程式碼想要通過 JSCore 拿到 JS 的模組和回撥函式,只能通過 C++ 提供的介面獲取,再加上 C++ 在 iOS 和安卓系統上也有良好的跨端執行的功能,選它作為橋接層是不錯的選擇。

JSCore

JSCore 是橋接層中的主要模組,它是 RN 架構中的 JS 引擎,負責 JS 程式碼的載入和解析。先來看下它的主要 API :

JSContextGetGlobalObject:獲取JavaScript執行環境的Global物件。
JSObjectSetProperty/JSObjectGetProperty:JavaScript物件的屬性操作:set和get。
JSEvaluateScript:在JavaScript環境中執行一段JS指令碼。
JSObjectCallAsFunction:在JavaScript環境中呼叫一個JavaScript函式

通過 API 可以看出來,開發者可以用 JSEvaluateScript 在 JSCore 環境中執行一段 JS 程式碼,也可以通過 JSContextGetGlobalObject 拿到 JS 上下文的 Global 變數,然後把它轉化成 C++ 可以使用的資料結構並且操作它,注入 API。而 JSObjectSetPropertyJSContextGetGlobalObject 也是比較重要的兩個 API ,稍後會在通訊流程中發揮作用。

Native 模組和 JavaScript 模組

說起通訊的話,整個過程肯定存在信源和信宿,也就是訊息的傳送者和接收者,在 RN 的通訊中,它們是 Native 和 JS 的模組,它們向對方提供能力都是以模組為功能單位的,類似 JSBridge 協議中的 ModuleID 的概念。

  • Native 模組在 Android 系統下是 Java 模組,由平臺程式碼實現,JS 通過模組 ID(moduleID) 和方法 ID(methodID) 來進行呼叫,一般都在 RN 原始碼工程的 java/com/facebook/react/modules/ 目錄下,可以給 RN 頁面開放原生系統的能力,如計時器的實現模組 Timing,給 JS 程式碼提供計時器的能力。
  • JavaScript 模組是由 JS 實現,程式碼在 /Libraries/ReactNative/ 目錄下,如 App 啟動模組 AppRegistery ,對 Java 環境來說,作用是提供操作 JS 環境的 API,如回撥,廣播等。Java 的呼叫方法是通過 JS 暴露出來的 callFunctionReturnFlushedQueue API。

JS 環境中會維護一份所有 Native 模組的 moduleID 和 methodID 的對映 NativeModules,用來呼叫 Native 模組的時候查詢對應 ID;Java 環境中也會維護一份 JavaScript 模組的對映 JSModuleRegistry,用來呼叫 JS 程式碼。而在實際的程式碼中,Native 模組和 JS 模組的通訊需要通過中間層也就是 C++ 層的過渡,也就是說 Native 模組和 JS 模組實際上都只是在和 C++ 模組進行通訊。

C++ 和 JS 通訊

上面提到,JSCore 可以讓 C++ 拿到 JS 執行環境的 global 物件並能操作它的屬性,而 JS 程式碼會在 global 物件中注入一些原生模組需要的 API,這是 JS 向 C++ 提供操作 API 的主要方式。

  • RN 環境中 JS 會在 global 物件中設定了 __fbBatchedBridge 變數,並在變數塞入了 4 個的 API,作為 JS 被呼叫的入口,主要 API 包括:

<!---->

callFunctionReturnFlushedQueue // 讓 C++ 呼叫 JS 模組
invokeCallbackAndReturnFlushedQueue // 讓 C++ 呼叫 JS 回撥
flushedQueue // 清空 JS 任務佇列
callFunctionReturnResultAndFlushedQueue // 讓 C++ 呼叫 JS 模組並返回結果
  • JS 還在 global 中還設定了 __fbGenNativeModule 方法,用來給 C++ 呼叫後在 JS 環境生成 Java 模組的對映物件,也就是 NativeModules 模組。它的資料結構類似於(跟實際的資料結構有偏差):

<!---->

{
    "Timing": {
        "moduleID": "1001",
        "method": {
            "createTimer": {
                "methodID": "10001"
            }
        }
    }
}
  • 通過 NativeModules 的對映,開發者能拿到呼叫模組和方法的 moduleIDmethodID ,在呼叫過程中會對映到具體的 Native 的方法。

同樣的,C++ 通過 JSCore 的 JSObjectSetProperty 方法在 global 物件中塞入了幾個 Native API,讓 JS 能通過它們來呼叫 C++ 模組。主要 API 有:

nativeFlushQueueImmediate // 立即清空 JS 任務佇列
nativeCallSyncHook // 同步呼叫 Native 方法
nativeRequire  // 載入 Native 模組
  • 上面介紹 API 的時候,有多個 API 的功能比較類似,就是清空 JS 的任務佇列,那是因為 JS 在呼叫 Native 模組是非同步呼叫,它會把呼叫引數包裝成一個呼叫任務放入 JS 任務佇列 MessageQueue 中,然後等待 Native 的呼叫。呼叫時機一般是在觸發事件的時候,事件會觸發 Native 回撥 JS 的回撥函式,Native 模組需要通過 __fbBatchedBridge 的四個 API 回撥 JS 程式碼,而這四個 API,都有 flushedQueue 功能:清空任務佇列並執行所有的任務,藉此來消費佇列中的 Native 呼叫任務。但是如果某一次呼叫距離上一次的 flushedQueue 行為有點久(一般是大於 5 ms),就會觸發立即呼叫的邏輯,JS 呼叫 nativeFlushQueueImmediate API,主動觸發任務消費。

平臺(Java)和 C++ 的通訊

Java 跟 C++ 的互相呼叫通過 JNI(Java Native Interface),通過 JNI,C++ 層會暴露出來一些 API 來給 Java 層呼叫,來讓 Java 能跟 JS 層進行通訊。下面是 C++ 通過 JNI 暴露給 Java 的一些方法:

initializeBridge // 初始化:C++ 從 Java 拿到 Native 模組,作為引數傳給 JS 生成 NativeModules
jniLoadScriptFromFile // 載入 JS 檔案
jniCallJSFunction // 呼叫 JS 模組
jniCallJSCallback// 呼叫 JS 回撥
setGlobalVariable // 編輯 global 變數
getJavaScriptContext // 獲取 JS 執行環境
  • 由上面的 API 基本可以判斷出,C++ 負責的是一些中間層的角色,有 JS 的載入,解析的工作,還有提供操作 JS 執行環境的 API;
  • 這裡操作 JS 的 API 都會走到上一節 __fbBatchedBridge 的四個 API 上,如 jniCallJSFunction 會呼叫 callFunctionReturnFlushedQueuejniCallJSCallback 會呼叫 invokeCallbackAndReturnFlushedQueue。由此,三個模組的呼叫鏈路就連線了起來。

呼叫示例

以 RN 中的 setTimeout 方法為例,走一遍呼叫流程。

  • 初始化過程

RN setTimeout 初始化

  • Timing Class:Native 中的延時呼叫的實現類,被 @reactModule 裝飾器描述為一個 Native 模組,在 RN 初始化的時候被放入 ModuleRegistry 對映表,用於後面的呼叫對映。
  • ModuleRegistry 對映表構造完成後,呼叫 C++ 的 initializeBridge ,把 ModuleRegistry 的模組通過 \__fbGenNativeModule 函式註冊進 JS 環境。
  • JS 程式碼中的 JSTimer 類 引用 Timing 模組的 createTimer 來實現 setTimeout,延遲執行函式。

     // 原始碼位置:/Libraries/Core/Timers/JSTimers.js
     const {Timing} = require('../../BatchedBridge/NativeModules');
    
     function setTimeout(func: Function, duration: number, ...args: any): number {
        // 建立回撥函式
        const id = _allocateCallback(
            () => func.apply(undefined, args),
            'setTimeout',
        );
        Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false);
        return id;
    },
    
  • setTimeout 的呼叫過程

RN setTimeout 呼叫

  • 當 setTimeout 在 JSTimer.js 被呼叫,通過 NativeModules 找到 Timing Class 的 moduleID 和 methodID,放進任務佇列 MessageQueue 中;
  • Native 通過事件或者主動觸發清空 MessageQueue 佇列,C++ 層把 moduleID ,methodID 和其他呼叫引數交給 ModuleRegistry ,由它來找到 Native 模組的程式碼,Timing 類;
  • Timing 呼叫 createTimer 方法,呼叫系統計時功能實現延遲呼叫;
  • 計時結束,Timing 類需要回撥 JS 函式

    // timerToCall 是回撥函式的 ID 陣列
    getReactApplicationContext().getJSModule(JSTimers.class)
        .callTimers(timerToCall);
    
  • getJSModule 方法會通過 JSModuleRegistry 找到需要呼叫的 JS 模組,並呼叫對應的方法,該流程中呼叫 JSTimers 模組的 callTimers 方法。
  • Java 程式碼通過 JNI 介面 jniCallJSFunction 通過 C++ 呼叫 JS 模組,並傳入 module:JSTimers 和 method:callTimers
  • C++ 呼叫 JS 暴露出來的 callFunctionReturnFlushedQueue API,帶上 module 和 method,回到 JS 的呼叫環境;
  • JS 執行 callFunctionReturnFlushedQueue 方法找到 RN 初始化階段註冊好的 JSTimer 模組的 callTimers 函式,進行呼叫。呼叫完畢後清空一下任務佇列 MessageQueue

RN 的 JSBridge

以上通過 RN 的 setTimeout 函式走了一遍 RN 內 Java 程式碼和 JS 程式碼的通訊流程。簡單來說,Java 模組和 JS 模組可以通過 NativeModules 和 JS 回撥函式互相呼叫,來達成一次跨端呼叫。但是業務中的 Bridge 需要包含一些額外的場景,比如併發呼叫,事件監聽等。

  • 併發呼叫:類似於在 web 端同時發多個請求,為了將請求結果回撥到正確的回撥函式內,需要儲存一個請求到回撥函式的對映,在 Bridge 的呼叫中也是一樣的。而這份對映可以維護在 JS 程式碼中,也可以維護在 Native 程式碼中,在跨端方案中,兩者都可行的情況下一般選擇 JS 程式碼的方案來保持靈活性,Native 只負責處理結果並回撥。
  • 事件監聽:比如 JS 程式碼監聽頁面是否切換到後臺,同一個回撥函式在頁面多次切換到後臺的時候,應該要被呼叫多次,但是 RN 的 JSCallback 只允許呼叫一次(每一個 callback 例項會帶上是否呼叫過的標記), 回撥顯然不適合這種場景,雲音樂的 Bridge 使用 RN 的事件通知:RCTDeviceEventEmitter 來代替回撥。RCTDeviceEventEmitter 是一個純 JS 實現的事件訂閱分發模組,Native 模組通過 getJSModule 可以拿到它的方法,因此可以在 Native 端發出一個 JS 事件並帶上回撥的引數和對映 ID 等,而不用走 JSCallback。

回到之前的問題:如何實現 RN 的 Bridge,能讓一個 Bridge 的 API 同時支援 H5 和 RN 的呼叫。因為 H5 和 RN 大多數的業務場景都是相同的,比如獲取使用者資訊 user.info,裝置資訊 device.info 類似的介面,在 H5 和 RN 中都是會用到的。除了跨端呼叫的協議要保持一致外,具體的實現模組,協議解析模組都是可以複用的。其中不一樣的就是呼叫鏈路。RN 鏈路中的主要模組包括:

  • 給 JS 程式碼呼叫的 NativeModule,作為呼叫入口,JS 程式碼呼叫它暴露出來的方法傳入呼叫引數並開始呼叫流程,但是該模組不解析協議和引數,可以稱作 RNRPCNativeModule;
  • 在 Native 模組處理完後,RNRPCNativeModule 使用 RCTDeviceEventEmitter 生成一個事件回撥給 JS 程式碼,並帶上執行結果。

除了以上兩個不一樣的模組外,其他模組都是可以複用的,如協議解析和任務分發模組,解析協議的呼叫模組,方法,引數等,並把它分發給具體的 Native 模組;還有 Native 具體的功能實現模組,都可以保持一致。

結合前面介紹的呼叫流程,開發者如果呼叫 User.info 這個 JSBridge 來獲取使用者資訊,呼叫流程如下:
User.info 呼叫

這樣的處理,能保證 H5 和 RN 能用同一份 moduleID 和 methodID 來呼叫 Native 的功能,而且保證在同一個模組進行處理。從開發者的角度來看,就是一個 Bridge 的 API 可以同時支援 H5 和 RN 的呼叫。

以上。

相關資料

本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!

相關文章