React Native startReactApplication 方法簡析

Yutoti_三石發表於2021-09-07

React Native 啟動流程簡析 這篇文章裡,我們梳理了 RN 的啟動流程,最後的 startReactApplication 由於相對複雜且涉及到最終執行前端 js 的流程,我們單獨將其提取出來,獨立成文加以分析。

首先來看 startReactApplication 的呼叫之處:

mReactRootView.startReactApplication(
    getReactNativeHost().getReactInstanceManager(), appKey, mLaunchOptions);

可以看到是在 rootView 上呼叫 startReactApplication,入參為 instanceManager、appKey、mLaunchOptions

順著 startReactApplication 扒出其呼叫鏈:mReactInstanceManager.createReactContextInBackground() -> recreateReactContextInBackgroundInner() -> recreateReactContextInBackgroundFromBundleLoader() -> recreateReactContextInBackground() -> runCreateReactContextOnNewThread()

recreateReactContextInBackgroundReactInstanceManager 中的方法,做了兩件事:

  1. 建立 ReactContextInitParams 例項 initParams,如下,其入參 jsExecutorFactory 為建立 ReactInstanceManager 時傳入。

    final ReactContextInitParams initParams =
        new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
    
  2. 呼叫 runCreateReactContextOnNewThread

runCreateReactContextOnNewThreadReactInstanceManager 中的方法,主要做了兩件事:

  1. 建立一個新的執行緒,並在新執行緒中通過 createReactContext 建立 ReactContext 上下文;
  2. 通過 setupReactContext 來設定上下文環境,並最終呼叫到 AppRegistry.js 啟動App。

createReactContext

先看其呼叫的地方:

final ReactApplicationContext reactApplicationContext =
    createReactContext(
        initParams.getJsExecutorFactory().create(),
        initParams.getJsBundleLoader());

其兩個入參分別為 JsExecutorFactory 建立的 JavaScriptExecutor 例項,和 JsBundleLoader 例項。

JavaScriptExecutor

startReactApplication 第一個入參為 getReactNativeHost().getReactInstanceManager() 獲取 ReactInstanceManager 例項。ReactInstanceManager 例項在 RN 應用中只有一個,先前在建立 MainActivity 時已建立。

回顧 React Native 啟動流程簡析,在建立過程中實際上是呼叫下面的方法:

ReactInstanceManager reactInstanceManager = builder.build()

builderReactInstanceManagerBuilder,我們來到該類的 build 方法,發現其最終是執行 return new ReactInstanceManager(...),在構造引數中第 4 個引數即為:getDefaultJSExecutorFactory,來到其定義處:

  private JavaScriptExecutorFactory getDefaultJSExecutorFactory(
      String appName, String deviceName, Context applicationContext) {
    try {
      // If JSC is included, use it as normal
      initializeSoLoaderIfNecessary(applicationContext);
      SoLoader.loadLibrary("jscexecutor");
      return new JSCExecutorFactory(appName, deviceName);
    } catch (UnsatisfiedLinkError jscE) { /* ... */ }
}

也就是說在建立 ReactInstanceManagerBuilder 時我們就建立了 JSCExecutorFactory,並在隨後呼叫其 create 方法建立 JSCExecutorJSCExecutorFactory 實現了 JavaScriptExecutorFactory 介面,其 create 方法如下,返回了 JSCExecutor 例項:

  @Override
  public JavaScriptExecutor create() throws Exception {
    WritableNativeMap jscConfig = new WritableNativeMap();
    jscConfig.putString("OwnerIdentity", "ReactNative");
    jscConfig.putString("AppIdentity", mAppName);
    jscConfig.putString("DeviceIdentity", mDeviceName);
    return new JSCExecutor(jscConfig);
  }

再往下看 JSCExecutor 的定義,其繼承自 JavaScriptExecutor 類:

@DoNotStrip
/* package */ class JSCExecutor extends JavaScriptExecutor {
  static {
    SoLoader.loadLibrary("jscexecutor");
  }
  /* package */ JSCExecutor(ReadableNativeMap jscConfig) {
    super(initHybrid(jscConfig));
  }
  @Override
  public String getName() {
    return "JSCExecutor";
  }
  private static native HybridData initHybrid(ReadableNativeMap jscConfig);
}

於是就很清楚了,createReactContext 第一個引數為 JSCExecutor 例項,是通過 SoLoader 載入的 C++ 模組。

JsBundleLoader

同樣的,在 return new ReactInstanceManager(...),其構造引數中第 5 個引數為:JSBundleLoader.createAssetLoader(mApplication, mJSBundleAssetUrl, false)

來到其定義之處,發現其返回了 JSBundleLoader 例項,並重寫了其 loadScript 方法。

public static JSBundleLoader createAssetLoader(
    final Context context, final String assetUrl, final boolean loadSynchronously) {
  return new JSBundleLoader() {
    @Override
    public String loadScript(JSBundleLoaderDelegate delegate) {
      delegate.loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously);
      return assetUrl;
    }
  };
}

