美文网首页
React Native 异常处理

React Native 异常处理

作者: AndroidHint | 来源:发表于2019-05-19 14:51 被阅读0次

    一、引言

    公司最近在大力推崇使用React Native(以下简称RN)来开发业务组件,来代替原生业务组件,以达到快速迭代、方便热修复等目的。虽然RN拥有比混合H5开发更好的性能体验,性能直逼原生,但是毕竟RN是一个新的框架,可能潜在不少问题。所以,我们希望能对RN的异常进行捕获,并进行上报处理,以便后期分析解决这些异常,优化用户体验。

    RN异常在大方向上可以分为启动期异常和运行期异常。下面就针对这两种异常进行分析。

    二、启动期异常

    启动期我们可以认为从调用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方法。

    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住,否则该异常就被抛出来了。

    所以,我们需要在这里进行一个修改:

    在不是开发者模式时,使用自定义的ExceptionHandler去处理启动期的异常。为了在一个地方集中处理异常,可以使用ReactInstanceManager中的ExceptionHandler进行处理。

    三、运行期异常

    运行期异常比启动期异常要复杂一些,下面罗列了八个运行时异常的场景,如果我们能够将这八个异常场景覆盖住,那么基本上就能达到目标。

    • JS调用Native模块,Native模块不存在
    • JS调用Native模块,函数原型不一致
    • JS调用Native模块,Native模块运行异常
    • Native调用JS模块,JS模块不存在
    • Native调用JS模块,函数原型不一致
    • Native调用JS模块,JS模块运行异常
    • JS本身代码运行异常
    • UI操作异常

    3.1 运行线程

    在解析运行期异常前,我们先来谈一下RN中的运行线程。RN在初始化时维护了三个队列,分别是:

    • UIQueue,专门执行UI操作。这里使用的是Android原生的UI线程
    • NativeQueue,执行Native模块方法的操作。通常由JS发起,它是一个后台线程
    • JSQueue,执行JS逻辑。它是一个后台线程

    因此三个队列对应着三个线程。具体来说,JS代码、Native代码是运行在这几个线程的,我们需要理解一些关于RN的Bridge原理。但由于这不是本文的重点,所以有兴趣的同学可以自行查找相关文章。

    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是同一个对象。

    四、总结

    经过上面对启动期和运行期RN异常场景的分析,我们发现可以使用自定义的一个ExceptionHandler对象对RN异常进行处理。只不过对于启动期的异常,需要我们对源码进行修改,以便在非开发模式下能异常能够被我们自定义的ExceptionHandler所捕获。
    在异常被捕获后,需要对异常信息进一步的处理。可以存储到本地,也可以发送给后台,然后在线分析异常信息,这一步具体该如何操作就需要根据业务需求决定了。

    五、参考文章

    相关文章

      网友评论

          本文标题:React Native 异常处理

          本文链接:https://www.haomeiwen.com/subject/wfhaaqtx.html