Turbo Modules是升級版的Native Modules,是基於JSI開發的一套JS與Native交互的輕量級框架,用來解決在使用Native Modules時遇到的問題。本篇博客主要對Turbo Modules和Native Modules進行了對比,並對Turbo Modules的實現進行了探究。除了介紹官方給出的優化點外,還通過具體示例對Turbo Modules與Native Modules的通信耗時進行了對比分析。 后續會以iOS視角,結合源碼補充JSI、Fabric等RN新架構中的實現原理。
下方是新舊架構種,NativeModule與TurboModule相關區別,下方會進行詳細展開。
二、為什么要推出Turbo Modules
1、Native Modules的缺點
下方是官方給出的Native Modules缺點,同時也是推出Turbo Modules的原因。
序號 |
總結 |
介紹 |
---|---|---|
1 | Native Modules不支持懶加載 |
在一個包中指定Native Modules有着更早的初始化時機。React Native的啟動時間隨着Native Modules的數量增加而增加,即使其中一些Native Modules從未使用過也會被創建。Native Modules還不能使用開源的LazyReactPackage進行懶加載,因為LazyReactPackage中ReactModuleSpecProcessor不能與Gradle一起運行,目前該問題尚未解決。 |
2 | Native Modules檢查JS與Native方法一致性較為困難 |
暫無簡單的方法可以檢查JavaScript調用的Native Modules是否在Native中被定義了。並且在熱更新時,暫無簡單的方式來檢查新版中JS代碼在調用Native Modules方法時入參是否正確。 |
3 | Native Modules以單例形式存在,其生命周期與橋關聯 |
Native Modules是以單例的形式存在,其生命周期與橋生命周期相關。該問題在Native與RN混編的APP中尤為明顯,因為RN橋可能會多次啟動和關閉。 |
4 | Native Modules的方法列表是在運行時進行掃描(多余的運行時操作) |
在啟動過程中,Native Modules通常被定義在多個包中。在運行時去遍歷,最終給出橋接的Native Modules列表而這些操作是完全不需要在運行時執行。 |
5 | Native Modules使用運行時的反射來實現的,完全可以放到編譯期來做 |
一個Native Module的方法和常量推斷是在運行時通過反射來實現的。這些操作完全可以放到編譯期。 |
2、Native Modules VS Turbo Modules
下方對Turbo Modules與Native Modules進行了對比,關於Turbos Modules相關的內容下方會詳細展開。
三、Turbos Module關鍵特性探究
本篇wiki中的示例是基於RN官方的“RCTSampleTurboModule”來展開分析,該示例中使用Turbo Modules在Native側定義導出一系列的方法,然后在JS側進行調用。其中有異步方法,也有同步方法,下方是核心代碼所在位置以及運行效果。