在建立完 JSCExecutor 例項和 JSBundleLoader 例項後,正式進入到 createReactContext 方法。

createReactContext

private ReactApplicationContext createReactContext(
  final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);

  CatalystInstanceImpl.Builder catalystInstanceBuilder = /* ... */

  try {
    catalystInstance = catalystInstanceBuilder.build();
  } finally { /* ... */ }

  reactContext.initializeWithInstance(catalystInstance);

  TurboModuleManager turboModuleManager =
    new TurboModuleManager( /* ... */ )

  catalystInstance.setTurboModuleManager(turboModuleManager);

  if (mJSIModulePackage != null) {
    catalystInstance.addJSIModules( /* ... */ );
  }

  catalystInstance.runJSBundle();
  return reactContext;

在這裡面,首先建立了 reactContext,並通過 catalystInstanceBuilder 建立了 catalystInstance。接著通過 initializeWithInstance 方法將 reactContextcatalystInstance 關聯起來,並進行了一系列的為 catalystInstance 初始化的工作。最後進入到方法 catalystInstance.runJSBundle() 中。

initializeWithInstance

通過呼叫 getUIQueueThreadgetNativeModulesQueueThreadgetJSQueueThread建立了3個執行緒佇列,分別是 UI執行緒、NativeModules 執行緒,和 JS 執行緒。

runJSBundle

public void runJSBundle() {
  mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
  synchronized (mJSCallsPendingInitLock) {
    mAcceptCalls = true;
    for (PendingJSCall function : mJSCallsPendingInit) {
      function.call(this);
    }
    mJSCallsPendingInit.clear();
    mJSBundleHasLoaded = true;
  }
  Systrace.registerListener(mTraceListener);
}

通過先前返回的 mJSBundleLoader 執行其 loadScript 方法:

public String loadScript(JSBundleLoaderDelegate delegate) {
  delegate.loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously);
  return assetUrl;
}

loadScriptFromAssets 方法在 CatalystInstanceImpl 中:

public void loadScriptFromAssets(
    AssetManager assetManager, String assetURL, boolean loadSynchronously) {
  mSourceURL = assetURL;
  jniLoadScriptFromAssets(assetManager, assetURL, loadSynchronously);
}

這裡的 assetURL 是在 createAssetLoader 建立 mJSBundleLoader 時傳入,其賦值時機是在 reactInstanceManagerBuilder 例項中,由 reactNativeHost 例項的 createReactInstanceManager 方法。若 開發者在 MainApplication.java 中通過重寫 getJSBundleFile 方法自定義了 assetURL 則使用該 url,否則使用系統預設,如:file://sdcard/myapp_cache/index.android.bundle

jniLoadScriptFromAssets 方法為 C++ 側定義的方法,用於讀取 js 檔案。為什麼 Java 程式碼中可以直接呼叫 C++ 方法,這裡還要打個問號,後續在分析 Java 與 C++ 通訊及 Java 與 JS 通訊時闡釋。

通過 createReactContext 建立了 reactContext,建立了 catalystInstance 例項,並將上述兩者關聯,接著通過 catalystInstance 讀入 js 檔案。接下來就進入到 setupReactContext 的環節。

setupReactContext

private void setupReactContext(final ReactApplicationContext reactContext) {
    synchronized (mAttachedReactRoots) {
      catalystInstance.initialize();
      for (ReactRoot reactRoot : mAttachedReactRoots) {
        if (reactRoot.getState().compareAndSet(ReactRoot.STATE_STOPPED, ReactRoot.STATE_STARTED)) {
          attachRootViewToInstance(reactRoot);
        }
      }
    }
    UiThreadUtil.runOnUiThread(
      public void run() {
        listener.onReactContextInitialized(reactContext);
      }
    )
    reactContext.runOnJSQueueThread(
      public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
      }
    )
    reactContext.runOnNativeModulesQueueThread(
      public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
      }
    )
}

這裡面做的事情如下:

  1. catalystInstance.initialize(): 所有原生模組的初始化
  2. attachRootViewToInstance(reactRoot): 繪製所有的 RootView 並新增到相應例項並設定相應的監聽事件
  3. 建立 UI 模組、JS 模組和原生模組執行緒,並設定 JS 模組和原生模組所線上程的優先順序

總結本文

從 createReactContext 和 setupReactContext 兩個方法的原始碼出發,分析了 RN startReactApplication 方法的執行過程,其中:

createReactContext 的主要作用是:建立 reactContext、建立 catalystInstance 例項,並將上述兩者關聯,接著通過 catalystInstance 讀入 js 檔案。

setupReactContext的主要作用是:初始化所有原生模組,繪製所有 rootview,建立 UI 模組、JS 模組和原生模組執行緒,並設定優先順序。

相關文章