React Native 異常處理

1、引言

公司最近在大力推崇使用React Native(如下簡稱RN)來開發業務組件,來代替原生業務組件,以達到快速迭代、方便熱修復等目的。雖然RN擁有比混合H5開發更好的性能體驗,性能直逼原生,可是畢竟RN是一個新的框架,可能潛在很多問題。因此,咱們但願能對RN的異常進行捕獲,並進行上報處理,以便後期分析解決這些異常,優化用戶體驗。react

RN異常在大方向上能夠分爲啓動期異常和運行期異常。下面就針對這兩種異常進行分析。bash

2、啓動期異常

啓動期咱們能夠認爲從調用ReactRootViewstartReactApplication方法開始,到ReactRootView渲染到界面後結束。咱們先從startReactApplication方法進行分析。框架

public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle initialProperties,
      @Nullable String initialUITemplate) {

    try {
      mReactInstanceManager = reactInstanceManager;
      mJSModuleName = moduleName;
      mAppProperties = initialProperties;
      mInitialUITemplate = initialUITemplate;

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

      attachToReactInstanceManager();

    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
    }
 }
複製代碼

startReactApplication方法中調用了ReactInstanceManagercreateReactContextInBackground方法,最終調用的是runCreateReactContextOnNewThread方法。ide

private void runCreateReactContextOnNewThread(final ReactContextInitParams initParams) {
	...省略
	mCreateReactContextThread =
	    new Thread(
	        null,
	        new Runnable() {
	          @Override
	          public void run() {
	            ...省略

	            try {
	              Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
	              ReactMarker.logMarker(VM_INIT);
	              final ReactApplicationContext reactApplicationContext =
	                  createReactContext(
	                      initParams.getJsExecutorFactory().create(),
	                      initParams.getJsBundleLoader());

	              mCreateReactContextThread = null;
	              ReactMarker.logMarker(PRE_SETUP_REACT_CONTEXT_START);
	              final Runnable maybeRecreateReactContextRunnable =
	                  new Runnable() {
	                    @Override
	                    public void run() {
	                      if (mPendingReactContextInitParams != null) {
	                        runCreateReactContextOnNewThread(mPendingReactContextInitParams);
	                        mPendingReactContextInitParams = null;
	                      }
	                    }
	                  };
	              Runnable setupReactContextRunnable =
	                  new Runnable() {
	                    @Override
	                    public void run() {
	                      try {
	                        setupReactContext(reactApplicationContext);
	                      } catch (Exception e) {
	                        mDevSupportManager.handleException(e);
	                      }
	                    }
	                  };

	              reactApplicationContext.runOnNativeModulesQueueThread(setupReactContextRunnable);
	              UiThreadUtil.runOnUiThread(maybeRecreateReactContextRunnable);
	            } catch (Exception e) {
	              mDevSupportManager.handleException(e);
	            }
	          }
	        },
	        "create_react_context");
	mCreateReactContextThread.start();
 }
複製代碼

在該方法中,咱們能夠看到當發生異常時,是有被catch住了。可是僅僅是在開發模式下才會被catch住,不然該異常就被拋出來了。函數

因此,咱們須要在這裏進行一個修改:post

在不是開發者模式時,使用自定義的ExceptionHandler去處理啓動期的異常。爲了在一個地方集中處理異常,可使用ReactInstanceManager中的ExceptionHandler進行處理。性能

3、運行期異常

運行期異常比啓動期異常要複雜一些,下面羅列了八個運行時異常的場景,若是咱們可以將這八個異常場景覆蓋住,那麼基本上就能達到目標。優化

  • JS調用Native模塊,Native模塊不存在
  • JS調用Native模塊,函數原型不一致
  • JS調用Native模塊,Native模塊運行異常
  • Native調用JS模塊,JS模塊不存在
  • Native調用JS模塊,函數原型不一致
  • Native調用JS模塊,JS模塊運行異常
  • JS自己代碼運行異常
  • UI操做異常

3.1 運行線程

在解析運行期異常前,咱們先來談一下RN中的運行線程。RN在初始化時維護了三個隊列,分別是:ui

  • UIQueue,專門執行UI操做。這裏使用的是Android原生的UI線程
  • NativeQueue,執行Native模塊方法的操做。一般由JS發起,它是一個後臺線程
  • JSQueue,執行JS邏輯。它是一個後臺線程

所以三個隊列對應着三個線程。具體來講,JS代碼、Native代碼是運行在這幾個線程的,咱們須要理解一些關於RN的Bridge原理。但因爲這不是本文的重點,因此有興趣的同窗能夠自行查找相關文章。spa

CatalystInstance(其實是CatalystInstanceImpl對象)實例化時,會初始化上面所說的三個隊列,並將它們的引用經過initializeBridge方法傳遞給C++層的Bridge。 須要注意的是,這幾個Queue裏面都引用了MessageQueueThreadHandler(一個Handler對象),事件都是經過MessageQueueThreadHandler對象post到其中的消息隊列中進行調度並執行(執行時調用dispatchMessage方法)。而MessageQueueThreadHandler重寫了dispatchMessage方法,幷包裝了一層try-catch,這使得上面所說的三個線程中發生的crash異常都能經過這裏的catch方法進行捕獲。