1、Turbo Modules執行過程概覽
首先通過官方示例來分析Turbo Modules的使用方式,在官方示例中創建了一個SampleTurboModule,在SampleTurboModule中導出了一系列的Native方法供JS使用,其功能與Native Modules所做的事情一致,但是其實現方式上有着本質區別,下方是相關調用過程。
Turbo Modules在使用過程中的調用流程:
-
JS側:首先在JS側可以通過import的形式來引入相關Turbo Modules,而在Turbo Modules聲明時,會創建JS側的方法接口,該接口中聲明了一些Turbo Modules橋接的方法。我們可以通過該接口定義,使用CodeGen來生成JSI側相關的調用方法,以及OC/Java側的方法接口,從而達到接口一致性的目的。
-
JSI&引擎層:自定義Turbo Modules需要實現JSI相關方法,可以將JSI相關方法與OC/Java方法進行映射,而這一步相關的方法也是由CodeGen自動生成。
-
Native側:在上層代碼(OC/Java)中,可以基於生成的接口來實現相關的橋方法,在JS側最終調用時,會執行該方法。
-
(1)、JS側對SampleTurboModule的使用
在JS側聲明SampleTurboModule時,會創建對應的自定義JS接口。使用時可以根據接口提供的方法來確定使用場景。而JSI層及OC/Java層對應的自定義Turbo Modules代碼,可以通過該接口生成對應的代碼及相關協議。稍后在CodeGen中會詳細介紹到。而本部分主要介紹模塊的注冊及使用。
模塊注冊:在JS側通過TurboModuleRegistry.getEnforcing方法對RCTSampleTurboModule模塊進行導出,並且聲明了一個Spec接口,其中包括了SampleTurboModule中聲明的相關方法。具體代碼如下:
1 /** 2 * Copyright (c) Facebook, Inc. and its affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 * 7 * @flow 8 * @format 9 */ 10 11 import type {UnsafeObject} from '../../Types/CodegenTypes'; 12 import type {RootTag, TurboModule} from '../RCTExport'; 13 import * as TurboModuleRegistry from '../TurboModuleRegistry'; 14 15 export interface Spec extends TurboModule { 16 // Exported methods. 17 +getConstants: () => {| 18 const1: boolean, 19 const2: number, 20 const3: string, 21 |}; 22 +voidFunc: () => void; 23 +getBool: (arg: boolean) => boolean; 24 +getNumber: (arg: number) => number; 25 +getString: (arg: string) => string; 26 +getArray: (arg: Array<any>) => Array<any>; 27 +getObject: (arg: Object) => Object; 28 +getUnsafeObject: (arg: UnsafeObject) => UnsafeObject; 29 +getRootTag: (arg: RootTag) => RootTag; 30 +getValue: (x: number, y: string, z: Object) => Object; 31 +getValueWithCallback: (callback: (value: string) => void) => void; 32 +getValueWithPromise: (error: boolean) => Promise<string>; 33 } 34 35 export default (TurboModuleRegistry.getEnforcing<Spec>( 36 'RCTSampleTurboModule', 37 ): Spec);
導入后,其使用方式與Native Modules使用一致,具體如下所示:
(2)、NativeSampleTurboModuleSpecJSI具體實現(C++實現 - 基於JSI提供JS可調用的方法)
下方使用C++編寫的NativeSampleTurboModuleSpecJSI即基於JSI為SampleTurboModule提供的具體實現類。該類繼承自ObjCTurboModule,而ObjCTurboModule繼承自TurboModule類,而TurboModule類繼承自HostObject類。該類是CodeGen自動生成。
而上述的JSI_EXPORT本質上是__attribute__((visibility("default")))的宏定義,該屬性用於設置動態鏈接庫中類的可見性。
#define JSI_EXPORT __attribute__((visibility("default")))
下方是NativeSampleTurboModuleSpecJSI類的構造函數中的具體實現,其中主要功能是使用methodMap將JS中的方法與JSI對應的方法實現進行關聯。而methodMap的key是JS側使用的方法名,value則是MethodMetadata對象,及JSI中聲明的方法。

上述在.h文件中進行了類的聲明,下方是.mm文件中的具體實現,以getString方法的具體實現為例。下方定義了一個名為__hostFunction_NativeSampleTurboModuleSpecJSI_getString的C++方法。該方法有一個類型為facebook::jsi::Value的返回值(Value是JS相關數據類型在JSI中的一個映射,JSI中關於Value的解釋:Represents any JS Value (undefined, null, boolean, number, symbol, string, or object). Movable, or explicitly copyable )。
在JS中每次通過SampleTurboModule調用getString方法,都會執行下面的方法,實際作用是調用Native側實現的getString,並返回相關值。

上述方法中四個參數如下所示:
-
&rt:第一個是當前JS的運行環境,Demo中使用的Hermes引擎,所以該參數為當前Hermes的運行時對象。
-
&turboModule:第二個參數是turboModule對象,即該示例自定義的SampleTurboModule對應的對象,也就是該方法屬於哪個Turbo Modules實例,如下所示。
-
*args:第三個參數就比較常規了,就是getString方法的入參。
-
count:則是該方法有幾個參數,此處的getString只有一個參數,那么Count = 1。
該方法實現中,調用了turboModule的invokeObjCMethod方法。invokeObjCMethod中傳入了getString方法的SEL,其中就會執行Objective-C中對應的getString方法,並把返回值返回出去。在invokeObjCMethod方法中,首先獲取當前Module的名稱和方法,然后開始打點。紅框中是關鍵代碼,如果是Promise,則創建對應的回調,否則直接調用performMethodInvocation執行相關方法。

-
(3)、Native橋接口(協議)聲明 -- NativeSampleTurboModuleSpec
CodeGen還會自動生成NativeSampleTurboModuleSpec的協議,該協議遵循了RCTBridgeModule和RCTTurboModule(后邊會詳細介紹),該接口中聲明的方法都是在JS側要調用的方法,也就是橋方法。
-
(4)、RCTSampleTurboModule的創建(Native側方法的具體實現)
基於NativeSampleTurboModuleSpec協議來創建自定義的Turbo Modules類RCTSampleTurboModule。在下方類聲明中的注釋信息中可以看出,該類100%與Native Modules系統進行兼容。在RCTSampleTurboModule類聲明時中遵循了RCTBridgeModule,在類的@implementation中實現了該協議中的相關方法,以及使用了RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD、RCT_EXPORT_METHOD的方式進行方法導出,目的在於兼容Native Modules。
上述代碼中的RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD、RCT_EXPORT_METHOD等方法完全是為了兼容Native Modules,如果在你的APP中沒必要兼容Native Modules,在僅僅使用Turbo Modules的情況下,完全可以把上述Export方法換成正常的Objective-C的實現。
除了上述的兼容方法外,在Turbo Modules的使用中比較關鍵的就是getTurboModule方法。該方法是RCTTurboModule協議中聲明的方法,目的在於獲取自定義的Turbo Modules對象。getTurboModule的方法實現比較簡單,就是調用了一個C++的庫函數來對NativeSampleTurboModuleSpecJSI類進行實例化。
2、Turbo Modules的注冊與調用
下方是Turbo Modules注冊及實例初始化的相關流程。Turbo Modules的注冊過程確切說是TurboModuleRegister初始化過程,並不會創建相關Turbo Modules對象。
Turbo Modules的注冊過程如下:
-
TurboModuleManager對象創建:首先在橋創建時,會調用RCTCxxBridge.start方法,在該方法中會創建TurboModuleManager對象。
-
TurboModuleBinding初始化:然后在TurboModuleManager中會調用installJSBindingWithRuntimeExecutor方法,來調用TurboModuleBinding的install方法進行初始化,並且將turboModuleProvider與TurboModuleBinding進行關聯。
-
JS側注入__turboModuleProxy方法:在TurboModuleBinding的Install中,動態的為JS側全局global添加了一個__turboModuleProxy方法,JS側可以調用__turboModuleProxy方法來獲取對應的Turbo Modules實例。
注冊器初始化后,可以在JS側調用相關__turboModuleProxy來獲取對象了,具體流程如下:
-
JS側調用方式:首先在JS側import TurboModule,然后會調用JS側TurboModuleRegistry的getEnforcing方法,最終通過全局變量global執行__turboModuleProxy。
-
獲取Turbo Modules實例:最終在Native側會執行provideTurboModule方法,該方法中主要分為三步----獲取緩存 -> 創建C++模塊實例 -> 創建ObjC/Java模塊實例,並將實例存入緩存。下方在介紹Turbo Modules的懶加載時會詳細介紹。
-
JS側獲取實例並調用相關方法:經過上述過程,在JS側可以獲取相關Turbo Modules對象然后調用相關方法。
平台 |
iOS |
Android |
---|---|---|
流程 |
|
|
在JS側會調用TurboModuleRegistry.getEnforcing方法來加載自定義的Turbo Modules,具體代碼如下所示:
1 export default (TurboModuleRegistry.getEnforcing<Spec>( 2 'SampleTurboModule', 3 ): Spec);
而上述調用最終會執行到requireModule(),在下述方法中首先通過Bridgeless來判斷是否支持Turbo Modules,如果不支持則返回同名的Native Modules。相反就會調用turboModuleProxy,而此處的__turboModuleProxy方法是通過global ( NodeJS.Global 類型,全局變量)獲取的。
而__turboModuleProxy方法則是在Native側通過運行時往JS的global上綁定的一個方法。而__turboModuleProxy方法則是通過JSI的形式注冊關聯到JS側的,最終會調用到Native側jsProxy方法,從調用棧上可看出在Native側的調用鏈為 jsProxy -> getModules -> provideTurboModule,provideTurboModule方法會返回對應的Module實例,如下圖所示:

以iOS側為例(Android實現方案類似,就不做過多贅述),provideTurboModule(入參為module name)方法中主要有三步:
-
第一步查找緩存:首先查找緩存,如果之前創建過對應的Turbo Modules對象,則直接返回。
-
第二步創建並返回對應的C++ modules:優先返回對應C++ Module。
-
第三步創建並返回平台指定的Module:最后是創建並返回平台指定側Module,此處是iOS系統,使用的ObjC,所以返回的是ObjCTurboModule,如果是安卓則返回JavaTurboModule。

3、Turbo Modules懶加載機制
當第一次對Turbo Module進行import調用時,上面的TurboModuleRegistry.getEnforcing方法才會執行,進而才會創建對應的Turbo Module實例對象並進行緩存。如果沒有對模塊進行import,那么對應的模塊將永遠不會初始化。
JS側首先讀取本地緩存,因為OC可以直接跟C++交互。在讀取緩存與創建C++對象時Java和OC有一些差異,OC可以直接創建C++實例,而Java必須通過C++創建,所以這里使用“Native側”統一表示。當緩存讀取失敗時,會創建一個純C++實例(pure-C++ Native Modules),在這里Android側代碼中沒有給出實現,iOS側有自己的實現,如果這里創建成功,會寫入緩存並且返回給JS側。當pure-C++實例沒有成功創建,就會創建JavaTurboModule/ObjcModule實例,因為Java實例不能直接被JS調用,因此Android側會額外創建一個C++實例包裹這個Java實例,然后將這個C++實例寫入緩存並返回。
4、Turbo Modules的創建與銷毀
上一部分對Turbo Modules的創建過程進行了重點介紹,該部分注重介紹Turbo Modules對象的銷毀過程(以iOS側為例):
-
Turbo Modules實例創建:在JS側調用Turbo Modules時會創建相關實例(懶加載),並且將創建好的實例存入CacheMap。
-
RCTBridge的創建:在RN示例中RCTRootView創建時,會創建RCTBridge相關實例。
-
RCTBridge的銷毀:當RCTRootView銷毀時,則會釋放RCTBridge實例。在RCTBridge釋放后,會發送橋銷毀的通知。
-
ModuleCacheMap清空:RCTTurboModuleManager對象收到通知后,會清空ModuleCacheMap。
Turbo Modules的生命周期也是與RCTBridge綁定的,當RCTBridge對象被釋放時,會發通知清除當前創建的Turbo Modules實例。在官方示例的AppDelete及RCTRootView創建時都會創建RCTBridge對象,也就是說Turbo Modules的生命周期是與RCTRootView的生命周期一致。具體分析如下:
-
在TurboModulesManager的,_invalidateModules方法中會對緩存進行清除,而_invalidateModules在收到bridge銷毀后的通知時調用。
-
RCTBridge在官方示例的AppDelete、RCTRootView、RCTSurfaceHostingProxyRootView(為了兼容RCTRootView,便於往Fabric中的RCTSurface上遷移)中都有初始化,所以當RCTRootView釋放時其對應的RCTBridge對象也會被釋放,此刻就會發通知然后清除緩存。而在AppDelete中的didFinishLaunching方法中,創建了RCTBridge對象,並將RCTBridge實例已參數的形式傳入了RCTRootView的構造方法中。所以在單bundle單頁面的情況下,每次退出頁面都會都模塊緩存Map進行清空。
-
經過代碼分析,開發過程中的Command + R也會對Turbo Modules的緩存進行清空。
5、接口一致性保障
(1)、Facebook官方工具(暫未正式公開對外使用)
CodeGen是一個開發工具,作用是靜態類型檢查器(Flow或TypeScript),目的是以自動化的形式來保證JS側與Native側的兼容性。用來解決之前檢查JS側接口與Native側接口一致性比較困難的問題。
The React Native team is also doubling down on the presence of a static type checker (either Flow or TypeScript) in the code. In particular, they are working on a tool called CodeGen to "automate" the compatibility between JS and the native side. By using the typed JavaScript as the source of truth, this generator can define the interface files needed by Fabric and TurboModules (elements of the new architecture that will be showcased in the third post) to send messages across the realms with confidence. This automation will speed up the communication too, as it’s not necessary to validate the data every time.
目前沒有找到官方關於介紹CodeGen使用的相關文檔,github上有人分享基於react-native-codegen生成代碼的工具,親測可用。(官方鏈接)
參考:
-
https://github.com/react-native-community/discussions-and-proposals/issues/92
-
https://github.com/facebook/react-native/tree/master/packages/react-native-codegen
(2)、微軟開源的react-native-tscodegen(可用)
除了上述FB的rn codegen,而微軟也開源了一款rn-tscodegin(Github地址),目的是根據TypeScript的接口,來生成Turbo Modules。在RN工程中親測可用。
四、Turbo Modules通信性能分析
官方相關文檔在介紹Turbo Modules的優化點時,沒有介紹其在通信過程中的優化點。本部分作為擴充,通過相關示例來探究Turbo Modules的通信過程中所做的事情。首先是線程切換上,其次是異步調用過程中的耗時探究。具體如下所示。
1、方法執行過程中的線程切換
-
同步調用:在Turbo Modules的同步方法調過程中沒有線程切換,都是在JS線程中完成的相關操作。所以如果在同步調用的方法中執行耗時操作勢必會造成JS線程阻塞,從而會影響其他JS線程的操作。
-
異步調用:而異步調用會有相關的線程切換,會將JS線程切換到主線程或者異步方法調用時指定的線程中,然后在相關線程中執行異步方法。執行回調時又會切換到JS線程中。經過相關Demo驗證,一次線程切換的操作耗時可忽略不計。
(1)、iOS側切換線程的過程
同步方法:Turbo Modules同步方法的調用過程不存在線程切換問題,依舊是在JS線程。
異步方法:在CallBack或者Promise方法執行時,會走到下方的方法中,該方法調用了dispatch_async,如果methodQueue,是主線程對應的隊列,那么就會切換到主線程中。
同步調用
異步調用:
在iOS側上述的methodQueue是在RCTBridgeModule代理的methodQueue方法提供,該方法會在橋定義時進行實現。方法中如果返回的是主隊列,那么就會切換到主線程。如果是創建的新隊列,則會創建一個新的線程。
(2)、Android側切換線程的過程
Android側的線程切換過程與iOS側大同小異,篇幅有限,就不做過多贅述。下方是安卓側線程切換相關流程。
2、異步調用耗時分析
同步調用無論在Native Modules和Turbo Modules中,執行過程都非常快(1~5ms),而異步橋的調用過程會慢一些(50 ~ 150ms之間)。本部分着重探究異步橋的調用耗時。
首先對Turbo Modules與Native Modules的異步橋調用進行了測試和分析,下方是相關數據,對應結果如下:
-
整體耗時:經過相關測試,雙端Turbo Modules的異步通信耗時與Native Modules的通信耗時優勢並不明顯,兩者不相上下。當然官方沒有明確給出Turbo Modules在橋調用過程中速度更快,而說Turbo Modules在加載過程中會有一些優化,並且基於JSI實現的,JS可直接通過JSI調用OC方法。整體耗時變化不明顯,也算符合預期。
-
JS to Native:在JS調用Native相關方法是,Turbo Modules因為是通過C++代碼之間調用Java/OC對應的方法,執行結果比較快,無論是Android還是iOS,都在1ms左右。而Native Modules因為通過消息隊列進行調用,性能會差一些,安卓在50ms左右,iOS在 20ms左右。
-
Native to JS:經過測試發現Turbo Module在Native to JS的過程中要比Native Module慢幾十毫秒,這點有點出乎意料。稍后會進行分析具體是什么地方耗時。
測試機型:
Android:Nokia X7 Android 9.0 4G+64G
iOS:iPhone 8 iOS12 2G+64G
測試場景:
異步橋調用
測試次數:
手動點擊100次,相關數據取平均值(Android & iOS的統計口徑及測試代碼邏輯保持一致)
平台 |
模塊 |
總耗時(ms) |
JS to Native(ms) |
Native to JS(ms) |
---|---|---|---|---|
Android |
Turbo Modules |
118.79 |
1.56 |
117.23 |
Native Modules |
126.05 |
46.1 |
79.93 |
|
iOS |
Turbo Modules |
83.62 |
1.05 |
82.61 |
Native Modules |
89.8 |
22.16 |
67.64 |
3、異步調用過程中CallBack的耗時分析
callback過程,即一次Native to JS的執行過程。通過調試可發現,最終是jsInvoke.invokeAsyn方法中的callback.call()方法耗時比較嚴重,該調用占據了整個Turbo Modules異步調用過程中的約95%的耗時。通過工具調試定位,具體執行方法的耗時落在了Hermes引擎中的相關方法的執行上(Native Modules也有同樣的問題)。
具體是Hermes引擎的哪些操作比較耗時?如何對其進行優化?最終能優化多少?在JSC和V8引擎上Turbo Modules表現如何?欲知后事如何,請聽下回分解。
strongWrapper->jsInvoker().invokeAsync([weakWrapper, responses, blockGuard]() { double start = [[NSDate new] timeIntervalSince1970] * 1000; auto strongWrapper2 = weakWrapper.lock(); if (!strongWrapper2) { return; } std::vector<jsi::Value> args = convertNSArrayToStdVector(strongWrapper2->runtime(), responses); strongWrapper2->callback().call(strongWrapper2->runtime(), (const jsi::Value *)args.data(), args.size()); strongWrapper2->destroy(); // Delete the CallbackWrapper when the block gets dealloced without being invoked. (void)blockGuard; double end = [[NSDate new] timeIntervalSince1970] * 1000; NSLog(@"Native Block End = %f",end - start ); });
從下方的分析過程中不難發現,一次CallBack過程中的操作耗時75ms,其中有73ms在Hermes引擎執行中。在調試過程中因為沒有加載Hermes源碼,具體耗時方法暫未定位,后續會繼續探索並嘗試給出相關優化方案, 具體調試過程:
五、總結
Turbo Modules的實現基於JSI,較Native Modules有明顯優勢。但是在異步橋調用的過程中,優勢並不明顯,而且有較大優化空間。具體總結如下:
-
Turbo Modules加載過程更優:Turbo Modules支持懶加載,所以在加載過程及生命周期上比Native Modules有明顯優勢。
-
Turbo Modules解決了接口一致性:Turbo Modules使用過程中,可通過JS側的接口生成C++中間層的JSI代碼,並且生成對應的OC/Java的接口。可以基於接口來實現Native方法,從而達到了JS - Native兩側的接口一致性。而Native Modules沒有相關機制來保證JS與Native側的接口一致性,所以往往會造成JS側調用的Native方法被刪除掉,進而造成Crash的情況。
-
Turbo Modules的方法加載效率更高:Turbo Modules的方法加載是編譯器就確定了,會經過JSI往OC/Java上映射相關的橋方法,在調用時直接執行。而Native Modules則是在運行時執行的,多余的運行時操作,影響性能。
-
Turbo Modules的JS to Native更快:無論是同步調用還是異步調用,JS to Native 調用過程中,Turbo Modules因為可以通過JSI直接調用OC/Java方法,所以其執行過程比。
-
Turbo Modules的Native to JS調用較Native Modules慢:實測,Turbo Modules在CallBack過程執行過程比Native Modules執行過程要長一些,表現不佳,具體原因分析中。聲明: