Web 開發有一個經典問題:「瀏覽器中從輸入 URL 到頁面渲染的這個過程當中都發生了什麼?」javascript
據我考據這個問題起碼有十年歷史了。在突飛猛進學不動的前端圈子裏,這個問題能一直被問,就是由於由於它是個很是好的問題,涉及很是多的知識點,平時作一些性能優化,均可以從這個問題出發,分析性能瓶頸,而後對症下藥進行優化。html
不過今天咱們不談 Web 的性能優化,只是藉助剛剛的那個那個經典問題的分析思路,從 React Native 的啓動到頁面的第一次渲染完成,結合 React Native 的源碼和 1.0 的新架構,一一分析 React Native 的啓動性能優化之路。前端
若是你喜歡個人文章,但願點贊👍 收藏 📁 評論 💬 三連支持一下,謝謝你,這對我真的很重要!
閱讀提醒:
1.文章中的源碼內容爲 RN 0.64 版本
2.源碼分析內容涉及Objective-C
、Java
、C++
、JavaScript
四門語言,我儘可能講得通俗易懂一些,若實在不理解能夠直接看結論
React Native 做爲一個 Web 前端友好的混合開發框架,啓動時能夠大體分爲兩個部分:java
其中 Native 容器啓動在現有架構(版本號小於 1.0.0)裏:大體能夠分爲 3 個部分:react
容器初始化後,舞臺就交給了 JavaScript,流程能夠細分爲 2 個部分:ios
最後 JS Thread 把計算好的佈局信息發送到 Native 端,計算 Shadow Tree,最後由 UI Thread 進行佈局和渲染。git
關於渲染部分的性能優化能夠見我以前寫的 《React Native 性能優化指南》,我從 渲染、 圖片、 動畫、 長列表等方向介紹了 RN 渲染優化的常見套路,感興趣的讀者能夠前往查看,我這裏就很少介紹了。
上面的幾個步驟,我畫了一張圖,下面我以這張圖爲目錄,從左向右介紹各個步驟的優化方向:github
提示:React Native 初始化時,有可能多個任務 並行執行,因此上圖只能表示 React Native 初始化的大體流程,並不和實際代碼的執行時序一一對應。
想提高 React Native 應用的性能,最一勞永逸的方法就是升級 RN 的大版本了。咱們的應用從 0.59 升級到 0.62 以後,咱們的 APP 沒有作任何的性能優化工做,啓動時間直接縮短了 1/2。當 React Native 的新架構發佈後,啓動速度和渲染速度都會大大增強。react-native
固然,RN 的版本升級並不容易(橫跨 iOS Android JS 三端,兼容破壞性更新),我以前寫過一篇《React Native 升級指南(0.59 -> 0.62)》的文章,若是有升級想法的老鐵能夠閱讀參考一下。瀏覽器
容器的初始化確定是從 APP 的入口文件開始分析,下面我會挑選一些關鍵代碼,梳理一下初始化的流程。
AppDelegate.m
是 iOS 的入口文件,代碼很是精簡,主要內容以下所示:
// AppDelegate.m - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 1.初始化一個 RCTBridge 實現加載 jsbundle 的方法 RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; // 2.利用 RCTBridge 初始化一個 RCTRootView RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"RN64" initialProperties:nil]; // 3.初始化 UIViewController self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; // 4.將 RCTRootView 賦值給 UIViewController 的 view rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; return YES; }
總的來看入口文件就作了三件事:
RCTBridge
實現加載 jsbundle 的方法RCTBridge
初始化一個 RCTRootView
RCTRootView
賦值給 UIViewController
的 view 實現 UI 的掛載從入口源碼咱們能夠發現,全部的初始化工做都指向 RCTRootView
,因此接下來咱們看看 RCTRootView
幹了些啥。
咱們先看一下 RCTRootView
的頭文件,刪繁就簡,咱們只看咱們關注的一些方法:
// RCTRootView.h @interface RCTRootView : UIView // AppDelegate.m 中用到的初始化方法 - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initialProperties:(nullable NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;
從頭文件看出:
RCTRootView
繼承自 UIView
,因此它本質上就是一個 UI 組件;RCTRootView
調用 initWithBridge
初始化時要傳入一個已經初始化的 RCTBridge
在 RCTRootView.m
文件裏,initWithBridge
初始化時會監聽一系列的 JS 加載監聽函數,監聽到 JS Bundle 文件加載結束後,就會調用 JS 裏的 AppRegistry.runApplication()
,啓動 RN 應用。
分析到這裏,咱們發現 RCTRootView.m
只是實現了對 RCTBridge
的的各類事件監聽,並非初始化的核心,因此咱們就又要轉到 RCTBridge
這個文件上去。
RCTBridge.m
裏,初始化的調用路徑有些長,全貼源碼有些長,總之最後調用的是 (void)setUp
,核心代碼以下:
- (Class)bridgeClass { return [RCTCxxBridge class]; } - (void)setUp { // 獲取bridgeClass 默認是 RCTCxxBridge Class bridgeClass = self.bridgeClass; // 初始化 RTCxxBridge self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self]; // 啓動 RTCxxBridge [self.batchedBridge start]; }
咱們能夠看到,RCTBridge
的初始化又指向了 RTCxxBridge
。
RTCxxBridge
能夠說是 React Native 初始化的核心,我查閱了一些資料,貌似 RTCxxBridge
曾用名爲 RCTBatchedBridge
,因此能夠粗暴的把這兩個類當成一回事兒。
由於在 RCTBridge
裏調用了 RTCxxBridge
的 start
方法,咱們就從 start
方法來看看作了些什麼。
// RTCxxBridge.mm - (void)start { // 1.初始化 JSThread,後續全部的 js 代碼都在這個線程裏面執行 _jsThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoop) object:nil]; [_jsThread start]; // 建立並行隊列 dispatch_group_t prepareBridge = dispatch_group_create(); // 2.註冊全部的 native modules [self registerExtraModules]; (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO]; // 3.初始化 JSExecutorFactory 實例 std::shared_ptr<JSExecutorFactory> executorFactory; // 4.初始化底層 Instance 實例,也就是 _reactInstance dispatch_group_enter(prepareBridge); [self ensureOnJavaScriptThread:^{ [weakSelf _initializeBridge:executorFactory]; dispatch_group_leave(prepareBridge); }]; // 5.加載 js 代碼 dispatch_group_enter(prepareBridge); __block NSData *sourceCode; [self loadSource:^(NSError *error, RCTSource *source) { if (error) { [weakSelf handleError:error]; } sourceCode = source.data; dispatch_group_leave(prepareBridge); } onProgress:^(RCTLoadingProgress *progressData) { } ]; // 6.等待 native moudle 和 JS 代碼加載完畢後就執行 JS dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ RCTCxxBridge *strongSelf = weakSelf; if (sourceCode && strongSelf.loading) { [strongSelf executeSourceCode:sourceCode sync:NO]; } }); }
上面代碼比較長,裏面用到了 GCD 多線程的一些知識點,用文字描述大體是以下的流程:
_jsThread
native modules
js
和 Native
之間的橋和 js 運行環境RCTMessageThread
,初始化 _reactInstance
其實上面的六個點均可以深挖下去,可是本節涉及到的源碼內容到這裏就能夠了,感興趣的讀者能夠結合我最後給出的參考資料和 React Native 源碼深挖探索一下。
和 iOS 同樣,啓動流程咱們先從入口文件開始分析,咱們先看 MainActivity.java
:
MainActivity
繼承自 ReactActivity
,ReactActivity
又繼承自 AppCompatActivity
:
// MainActivity.java public class MainActivity extends ReactActivity { // 返回組件名,和 js 入口註冊名字一致 @Override protected String getMainComponentName() { return "rn_performance_demo"; } }
咱們再從 Android 的入口文件 MainApplication.java
開始分析:
// MainApplication.java public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { // 返回 app 須要的 ReactPackage,添加須要加載的模塊, // 這個地方就是咱們在項目中添加依賴包時須要添加第三方 package 的地方 @Override protected List<ReactPackage> getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List<ReactPackage> packages = new PackageList(this).getPackages(); return packages; } // js bundle 入口文件,設置爲 index.js @Override protected String getJSMainModuleName() { return "index"; } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); // SoLoader:加載C++底層庫 SoLoader.init(this, /* native exopackage */ false); } }
ReactApplication
接口很簡單,要求咱們建立一個 ReactNativeHost
對象:
public interface ReactApplication { ReactNativeHost getReactNativeHost(); }
從上面的分析咱們能夠看出一切指向了 ReactNativeHost
這個類,下面咱們就看一下它。
ReactNativeHost
主要的工做就是建立了 ReactInstanceManager
:
public abstract class ReactNativeHost { protected ReactInstanceManager createReactInstanceManager() { ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START); ReactInstanceManagerBuilder builder = ReactInstanceManager.builder() // 應用上下文 .setApplication(mApplication) // JSMainModulePath 至關於應用首頁的 js Bundle,能夠傳遞 url 從服務器拉取 js Bundle // 固然這個只在 dev 模式下可使用 .setJSMainModulePath(getJSMainModuleName()) // 是否開啓 dev 模式 .setUseDeveloperSupport(getUseDeveloperSupport()) // 紅盒的回調 .setRedBoxHandler(getRedBoxHandler()) .setJavaScriptExecutorFactory(getJavaScriptExecutorFactory()) .setUIImplementationProvider(getUIImplementationProvider()) .setJSIModulesPackage(getJSIModulePackage()) .setInitialLifecycleState(LifecycleState.BEFORE_CREATE); // 添加 ReactPackage for (ReactPackage reactPackage : getPackages()) { builder.addPackage(reactPackage); } // 獲取 js Bundle 的加載路徑 String jsBundleFile = getJSBundleFile(); if (jsBundleFile != null) { builder.setJSBundleFile(jsBundleFile); } else { builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName())); } ReactInstanceManager reactInstanceManager = builder.build(); return reactInstanceManager; } }
咱們再回到 ReactActivity
,它本身並無作什麼事情,全部的功能都由它的委託類 ReactActivityDelegate
來完成,因此咱們直接看ReactActivityDelegate
是怎麼實現的:
public class ReactActivityDelegate { protected void onCreate(Bundle savedInstanceState) { String mainComponentName = getMainComponentName(); mReactDelegate = new ReactDelegate( getPlainActivity(), getReactNativeHost(), mainComponentName, getLaunchOptions()) { @Override protected ReactRootView createRootView() { return ReactActivityDelegate.this.createRootView(); } }; if (mMainComponentName != null) { // 載入 app 頁面 loadApp(mainComponentName); } } protected void loadApp(String appKey) { mReactDelegate.loadApp(appKey); // Activity 的 setContentView() 方法 getPlainActivity().setContentView(mReactDelegate.getReactRootView()); } }
onCreate()
的時候又實例化了一個 ReactDelegate
,咱們再看看它的實現。
在 ReactDelegate.java
裏,我沒看見它作了兩件事:
ReactRootView
做爲根視圖getReactNativeHost().getReactInstanceManager()
啓動 RN 應用public class ReactDelegate { public void loadApp(String appKey) { if (mReactRootView != null) { throw new IllegalStateException("Cannot loadApp while app is already running."); } // 建立 ReactRootView 做爲根視圖 mReactRootView = createRootView(); // 啓動 RN 應用 mReactRootView.startReactApplication( getReactNativeHost().getReactInstanceManager(), appKey, mLaunchOptions); } }
基礎的啓動流程本節涉及到的源碼內容到這裏就能夠了,感興趣的讀者能夠結合我最後給出的參考資料和 React Native 源碼深挖探索一下。
對於 React Native 爲主體的應用,APP 啓動後就要立馬初始化 RN 容器,基本上沒有什麼優化思路;可是 Native 爲主的混合開發 APP 卻有招:
既然初始化耗時最長,咱們在正式進入 React Native 容器前提早初始化不就行了?
這個方法很是的常見,由於不少 H5 容器也是這樣作的。正式進入 WebView 網頁前,先作一個 WebView 容器池,提早初始化 WebView,進入 H5 容器後,直接加載數據渲染,以達到網頁秒開的效果。
RN 容器池這個概念看着很玄乎,其實就是一個 Map
,key
爲 RN 頁面的 componentName
(即 AppRegistry.registerComponent(appName, Component)
中傳入的 appName
),value
就是一個已經實例化的 RCTRootView/ReactRootView
。
APP 啓動後找個觸發時機提早初始化,進入 RN 容器前先讀容器池,若是有匹配的容器,直接拿來用便可,沒有匹配的再從新初始化。
寫兩個很簡單的案例,iOS 能夠以下圖所示,構建 RN 容器池:
@property (nonatomic, strong) NSMutableDictionary<NSString *, RCTRootView *> *rootViewRool; // 容器池 -(NSMutableDictionary<NSString *, RCTRootView *> *)rootViewRool { if (!_rootViewRool) { _rootViewRool = @{}.mutableCopy; } return _rootViewRool; } // 緩存 RCTRootView -(void)cacheRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge { // 初始化 RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:componentName initialProperties:props]; // 實例化後要加載到屏幕的最下面,不然不能觸發視圖渲染 [[UIApplication sharedApplication].keyWindow.rootViewController.view insertSubview:rootView atIndex:0]; rootView.frame = [UIScreen mainScreen].bounds; // 把緩存好的 RCTRootView 放到容器池中 NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path]; self.rootViewRool[key] = rootView; } // 讀取容器 -(RCTRootView *)getRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge { NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path]; RCTRootView *rootView = self.rootViewRool[key]; if (rootView) { return rootView; } // 兜底邏輯 return [[RCTRootView alloc] initWithBridge:bridge moduleName:componentName initialProperties:props]; }
Android 以下構建 RN 容器池:
private HashMap<String, ReactRootView> rootViewPool = new HashMap<>(); // 建立容器 private ReactRootView createRootView(String componentName, String path, Bundle props, Context context) { ReactInstanceManager bridgeInstance = ((ReactApplication) application).getReactNativeHost().getReactInstanceManager(); ReactRootView rootView = new ReactRootView(context); if(props == null) { props = new Bundle(); } props.putString("path", path); rootView.startReactApplication(bridgeInstance, componentName, props); return rootView; } // 緩存容器 public void cahceRootView(String componentName, String path, Bundle props, Context context) { ReactRootView rootView = createRootView(componentName, path, props, context); String key = componentName + "_" + path; // 把緩存好的 RCTRootView 放到容器池中 rootViewPool.put(key, rootView); } // 讀取容器 public ReactRootView getRootView(String componentName, String path, Bundle props, Context context) { String key = componentName + "_" + path; ReactRootView rootView = rootViewPool.get(key); if (rootView != null) { rootView.setAppProperties(newProps); rootViewPool.remove(key); return rootView; } // 兜底邏輯 return createRootView(componentName, path, props, context); }
固然,因爲每次 RCTRootView/ReactRootView
都要佔用必定的內存,因此何時實例化,實例化幾個容器,池的大小限制,何時清除容器,都須要結合業務進行實踐和摸索。
iOS 的 Native Modules 有 3 塊兒內容,大頭是中間的 _initializeModules
函數:
// RCTCxxBridge.mm - (void)start { // 初始化 RCTBridge 時調用 initWithBundleURL_moduleProvider_launchOptions 中的 moduleProvider 返回的 native modules [self registerExtraModules]; // 註冊全部的自定義 Native Module (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO]; // 初始化全部懶加載的 native module,只有用 Chrome debug 時纔會調用 [self registerExtraLazyModules]; }
咱們看看 _initializeModules
函數作了什麼:
// RCTCxxBridge.mm - (NSArray<RCTModuleData *> *)_initializeModules:(NSArray<Class> *)modules withDispatchGroup:(dispatch_group_t)dispatchGroup lazilyDiscovered:(BOOL)lazilyDiscovered { for (RCTModuleData *moduleData in _moduleDataByID) { if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) { // Modules that were pre-initialized should ideally be set up before // bridge init has finished, otherwise the caller may try to access the // module directly rather than via `[bridge moduleForClass:]`, which won't // trigger the lazy initialization process. If the module cannot safely be // set up on the current thread, it will instead be async dispatched // to the main thread to be set up in _prepareModulesWithDispatchGroup:. (void)[moduleData instance]; } } _moduleSetupComplete = YES; [self _prepareModulesWithDispatchGroup:dispatchGroup]; }
根據 _initializeModules
和 _prepareModulesWithDispatchGroup
的註釋,能夠看出 iOS 在 JS Bundle 加載的過程當中(在 JSThead 線程進行),同時在主線程初始化全部的 Native Modules。
結合前面的源碼分析,咱們能夠看出 React Native iOS 容器初始化的時候,會初始化全部的 Native Modules,若 Native Modules
比較多,就會影響 Android RN 容器的啓動時間。
關於 Native Modules 的註冊,其實在 MainApplication.java
這個入口文件裏已經給出了線索:
// MainApplication.java protected List<ReactPackage> getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List<ReactPackage> packages = new PackageList(this).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); return packages; }
因爲 0.60 以後 React Native 啓用了 auto link
,安裝的第三方 Native Modules 都在 PackageList
裏,因此咱們只要 getPackages()
一下就能獲取 auto link
的 Modules。
源碼裏,在 ReactInstanceManager.java
這個文件中,會運行 createReactContext()
建立 ReactContext
,這裏面有一步就是註冊 nativeModules
的註冊表:
// ReactInstanceManager.java private ReactApplicationContext createReactContext( JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) { // 註冊 nativeModules 註冊表 NativeModuleRegistry nativeModuleRegistry = processPackages(reactContext, mPackages, false); }
根據函數調用,咱們追蹤到 processPackages()
這個函數裏,利用一個 for 循環把 mPackages 裏的 Native Modules 所有加入註冊表:
// ReactInstanceManager.java private NativeModuleRegistry processPackages( ReactApplicationContext reactContext, List<ReactPackage> packages, boolean checkAndUpdatePackageMembership) { // 建立 JavaModule 註冊表 Builder,用來建立 JavaModule 註冊表, // JavaModule 註冊表將全部的 JavaModule 註冊到 CatalystInstance 中 NativeModuleRegistryBuilder nativeModuleRegistryBuilder = new NativeModuleRegistryBuilder(reactContext, this); // 給 mPackages 加鎖 // mPackages 類型爲 List<ReactPackage>,與 MainApplication.java 裏的 packages 對應 synchronized (mPackages) { for (ReactPackage reactPackage : packages) { try { // 循環處理咱們在 Application 裏注入的 ReactPackage,處理的過程就是把各自的 Module 添加到對應的註冊表中 processPackage(reactPackage, nativeModuleRegistryBuilder); } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } } } NativeModuleRegistry nativeModuleRegistry; try { // 生成 Java Module 註冊表 nativeModuleRegistry = nativeModuleRegistryBuilder.build(); } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END); } return nativeModuleRegistry; }
最後調用 processPackage()
進行真正的註冊:
// ReactInstanceManager.java private void processPackage( ReactPackage reactPackage, NativeModuleRegistryBuilder nativeModuleRegistryBuilder ) { nativeModuleRegistryBuilder.processPackage(reactPackage); }
從上面的流程能夠看出,Android 註冊 Native Modules
的時候是同步全量註冊的,若 Native Modules
比較多,就會影響 Android RN 容器的啓動時間。
說實話,Native Modules 全量綁定在現有的架構裏是無解的:無論這個 Native Methods 你有沒有用到,容器啓動時先所有初始化一遍。在新的 RN 架構裏,TurboModules 會解決這個問題(本文下一小節會介紹)。
若是非要說優化,其實還有個思路,你不是全量初始化嗎,那我讓 Native Modules 的數量減小不就好了?新架構裏有一步叫作 Lean Core,就是精簡 React Native 核心,把一些功能/組件從 RN 的主工程項目裏移出去(例如 WebView
組件),交給社區維護,你想用的時候再單獨下載集成。
這樣作的好處主要有幾點:
如今 Lean Core 的工做基本已經完成,更多討論可見官方 issues 討論區,咱們只要同步升級 React Native 版本就能夠享用 Lean Core 的成果。
React Native 新架構已經跳票快兩年了,每次問進度,官方回覆都是「別催了別催了在作了在作了」。
我我的去年期待了一全年,可是啥都沒等到,因此 RN 啥時候更新到 1.0.0 版本,我已經不在意了。雖然 RN 官方一直在鴿,可是不得不說他們的新架構仍是有些東西的,市面上存在關於 RN 新架構的文章和視頻我基本都看了一遍,因此我的對新架構仍是有個總體的認知。
由於新架構尚未正式放出,因此具體細節上確定還存在一些差別,具體執行細節仍是要等 React Native 官方爲準。
JSI 的全名是 JavaScript Interface,一個用 C++ 寫的框架,做用是支持 JS 直接調用 Native 方法,而不是如今經過 Bridge 異步通信。
JS 直接調用 Native 如何理解呢?咱們舉一個最簡單的例子。在瀏覽器上調用 setTimeout
document.getElementById
這類 API 的時候,其實就是在 JS 側直接調用 Native Code,咱們能夠在瀏覽器控制檯裏驗證一下:
好比說我執行了一條命令:
let el = document.createElement('div')
變量 el
持有的不是一個 JS 對象,而是一個在 C++ 中被實例化的對象。對於 el 持有的這個對象咱們再設置一下相關屬性:
el.setAttribute('width', 100)
這時候實際上是 JS 同步調用 C++ 中的 setWidth
方法,改變這個元素的寬度。
React Native 新架構中的 JSI,主要就是起這個做用的,藉助 JSI,咱們能夠用 JS 直接得到 C++ 對象的引用(Host Objects),進而直接控制 UI,直接調用 Native Modules 的方法,省去 bridge 異步通信的開銷。
下面咱們舉個小例子,來看一下 Java/OC 如何藉助 JSI 向 JS 暴露同步調用的方法。
#pragma once #include <string> #include <unordered_map> #include <jsi/jsi.h> // SampleJSIObject 繼承自 HostObject,表示這個一個暴露給 JS 的對象 // 對於 JS 來講,JS 能夠直接同步調用這個對象上的屬性和方法 class JSI_EXPORT SampleJSIObject : public facebook::jsi::HostObject { public: // 第一步 // 將 window.__SampleJSIObject 暴露給JavaScript // 這是一個靜態函數,通常在應用初始化時從 ObjC/Java 中調用 static void SampleJSIObject::install(jsi::Runtime &runtime) { runtime.global().setProperty( runtime, "__sampleJSIObject", jsi::Function::createFromHostFunction( runtime, jsi::PropNameID::forAscii(runtime, "__SampleJSIObject"), 1, [binding](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) { // 返回調用 window.__SampleJSIObject 時獲得的內容 return std::make_shared<SampleJSIObject>(); })); } // 相似於 getter,每次 JS 訪問這個對象的時候,都要通過這個方法,做用相似於一個包裝器 // 好比說咱們調用 window.__sampleJSIObject.method1(),這個方法就會被調用 jsi::Value TurboModule::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { // 調用方法名 // 好比說調用 window.__sampleJSIObject.method1() 時,propNameUtf8 就是 method1 std::string propNameUtf8 = propName.utf8(runtime); return jsi::Function::createFromHostFunction( runtime, propName, argCount, [](facebook::jsi::Runtime &rt, const facebook::jsi::Value &thisVal, const facebook::jsi::Value *args, size_t count) { if (propNameUtf8 == 'method1') { // 調用 method1 時,相關的函數處理邏輯 } }); } std::vector<PropNameID> getPropertyNames(Runtime& rt){ } }
上面的例子比較簡短,想要深刻了解 JSI,能夠看《React Native JSI Challenge》這篇文章或直接閱讀源碼。
通過前面的源碼分析,咱們能夠得知,現有架構裏,Native 初始化時會全量加載 native modules,隨着業務的迭代,native modules 只會愈來愈多,這裏的耗時會愈來愈長。
TurboModules 就能夠一次性解決這個問題。在新架構裏,native modules 是懶加載的,也就是說只有你調用相應的 native modules 時纔會初始化加載,這樣就解決了初始化全量加載耗時較長的問題。
TurboModules 的調用路徑大概是這樣的:
global.__turboModuleProxy
SampleTurboModule
,咱們先在 JavaScript 側執行 require('NativeSampleTurboModule')
TurboModuleRegistry.getEnforcing()
,而後就會調用 global.__turboModuleProxy("SampleTurboModule")
global.__turboModuleProxy
的時候,就會調用第一步 JSI 暴露的 Native 方法,這時候 C++ 層經過傳入的字符串 "SampleTurboModule",找到 ObjC/Java 的實現,最後返回一個對應的 JSI 對象SampleTurboModule
的 JSI 對象,就能夠用 JavaScript 同步調用 JSI 對象上的屬性和方法經過上面的步驟,咱們能夠看到藉助 TurboModules, Native Modules 只有初次調用的時候纔會加載,這樣就完全乾掉 React Native 容器初始化時全量加載 Native Modules 時的時間;同時咱們能夠藉助 JSI 實現 JS 和 Native 的同步調用,耗時更少,效率更高。
本文主要從 Native 的角度出發,從源碼分析 React Native 現有架構的啓動流程,總結了幾個 Native 層的性能優化點;最後又簡單介紹了一下React Native 的新架構。下一篇文章我會講解如何從 JavaScript 入手,優化 React Native 的啓動速度。
若是你喜歡個人文章,但願點贊👍 收藏 📁 評論 💬 三連支持一下,謝謝你,這對我真的很重要!
歡迎你們關注個人微信公衆號:滷蛋實驗室,目前專一前端技術,對圖形學也有一些微小研究。
原文連接 👉 ⚡️ React Native 啓動速度優化——Native 篇(內含源碼分析):更新更及時,閱讀體驗更佳
React Native 升級指南(0.59 -> 0.62)
Chain React 2019 - Ram Narasimhan - Performance in React Native