ReactNative For Android(RN4A)源碼解讀-運行環境的創建流程

RN4A運行環境的創建流程

前言

國內近年來對ReactNaitve討論的火爆程度不言而喻,可能你都已經用了一段時間的RN4A了。不過你是否清楚RN4A是如何初始化一個環境?Js是何時通知Native渲染UI組件?從RN4A的環境初始化到ReactView呈現到UI的時候,RN4A都干了什么?別急,接下來就為你揭曉這些問題的答案。(PS: 本文源碼分析基于當前最新ReactNative源碼 v0.40.0-rc.4,限于作者水平有限,如果有錯誤和理解不當之處感謝指出。)

總體流程

為了接下來細節分析的時候你心中有個整體印象,我們先直接說下RN4A的框架初始化的總體流程。(PS:根據項目實際接入RN4A的方式不同,流程可能有所不同,這里只是官方使用的一種初始化流程。)

創建ReactRootView -> 創建ReactInstanceManager -> 創建ReactContext -> RN4A環境初始化完成 -> 通知Js渲染界面。

萬物之始ReactRootView

俗話說"擒賊先擒王",一般分析代碼都會從源頭走起。如果你查看RN4A的接入文檔,就知道RN4A已經為我們封裝好了ReactActiviy類,只要通過繼承它,你可以省掉RN4A與Activity之間的絕大部分邏輯交互,包括生命周期回調,以及發送消息通知Js渲染UI的操作等等。通過源碼我們可以看到ReactActivity中所有的邏輯都是由ReactActivityDelegate類來代理,這是一個不錯的設計方式,你可以輕松地把ReactActivityDelegate集成到你自己的Activity中,自由定制RN4A環境的初始化方案。好的,有點扯遠了,現在讓我們來看下ReactActivityDelegate中的關鍵方法吧。

  protected void onCreate(Bundle savedInstanceState) {
    //省略判斷懸浮窗權限
    ...

    if (mMainComponentName != null && !needsOverlayPermission) {
      loadApp(mMainComponentName);
    }
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  }

我們知道onCreate方法就是Activity開始執行的地方,ReactActivityDelegateonCreate方法自然也會在Activity的onCreate中調用,這里可以看到,代碼會判斷當前App是否有顯示懸浮層的權限,然后開始調用loadApp方法,注意,這里就是RN4A官方方式加載的入口了。我們接下來繼續跟蹤下去:

  protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }

這段代碼邏輯很簡單,createRootView方法創建了一個RN4A的根View(ReactRootView),所有RN4A的View都會創建在ReactRootView里,然后將ReactRootView當成了Activity的內容布局,一般將ReactRootView作為Activity的內容布局是比較省事的方式,當然,你也可以將它作為某個ViewGroup的子View,只不過這種方式你很容易會踩到一些坑,比如你需要處理RN的View和原生View之間的事件沖突。好了,那我們接著看代碼,我們看下關鍵的startReactApplication方法,這里注意一下它的形參,第一個參數需要一個ReactInstanceManager的實例,ReactNativeHostgetReactInstanceManager這個方法會創建一個ReactInstanceManager實例,ReactInstanceManager是RN4A的核心類,我們需要先來看下它是如何被初始化的。

  protected ReactInstanceManager createReactInstanceManager() {
    ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
      .setApplication(mApplication)
      .setJSMainModuleName(getJSMainModuleName())
      .setUseDeveloperSupport(getUseDeveloperSupport())
      .setRedBoxHandler(getRedBoxHandler())
      .setUIImplementationProvider(getUIImplementationProvider())
      .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

    for (ReactPackage reactPackage : getPackages()) {
      builder.addPackage(reactPackage);
    }

    String jsBundleFile = getJSBundleFile();
    if (jsBundleFile != null) {
      builder.setJSBundleFile(jsBundleFile);
    } else {
      builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
    }
    return builder.build();
  }

