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、参考文章

相关文章
相关标签/搜索