@Override
public void dispatchMessage(Message msg) {
   try {
    super.dispatchMessage(msg);
  } catch (Exception e) {
    mExceptionHandler.handleException(e);
  }
}
複製代碼

幸運的是,這裏的mExceptionHandler對象就是咱們傳遞給ReactInstanceManagerExceptionHandler

那是否是說,咱們只要將自定義的ExceptionHandler對象傳遞給ReactInstanceManager,就可以統一捕獲並處理RN運行期發生的異常呢?下面咱們對上面提到的運行期的八個異常場景進行分析。

3.2 NativeQueue捕獲的異常

  • JS調用Native模塊,函數原型不一致(捕獲2)

因爲調用Native模塊,是經過messageQueue.js,而後經過C++層的Bridge,而後是NativeToJsBridge.cpp,最後執行在了NativeQueue所在的後臺線程中。因此這個過程當中發生的異常就能夠被NativeQueue所捕獲。

  • JS調用Native模塊,Native模塊運行異常(捕獲3)

道理和上面提到的同樣,Native模塊運行時是在NativeQueue所在的線程,既然這個過程當中運行異常,那麼其中的異常就會被NativeQueue所捕獲。

3.3 JSQueue捕獲的異常

上面提到過,CatalystInstance初始化時會將JSQueue的引用傳遞到C++ Bridge,而執行JS邏輯時都會執行在該JSQueue中。

  • JS調用Native模塊,Native模塊不存在(捕獲1)

JS調用Native模塊,是經過NativeModules.js去查找是否有該模塊存在的。當調用一個不存在的Native模塊時,確定就發生JS錯誤了,這就會被JSQueue所捕獲。

  • Native調用JS模塊,JS模塊不存在(捕獲4)
  • Native調用JS模塊,函數原型不一致(捕獲5)

Native去找JS模塊時,實際上是經過Java中的動態代碼,走CatalystInstanceImplcallFunction方法,最後會走到NativeToJsBridge.cpp裏面的callFunction函數,最後仍是交給JSQueue去解析模塊、運行函數,那麼發生錯誤(JS模塊不存在,函數原型不一致)天然會被JSQueue所捕獲。

  • Native調用JS模塊,JS模塊運行異常(捕獲6)
  • JS自己代碼運行異常(捕獲7)

這兩個異常都是在JS運行時發生的錯誤,那麼天然會被JSQueue所捕獲。

3.4 UI操做異常

這裏之因此將UI操做異常單獨拿出來,是由於UI操做的異常並不執行在上面所說的三個運行隊列中,因此UI操做異常就不會被上面所說的ExceptionHandler所捕獲。 咱們知道UI操做實際上調用的是UIManagerModule,可是這個過程並非同步的,而是有一個入隊列並調度的一個過程,以下圖所示。

UIManagercreateView舉例,其最後會將該UI操做的執行邏輯調度到GuardedFrameCallbackdoFrame方法。

@Override
public final void doFrame(long frameTimeNanos) {
@Override
public final void doFrame(long frameTimeNanos) {
	try {
	  doFrameGuarded(frameTimeNanos);
	} catch (RuntimeException e) {
	  mReactContext.handleException(e);
	}
}
複製代碼

能夠看到,這裏的異常也有被catch住了,調用的是ReactContexthandleException方法。

public void handleException(Exception e) {
    if (mCatalystInstance != null &&
        !mCatalystInstance.isDestroyed() &&
        mNativeModuleCallExceptionHandler != null) {
      mNativeModuleCallExceptionHandler.handleException(e);
    } else {
      throw new RuntimeException(e);
    }
 }		
複製代碼

而後分發給mNativeModuleCallExceptionHandler進行處理,而mNativeModuleCallExceptionHandler又是經過ReactInstanceManager進行賦值的。

private ReactApplicationContext createReactContext(
      JavaScriptExecutor jsExecutor,
      JSBundleLoader jsBundleLoader) {
	...省略
    final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);

    NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null
        ? mNativeModuleCallExceptionHandler
        : mDevSupportManager;
    reactContext.setNativeModuleCallExceptionHandler(exceptionHandler);
    ...省略
    return reactContext;
 }
複製代碼

而這裏的mNativeModuleCallExceptionHandler就是從外面傳遞過來的,這和上面咱們傳遞給ReactInstanceManagerExceptionHandler是同一個對象。

4、總結

通過上面對啓動期和運行期RN異常場景的分析,咱們發現可使用自定義的一個ExceptionHandler對象對RN異常進行處理。只不過對於啓動期的異常,須要咱們對源碼進行修改,以便在非開發模式下能異常可以被咱們自定義的ExceptionHandler所捕獲。 在異常被捕獲後,須要對異常信息進一步的處理。能夠存儲到本地,也能夠發送給後臺,而後在線分析異常信息,這一步具體該如何操做就須要根據業務需求決定了。

5、參考文章

相關文章
相關標籤/搜索