由于ReactInstanceManager的參數很多,所以RN4A使用了建造者(Builder)模式,我們先簡單看一下這些參數的意義:

  • application - 這個就不說了;
  • jsMainModuleName - 在Js文件中設置的模塊名稱,通過該名稱加載對應的Js組件;
  • useDeveloperSupport - 設置是否使用Dev調試工具;
  • redBoxHandler - 設置紅框處理器,Js運行時的異常展示出來的紅框;
  • UIImplementationProvider - UIManagerModule的工人,負責處理從Js過來的跟UI操作相關的消息(View的創建、測量、更新等各種臟活);
  • initialLifecycleState - ReactInstanceManager實例初始化時候的生命周期;
  • reactPackage - 自定義的ReactNative包:
  • jsBundleFile - 放在手機文件系統中的JsBundle的文件路徑;
  • bundleAssetName - 內置在Assets目錄下的JsBundle文件,如果設置了則不會走其他的JsBundle加載方式,需要注意在jsBundleFile有值的情況下不會生效;

看完ReactInstanceManager的創建,我們再返回到之前loadApp方法處,繼續跟蹤ReactRootViewstartReactApplication方法:

public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle launchOptions) {
    UiThreadUtil.assertOnUiThread();

    // TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
    // here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
    // it in the case of re-creating the catalyst instance
    Assertions.assertCondition(
        mReactInstanceManager == null,
        "This root view has already been attached to a catalyst instance manager");

    mReactInstanceManager = reactInstanceManager;
    mJSModuleName = moduleName;
    mLaunchOptions = launchOptions;

    if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
      mReactInstanceManager.createReactContextInBackground();
    }

    // We need to wait for the initial onMeasure, if this view has not yet been measured, we set which
    // will make this view startReactApplication itself to instance manager once onMeasure is called.
    if (mWasMeasured) {
      attachToReactInstanceManager();
    }
  }

這里有兩個重要的操作, mReactInstanceManager.createReactContextInBackground方法完成了RN4A環境的創建和初始化,其中RN4A橋的創建和Js腳本的加載都是在這里面進行的;而另一個attachToReactInstanceManager則將ReactRootView實例與ReactInstanceManager實例綁定起來,并從Native發送runApplication消息到Js,Js收到消息便會開始執行相應的業務,這里需要注意的是,如果ReactInstanceManager是第一次創建的話由于它的內部還沒有創建好RN4A上下文實例(ReactContext),Native此時并不會發送runApplication消息給Js,而是將這個操作放在RN4A所有的環境創建完成之后才被執行,這里只是先提下,下面還會說到。
接著我們看下mReactInstanceManager.createReactContextInBackground這個方法,由源碼可知XReactInstanceManagerImplReactInstanceManager的唯一實現類,所以你可以一直跟蹤到下面的代碼,

private void recreateReactContextInBackgroundInner() {
    UiThreadUtil.assertOnUiThread();

    if (mUseDeveloperSupport && mJSMainModuleName != null) {
      final DeveloperSettings devSettings = mDevSupportManager.getDevSettings();

      // If remote JS debugging is enabled, load from dev server.
      if (mDevSupportManager.hasUpToDateJSBundleInCache() &&
          !devSettings.isRemoteJSDebugEnabled()) {
        // If there is a up-to-date bundle downloaded from server,
        // with remote JS debugging disabled, always use that.
        onJSBundleLoadedFromServer();
      } else if (mBundleLoader == null) {
        mDevSupportManager.handleReloadJS();
      } else {
        mDevSupportManager.isPackagerRunning(
            new DevServerHelper.PackagerStatusCallback() {
              @Override
              public void onPackagerStatusFetched(final boolean packagerIsRunning) {
                UiThreadUtil.runOnUiThread(
                    new Runnable() {
                      @Override
                      public void run() {
                        if (packagerIsRunning) {
                          mDevSupportManager.handleReloadJS();
                        } else {
                          // If dev server is down, disable the remote JS debugging.
                          devSettings.setRemoteJSDebugEnabled(false);
                          recreateReactContextInBackgroundFromBundleLoader();
                        }
                      }
                    });
              }
            });
      }
      return;
    }

    recreateReactContextInBackgroundFromBundleLoader();
  }

上面的代碼還挺長的,其實只要關注recreateReactContextInBackgroundFromBundleLoader方法就行了,不過這里還是需要簡單說下這一處源碼的邏輯。執行過程大概是這樣的,如果你啟用了RN4A的Dev支持,并且Js模塊名(JsModuleName)不是空的,RN4A就會判斷你本地是否有最新的JsBundler文件,如果有的話就直接讀取本地的JsBundle文件,否則會從你的LocalServer中加載JsBundle文件,這里為了分析源碼方便,我們先假設RN4A是處在Release環境中執行的,所以我們就直接走recreateReactContextInBackgroundFromBundleLoader代碼, 在RN4A源碼中經過幾處跳轉之后我們就會看到下面這段邏輯。

