⚡️ React Native 啓動速度優化——Native 篇(內含源碼分析)

Web 開發有一個經典問題:「瀏覽器中從輸入 URL 到頁面渲染的這個過程當中都發生了什麼?javascript

據我考據這個問題起碼有十年歷史了。在突飛猛進學不動的前端圈子裏,這個問題能一直被問,就是由於由於它是個很是好的問題,涉及很是多的知識點,平時作一些性能優化,均可以從這個問題出發,分析性能瓶頸,而後對症下藥進行優化。html

不過今天咱們不談 Web 的性能優化,只是藉助剛剛的那個那個經典問題的分析思路,從 React Native 的啓動到頁面的第一次渲染完成,結合 React Native 的源碼和 1.0 的新架構,一一分析 React Native 的啓動性能優化之路前端

若是你喜歡個人文章,但願點贊👍 收藏 📁 評論 💬 三連支持一下,謝謝你,這對我真的很重要!
閱讀提醒
1.文章中的源碼內容爲 RN 0.64 版本
2.源碼分析內容涉及 Objective-CJavaC++JavaScript 四門語言,我儘可能講得通俗易懂一些,若實在不理解能夠直接看結論

0.React Native 啓動流程

React Native 做爲一個 Web 前端友好的混合開發框架,啓動時能夠大體分爲兩個部分:java

  • Native 容器的運行
  • JavaScript 代碼的運行

其中 Native 容器啓動在現有架構(版本號小於 1.0.0)裏:大體能夠分爲 3 個部分:react

  • Native 容器初始化
  • Native Modules 的全量綁定
  • JSEngine 的初始化

容器初始化後,舞臺就交給了 JavaScript,流程能夠細分爲 2 個部分:ios

  • JavaScript 代碼的加載、解析和執行
  • JS Component 的構建

最後 JS Thread 把計算好的佈局信息發送到 Native 端,計算 Shadow Tree,最後由 UI Thread 進行佈局和渲染。git

關於渲染部分的性能優化能夠見我以前寫的 《React Native 性能優化指南》,我從 渲染圖片動畫長列表等方向介紹了 RN 渲染優化的常見套路,感興趣的讀者能夠前往查看,我這裏就很少介紹了。

上面的幾個步驟,我畫了一張圖,下面我以這張圖爲目錄,從左向右介紹各個步驟的優化方向:github

提示:React Native 初始化時,有可能多個任務 並行執行,因此上圖只能表示 React Native 初始化的大體流程,並不和實際代碼的執行時序一一對應。

1.升級 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)》的文章,若是有升級想法的老鐵能夠閱讀參考一下。瀏覽器

2.Native 容器初始化

容器的初始化確定是從 APP 的入口文件開始分析,下面我會挑選一些關鍵代碼,梳理一下初始化的流程。

iOS 源碼分析

1.AppDelegate.m

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 幹了些啥。

2.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 這個文件上去。

3.RCTBridge.m

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

4.RTCxxBridge.mm

RTCxxBridge 能夠說是 React Native 初始化的核心,我查閱了一些資料,貌似 RTCxxBridge 曾用名爲 RCTBatchedBridge,因此能夠粗暴的把這兩個類當成一回事兒。

由於在 RCTBridge 裏調用了 RTCxxBridgestart 方法,咱們就從 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 多線程的一些知識點,用文字描述大體是以下的流程:

  1. 初始化 js 線程 _jsThread
  2. 在主線程上註冊全部 native modules
  3. 準備 jsNative 之間的橋和 js 運行環境
  4. 在 JS 線程上建立消息隊列 RCTMessageThread,初始化 _reactInstance
  5. 在 JS 線程上加載 JS Bundle
  6. 等上面的事情所有作完後,執行 JS 代碼

其實上面的六個點均可以深挖下去,可是本節涉及到的源碼內容到這裏就能夠了,感興趣的讀者能夠結合我最後給出的參考資料和 React Native 源碼深挖探索一下。