private void recreateReactContextInBackground(
      JavaScriptExecutor.Factory jsExecutorFactory,
      JSBundleLoader jsBundleLoader) {
    UiThreadUtil.assertOnUiThread();

    ReactContextInitParams initParams =
        new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
    if (mReactContextInitAsyncTask == null) {
      // No background task to create react context is currently running, create and execute one.
      mReactContextInitAsyncTask = new ReactContextInitAsyncTask();
      mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, initParams);
    } else {
      // Background task is currently running, queue up most recent init params to recreate context
      // once task completes.
      mPendingReactContextInitParams = initParams;
    }
  }

這是RN4A的一段相對重要的邏輯,我們可以看到RN4A使用了Android的異步任務(ReactContextInitAsyncTask)來執行初始化操作,閱讀源碼可以知道RN4A先判斷當前有沒有ReactContextInitAsyncTask在進行,如果有的話,RN4A會將本次的初始化參數存放到initParams全局變量,等ReactContextInitAsyncTask初始化完成之后再去重新執行初始化操作,如果當前沒有ReactContextInitAsyncTask任務在執行,則直接新建一個ReactContextInitAsyncTask任務并開始執行初始化操作。我們接著跟進源碼:

  /*
   * Task class responsible for (re)creating react context in the background. These tasks can only
   * be executing one at time, see {@link #recreateReactContextInBackground()}.
   */
  private final class ReactContextInitAsyncTask extends
      AsyncTask<ReactContextInitParams, Void, Result<ReactApplicationContext>> {
    @Override
    protected void onPreExecute() {
      if (mCurrentReactContext != null) {
        tearDownReactContext(mCurrentReactContext);
        mCurrentReactContext = null;
      }
    }

    @Override
    protected Result<ReactApplicationContext> doInBackground(ReactContextInitParams... params) {
        //省略一些代碼
      Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
      try {
        JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();
        return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader()));
      } catch (Exception e) {
        // Pass exception to onPostExecute() so it can be handled on the main thread
        return Result.of(e);
      }
    }

    @Override
    protected void onPostExecute(Result<ReactApplicationContext> result) {
      try {
        setupReactContext(result.get());
      } catch (Exception e) {
        mDevSupportManager.handleException(e);
      } finally {
        mReactContextInitAsyncTask = null;
      }

      // Handle enqueued request to re-initialize react context.
      if (mPendingReactContextInitParams != null) {
        recreateReactContextInBackground(
            mPendingReactContextInitParams.getJsExecutorFactory(),
            mPendingReactContextInitParams.getJsBundleLoader());
        mPendingReactContextInitParams = null;
      }
    }

    @Override
    protected void onCancelled(Result<ReactApplicationContext> reactApplicationContextResult) {
      //省略一些代碼
    }
  }

這也算是RN4A的核心一部分了,通過源碼我們可以知道RN4A會在任務開始時候卸載掉舊的RN4A上下文實例(ReactContext是一個RN4A的上下文環境,持有了UI、Js和Native三線程,并維持了一個和Js通信的橋等),然后在異步任務線程池中RN4A會開始創建一個新的RN4A上下文實例(ReactContext),并在執行結束之后設置這個RN4A上下文實例(ReactContext);當RN4A執行完成之后如果發現有initParams(上文提到的參數),就會重新開始執行ReactContextInitAsyncTask任務。接下來我們去看下createReactContext方法都在做啥。

  /**
   * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set
   */
  private ReactApplicationContext createReactContext(
      JavaScriptExecutor jsExecutor,
      JSBundleLoader jsBundleLoader) {
    mSourceUrl = jsBundleLoader.getSourceUrl();
    List<ModuleSpec> moduleSpecs = new ArrayList<>();
    Map<Class, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
    JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();

    final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
    if (mUseDeveloperSupport) {
      reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
    }
    try {
      CoreModulesPackage coreModulesPackage =
        new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider);
      processPackage(
        coreModulesPackage,
        reactContext,
        moduleSpecs,
        reactModuleInfoMap,
        jsModulesBuilder);
    } finally {
        //省略一些代碼 
    }
    for (ReactPackage reactPackage : mPackages) {
      try {
        processPackage(
          reactPackage,
          reactContext,
          moduleSpecs,
          reactModuleInfoMap,
          jsModulesBuilder);
      } finally {
        //省略一些代碼
      }
    }
    NativeModuleRegistry nativeModuleRegistry;
    try {
       nativeModuleRegistry = new NativeModuleRegistry(moduleSpecs, reactModuleInfoMap);
    } finally {
        //省略一些代碼
    }

    NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null
        ? mNativeModuleCallExceptionHandler
        : mDevSupportManager;
    CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder()
        .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
        .setJSExecutor(jsExecutor)
        .setRegistry(nativeModuleRegistry)
        .setJSModuleRegistry(jsModulesBuilder.build())
        .setJSBundleLoader(jsBundleLoader)
        .setNativeModuleCallExceptionHandler(exceptionHandler);

    final CatalystInstance catalystInstance;
    try {
      catalystInstance = catalystInstanceBuilder.build();
    } finally {
        //省略一些代碼
    }

    if (mBridgeIdleDebugListener != null) {
      catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
    }

    reactContext.initializeWithInstance(catalystInstance);
    catalystInstance.runJSBundle();

    return reactContext;
  }

上面的代碼看著有點長,不過邏輯也就這么幾步:

  1. 生成一個RN4A上下文實例(ReactContext),并在開啟Dev模式情況下設置一個Native異常處理器,用于捕獲三個線程(UI、Js和Native)中發生的異常;
  2. 處理RN包,包括核心包(CorePackage)以及注冊的自定義包(如官方提供的MainPackage),NativeModule信息被存放到moduleSpecsreactModuleInfoMap中,而JsModule則被放到jsModulesBuilder中;
  3. 通過橋構造器(CatalystInstanceImpl.Builder)構建出一個CatalystInstance實例,它在RN4A中負責中掌管Js和Native之間的通信;
  4. 將創建好的橋(CatalystInstance)放到RN4A上下文(ReactContext)中并進行初始化,這個過程實際上只是讓RN4A上下文(ReactContext)持有三個關鍵線程管理實例(UI、JS和Native),RN4A的全部工作依賴這三條線程之間的相互協作;
  5. 最后就是運行在橋實例(CatalystInstance)中的JsBundle了,這里會從Jni層去調用Js引擎解釋執行Js代碼,關于RN4A的Jni層的邏輯,限于篇幅留待之后分析。

接下來讓我們從ReactContextInitAsyncTask類的onPostExecute方法接著看,進入setupReactContext方法。

private void setupReactContext(ReactApplicationContext reactContext) {
    UiThreadUtil.assertOnUiThread();
    Assertions.assertCondition(mCurrentReactContext == null);
    mCurrentReactContext = Assertions.assertNotNull(reactContext);
    CatalystInstance catalystInstance =
        Assertions.assertNotNull(reactContext.getCatalystInstance());

    catalystInstance.initialize();
    mDevSupportManager.onNewReactContextCreated(reactContext);
    mMemoryPressureRouter.addMemoryPressureListener(catalystInstance);
    moveReactContextToCurrentLifecycleState();

    for (ReactRootView rootView : mAttachedRootViews) {
      attachMeasuredRootViewToInstance(rootView, catalystInstance);
    }

    ReactInstanceEventListener[] listeners =
      new ReactInstanceEventListener[mReactInstanceEventListeners.size()];
    listeners = mReactInstanceEventListeners.toArray(listeners);

    for (ReactInstanceEventListener listener : listeners) {
      listener.onReactContextInitialized(reactContext);
    }
  }

這里RN4A會去執行一次橋CatalystInstance的初始化邏輯,并把初始化完成的消息發送出去,比如通知注冊在橋的Module執行初始化的一些操作,告訴綁定的ReactRootView可以通過attachMeasuredRootViewToInstance方法通知Js"真正"執行業務邏輯了,之后Js就會開始通過一系列的消息指揮Native渲染展示UI等等。

結語

至此,RN4A從環境初始化到RN界面展示出來所經過的一個流程我們已經走了一遍,這篇文章只是分析RN4A框架的開篇,Native和Js之間的通信方式,Js Dom的解析渲染等等,會在后續的文章中分析,敬請期待。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容