Android 源碼分析

1.MainActivity.java & MainApplication.java

和 iOS 同樣,啓動流程咱們先從入口文件開始分析,咱們先看 MainActivity.java

MainActivity 繼承自 ReactActivityReactActivity 又繼承自 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 這個類,下面咱們就看一下它。

2.ReactNativeHost.java

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;
  }
}

3.ReactActivityDelegate.java

咱們再回到 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,咱們再看看它的實現。

4.ReactDelegate.java

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 容器池這個概念看着很玄乎,其實就是一個 Mapkey 爲 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 都要佔用必定的內存,因此何時實例化,實例化幾個容器,池的大小限制,何時清除容器,都須要結合業務進行實踐和摸索。

3.Native Modules 綁定

iOS 源碼分析

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 容器的啓動時間。

Android 源碼分析

關於 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 組件),交給社區維護,你想用的時候再單獨下載集成。

這樣作的好處主要有幾點:

  • 核心更加精簡,RN 維護者有更多的精力維護主要功能
  • 減少 Native Modules 的綁定耗時和多餘的 JS 加載時間,包體積的減少,對初始化性能更友好(咱們升級 RN 版本到 0.62 後初始化速度提高一倍,基本都是 Lean Core 的功勞)
  • 加快迭代速度,優化開發體驗等

如今 Lean Core 的工做基本已經完成,更多討論可見官方 issues 討論區,咱們只要同步升級 React Native 版本就能夠享用 Lean Core 的成果。

4.RN 新架構如何優化啓動性能

React Native 新架構已經跳票快兩年了,每次問進度,官方回覆都是「別催了別催了在作了在作了」。

我我的去年期待了一全年,可是啥都沒等到,因此 RN 啥時候更新到 1.0.0 版本,我已經不在意了。雖然 RN 官方一直在鴿,可是不得不說他們的新架構仍是有些東西的,市面上存在關於 RN 新架構的文章和視頻我基本都看了一遍,因此我的對新架構仍是有個總體的認知。

由於新架構尚未正式放出,因此具體細節上確定還存在一些差別,具體執行細節仍是要等 React Native 官方爲準。

JSI

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》這篇文章或直接閱讀源碼。

TurboModules

通過前面的源碼分析,咱們能夠得知,現有架構裏,Native 初始化時會全量加載 native modules,隨着業務的迭代,native modules 只會愈來愈多,這裏的耗時會愈來愈長。

TurboModules 就能夠一次性解決這個問題。在新架構裏,native modules 是懶加載的,也就是說只有你調用相應的 native modules 時纔會初始化加載,這樣就解決了初始化全量加載耗時較長的問題。

TurboModules 的調用路徑大概是這樣的:

  1. 先用 JSI 建立一個頂層的「Native Modules Proxy」,稱之爲 global.__turboModuleProxy
  2. 訪問一個 Native Modules,好比說要訪問 SampleTurboModule,咱們先在 JavaScript 側執行 require('NativeSampleTurboModule')
  3. 在 NativeSampleTurboModule.js 這個文件裏,咱們先調用 TurboModuleRegistry.getEnforcing(),而後就會調用 global.__turboModuleProxy("SampleTurboModule")
  4. 調用 global.__turboModuleProxy 的時候,就會調用第一步 JSI 暴露的 Native 方法,這時候 C++ 層經過傳入的字符串 "SampleTurboModule",找到 ObjC/Java 的實現,最後返回一個對應的 JSI 對象
  5. 如今咱們獲得了 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 性能優化指南

React Native 升級指南(0.59 -> 0.62)

Chain React 2019 - Ram Narasimhan - Performance in React Native

React Native's new architecture - Glossary of terms

React Native JSI Challenge

RFC0002: Turbo Modules ™

ReactNative與iOS原生通訊原理解析-初始化

React Native iOS 源碼解析

ReactNative源碼篇:源碼初識

如何用React Native預加載方案解決白屏問題

相關文章
相關標籤/搜索